Skip to content

Commit a759196

Browse files
Pooya Parsaatinux
authored andcommitted
feat(vue-app): build indicator (nuxt#5820)
* feat: inline HMR progress indicator * support router base * fix nuxt err * fix space * fix indentation * return in case of ws message parsing error * close ws on beforeDestroy * ui: Update loading indicator UI * builder: Add build.indicator option * ui: Use only logo and % * hotfix: Alphabetical order * hotfix: Add fixed with and add back v-if * minor style change * rename component to build-indicator * feat: animated progress * assign name to component * update test * naming consistency * render into app to prevent dom wrapping * extra new line * better App.js formatting * update snapshot * clear interval
1 parent e161d70 commit a759196

File tree

9 files changed

+165
-4
lines changed

9 files changed

+165
-4
lines changed

packages/builder/src/context/template.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default class TemplateContext {
2020
isDev: options.dev,
2121
isTest: options.test,
2222
debug: options.debug,
23+
buildIndicator: options.dev && options.build.indicator,
2324
vue: { config: options.vue.config },
2425
fetch: options.fetch,
2526
mode: options.mode,

packages/builder/test/context/__snapshots__/template.test.js.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ TemplateContext {
77
],
88
"templateVars": Object {
99
"appPath": "./App.js",
10+
"buildIndicator": undefined,
1011
"components": Object {
1112
"ErrorPage": "relativeBuild(test_error_page)",
1213
},

packages/config/src/config/build.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import env from 'std-env'
33
export default () => ({
44
quiet: Boolean(env.ci || env.test),
55
analyze: false,
6+
indicator: true,
67
profile: process.argv.includes('--profile'),
78
extractCSS: false,
89
crossorigin: undefined,

packages/config/test/__snapshots__/options.test.js.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Object {
5454
"useShortDoctype": true,
5555
},
5656
},
57+
"indicator": true,
5758
"loaders": Object {
5859
"css": Object {
5960
"sourceMap": false,

packages/config/test/config/__snapshots__/index.test.js.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Object {
4141
"useShortDoctype": true,
4242
},
4343
},
44+
"indicator": true,
4445
"loaders": Object {
4546
"css": Object {},
4647
"cssModules": Object {
@@ -374,6 +375,7 @@ Object {
374375
"useShortDoctype": true,
375376
},
376377
},
378+
"indicator": true,
377379
"loaders": Object {
378380
"css": Object {},
379381
"cssModules": Object {

packages/config/types/build.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export interface NuxtConfigurationBuild {
4949
hardSource?: boolean
5050
hotMiddleware?: WebpackHotMiddlewareOptions
5151
html?: { minify: HtmlMinifierOptions }
52+
indicator?: boolean
5253
loaders?: NuxtConfigurationLoaders
5354
optimization?: WebpackOptions.Optimization
5455
optimizeCSS?: OptimizeCssAssetsWebpackPluginOptions | boolean

packages/vue-app/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const template = {
1313
'server.js',
1414
'utils.js',
1515
'empty.js',
16+
'components/nuxt-build-indicator.vue',
1617
'components/nuxt-error.vue',
1718
'components/nuxt-loading.vue',
1819
'components/nuxt-child.js',

packages/vue-app/template/App.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Vue from 'vue'
22
<% if (loading) { %>import NuxtLoading from '<%= (typeof loading === "string" ? loading : "./components/nuxt-loading.vue") %>'<% } %>
3+
<%if (buildIndicator) { %>import NuxtBuildIndicator from './components/nuxt-build-indicator'<% } %>
34
<% css.forEach((c) => { %>
45
import '<%= relativeToBuild(resolvePath(c.src || c, { isStyle: true })) %>'
56
<% }) %>
@@ -49,10 +50,7 @@ export default {
4950
domProps: {
5051
id: '<%= globals.id %>'
5152
}
52-
}, [
53-
<% if (loading) { %>loadingEl,<% } %>
54-
transitionEl
55-
])
53+
}, [<% if (loading) { %>loadingEl, <% } %><%if (buildIndicator) { %>h(NuxtBuildIndicator), <% } %>transitionEl])
5654
},
5755
data: () => ({
5856
isOnline: true,
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<template>
2+
<transition appear>
3+
<div class="nuxt__build_indicator" v-if="building">
4+
<svg viewBox="0 0 96 72" version="1" xmlns="http://www.w3.org/2000/svg">
5+
<g fill="none" fill-rule="evenodd">
6+
<path d="M6 66h23l1-3 21-37L40 6 6 66zM79 66h11L62 17l-5 9 22 37v3zM54 31L35 66h38z"/>
7+
<path d="M29 69v-1-2H6L40 6l11 20 3-6L44 3s-2-3-4-3-3 1-5 3L1 63c0 1-2 3 0 6 0 1 2 2 5 2h28c-3 0-4-1-5-2z" fill="#00C58E"/>
8+
<path d="M95 63L67 14c0-1-2-3-5-3-1 0-3 0-4 3l-4 6 3 6 5-9 28 49H79a5 5 0 0 1 0 3c-2 2-5 2-5 2h16c1 0 4 0 5-2 1-1 2-3 0-6z" fill="#00C58E"/>
9+
<path d="M79 69v-1-2-3L57 26l-3-6-3 6-21 37-1 3a5 5 0 0 0 0 3c1 1 2 2 5 2h40s3 0 5-2zM54 31l19 35H35l19-35z" fill="#FFF" fill-rule="nonzero"/>
10+
</g>
11+
</svg>
12+
{{ animatedProgress }}%
13+
</div>
14+
</transition>
15+
</template>
16+
17+
<script>
18+
export default {
19+
name: 'nuxt-build-indicator',
20+
data() {
21+
return {
22+
building: false,
23+
progress: 0,
24+
animatedProgress: 0,
25+
reconnectAttempts: 0,
26+
}
27+
},
28+
mounted() {
29+
if (WebSocket === undefined) {
30+
return // Unsupported
31+
}
32+
this.wsConnect('<%= router.base %>_loading/ws')
33+
},
34+
beforeDestroy() {
35+
this.wsClose()
36+
},
37+
watch: {
38+
progress(val, oldVal) {
39+
// Cancel old animation
40+
clearInterval(this._progressAnimation)
41+
// Average progress may decrease but ignore it!
42+
if (val < oldVal) {
43+
return
44+
}
45+
// Jump to edge imediately
46+
if (val < 10 || val > 90) {
47+
this.animatedProgress = val
48+
}
49+
// Animate to value
50+
this._progressAnimation = setInterval(() => {
51+
const diff = this.progress - this.animatedProgress
52+
if (diff > 0) {
53+
this.animatedProgress++
54+
} else {
55+
clearInterval(this._progressAnimation)
56+
}
57+
}, 50)
58+
}
59+
},
60+
methods: {
61+
wsConnect(path) {
62+
if (path) {
63+
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
64+
this.wsURL = `${protocol}://${location.hostname}:${location.port}${path}`
65+
}
66+
67+
this.ws = new WebSocket(this.wsURL)
68+
this.ws.onclose = this.onWSClose.bind(this)
69+
this.ws.onerror = this.onWSError.bind(this)
70+
this.ws.onmessage = this.onWSMessage.bind(this)
71+
},
72+
73+
wsReconnect(e) {
74+
this.reconnectAttempts++
75+
if (this.reconnectAttempts > 10) {
76+
return
77+
}
78+
setTimeout(() => { this.wsConnect() }, 1000)
79+
},
80+
81+
onWSClose(e) {
82+
// https://tools.ietf.org/html/rfc6455#section-11.7
83+
if (e.code !== 1000 && e.code !== 1005) {
84+
this.wsReconnect() // Unkown error
85+
}
86+
},
87+
88+
onWSError(error) {
89+
if (error.code === 'ECONNREFUSED') {
90+
this.wsReconnect(error)
91+
}
92+
},
93+
94+
onWSMessage(msg) {
95+
let data = msg.data
96+
97+
try {
98+
if (data[0] === '{') {
99+
data = JSON.parse(data)
100+
}
101+
} catch (e) {
102+
return
103+
}
104+
105+
this.progress = Math.round(data.states.reduce((p, s) => p + s.progress, 0) / data.states.length)
106+
if (!data.allDone) {
107+
this.building = true
108+
} else {
109+
this.$nextTick(() => {
110+
this.building = false
111+
this.animatedProgress = 0
112+
this.progress = 0
113+
clearInterval(this._progressAnimation)
114+
})
115+
}
116+
},
117+
118+
wsClose() {
119+
if (this.ws) {
120+
this.ws.close()
121+
delete this.ws
122+
}
123+
}
124+
}
125+
}
126+
</script>
127+
128+
<style scopped>
129+
.nuxt__build_indicator {
130+
position: absolute;
131+
font-family: monospace;
132+
bottom: 20px;
133+
right: 20px;
134+
background-color: #2E495E;
135+
padding: 5px 10px;
136+
border-radius: 2px;
137+
box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.2);
138+
color: #00C48D;
139+
width: 54px;
140+
}
141+
.v-enter-active, .v-leave-active {
142+
transition-delay: 0.2s;
143+
transition-property: all;
144+
transition-duration: 0.3s;
145+
}
146+
.v-leave-to {
147+
opacity: 0;
148+
transform: translateY(20px);
149+
}
150+
svg {
151+
width: 1.1em;
152+
position: relative;
153+
top: 1px;
154+
}
155+
</style>

0 commit comments

Comments
 (0)