diff --git a/src/client/client.ts b/src/client/client.ts index 909b676742d594..0da7503e696b72 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -63,15 +63,9 @@ function warnFailedFetch(err: Error, path: string | string[]) { // Listen for messages socket.addEventListener('message', async ({ data }) => { - const { - type, - path, - changeSrcPath, - id, - index, - timestamp, - customData - } = JSON.parse(data) + const { type, path, changeSrcPath, id, timestamp, customData } = JSON.parse( + data + ) if (changeSrcPath) { await bustSwCache(changeSrcPath) @@ -84,35 +78,10 @@ socket.addEventListener('message', async ({ data }) => { case 'connected': console.log(`[vite] connected.`) break - case 'vue-reload': - import(`${path}?t=${timestamp}`) - .then((m) => { - __VUE_HMR_RUNTIME__.reload(path, m.default) - console.log(`[vite] ${path} reloaded.`) - }) - .catch((err) => warnFailedFetch(err, path)) - break - case 'vue-rerender': - const templatePath = `${path}?type=template` - await bustSwCache(templatePath) - import(`${templatePath}&t=${timestamp}`).then((m) => { - __VUE_HMR_RUNTIME__.rerender(path, m.render) - console.log(`[vite] ${path} template updated.`) - }) - break - case 'vue-style-update': - const stylePath = `${path}?type=style&index=${index}` - await bustSwCache(stylePath) - const content = await import(stylePath + `&t=${timestamp}`) - updateStyle(id, content.default) - console.log( - `[vite] ${path} style${index > 0 ? `#${index}` : ``} updated.` - ) - break case 'style-update': - await bustSwCache(`${path}?import`) - const style = await import(`${path}?t=${timestamp}`) - updateStyle(id, style.default) + const hasQuery = path.includes('?') ? '&' : '?' + await bustSwCache(`${path}${hasQuery}import`) + await import(`${path}${hasQuery}t=${timestamp}`) console.log(`[vite] ${path} updated.`) break case 'style-remove': @@ -220,8 +189,14 @@ async function updateModule( Array.from(modulesToUpdate).map(async (dep) => { const disposer = jsDisposeMap.get(dep) if (disposer) await disposer() - const newMod = await import(dep + `?t=${timestamp}`) - moduleMap.set(dep, newMod) + try { + const newMod = await import( + dep + (dep.includes('?') ? '&' : '?') + `t=${timestamp}` + ) + moduleMap.set(dep, newMod) + } catch (e) { + warnFailedFetch(e, dep) + } }) ) diff --git a/src/node/server/serverPluginCss.ts b/src/node/server/serverPluginCss.ts index d78a64d9f05fd2..00bad318fd18f7 100644 --- a/src/node/server/serverPluginCss.ts +++ b/src/node/server/serverPluginCss.ts @@ -1,10 +1,10 @@ import { ServerPlugin } from '.' -import { hmrClientId } from './serverPluginHmr' import hash_sum from 'hash-sum' import { Context } from 'koa' import { cleanUrl, isImportRequest, readBody } from '../utils' import { srcImportMap, vueCache } from './serverPluginVue' import { + codegenCss, compileCss, cssPreprocessLangRE, rewriteCssUrls @@ -34,33 +34,21 @@ export const cssPlugin: ServerPlugin = ({ // note ctx.body could be null if upstream set status to 304 ctx.body ) { + const id = JSON.stringify(hash_sum(ctx.path)) if (isImportRequest(ctx)) { await processCss(root, ctx) // we rewrite css with `?import` to a js module that inserts a style // tag linking to the actual raw url ctx.type = 'js' - const id = JSON.stringify(hash_sum(ctx.path)) - let code = - `import { updateStyle } from "${hmrClientId}"\n` + - `const css = ${JSON.stringify(processedCSS.get(ctx.path)!.css)}\n` + - `updateStyle(${id}, css)\n` - if (ctx.path.endsWith('.module.css')) { - code += `export default ${JSON.stringify( - processedCSS.get(ctx.path)!.modules - )}` - } else { - code += `export default css` - } - ctx.body = code.trim() + const { css, modules } = processedCSS.get(ctx.path)! + ctx.body = codegenCss(id, css, modules) } else { // raw request, return compiled css if (!processedCSS.has(ctx.path)) { await processCss(root, ctx) } ctx.type = 'js' - ctx.body = `export default ${JSON.stringify( - processedCSS.get(ctx.path)!.css - )}` + ctx.body = codegenCss(id, processedCSS.get(ctx.path)!.css) } } }) @@ -78,10 +66,8 @@ export const cssPlugin: ServerPlugin = ({ chalk.green(`[vite:hmr] `) + `${publicPath} updated. (style)` ) watcher.send({ - type: 'vue-style-update', - path: publicPath, - index: Number(index), - id: `${hash_sum(publicPath)}-${index}`, + type: 'style-update', + path: `${publicPath}?type=style&index=${index}`, timestamp: Date.now() }) return @@ -94,14 +80,12 @@ export const cssPlugin: ServerPlugin = ({ } const publicPath = resolver.fileToRequest(file) - const id = hash_sum(publicPath) // bust process cache processedCSS.delete(publicPath) watcher.send({ type: 'style-update', - id, path: publicPath, timestamp: Date.now() }) diff --git a/src/node/server/serverPluginHmr.ts b/src/node/server/serverPluginHmr.ts index f166f445ef00cd..756f392d05f5f6 100644 --- a/src/node/server/serverPluginHmr.ts +++ b/src/node/server/serverPluginHmr.ts @@ -73,9 +73,6 @@ export const hmrClientPublicPath = `/${hmrClientId}` interface HMRPayload { type: - | 'vue-rerender' - | 'vue-reload' - | 'vue-style-update' | 'js-update' | 'style-update' | 'style-remove' @@ -178,12 +175,24 @@ export const hmrPlugin: ServerPlugin = ({ } // check which part of the file changed - let needReload = false - let needCssModuleReload = false let needRerender = false + const vueReload = () => { + send({ + type: 'js-update', + path: publicPath, + changeSrcPath: publicPath, + timestamp + }) + console.log( + chalk.green(`[vite:hmr] `) + + `${path.relative(root, file)} updated. (reload)` + ) + } + if (!isEqual(descriptor.script, prevDescriptor.script)) { - needReload = true + vueReload() + return } if (!isEqual(descriptor.template, prevDescriptor.template)) { @@ -194,12 +203,6 @@ export const hmrPlugin: ServerPlugin = ({ const styleId = hash_sum(publicPath) const prevStyles = prevDescriptor.styles || [] const nextStyles = descriptor.styles || [] - if ( - !needReload && - prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped) - ) { - needReload = true - } // css modules update causes a reload because the $style object is changed // and it may be used in JS. It also needs to trigger a vue-style-update @@ -208,25 +211,26 @@ export const hmrPlugin: ServerPlugin = ({ prevStyles.some((s) => s.module != null) || nextStyles.some((s) => s.module != null) ) { - needCssModuleReload = true + vueReload() + return + } + + if (prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped)) { + needRerender = true } // only need to update styles if not reloading, since reload forces // style updates as well. - if (!needReload) { - nextStyles.forEach((_, i) => { - if (!prevStyles[i] || !isEqual(prevStyles[i], nextStyles[i])) { - didUpdateStyle = true - send({ - type: 'vue-style-update', - path: publicPath, - index: i, - id: `${styleId}-${i}`, - timestamp - }) - } - }) - } + nextStyles.forEach((_, i) => { + if (!prevStyles[i] || !isEqual(prevStyles[i], nextStyles[i])) { + didUpdateStyle = true + send({ + type: 'style-update', + path: `${publicPath}?type=style&index=${i}`, + timestamp + }) + } + }) // stale styles always need to be removed prevStyles.slice(nextStyles.length).forEach((_, i) => { @@ -239,22 +243,17 @@ export const hmrPlugin: ServerPlugin = ({ }) }) - if (needReload || needCssModuleReload) { - send({ - type: 'vue-reload', - path: publicPath, - timestamp - }) - } else if (needRerender) { + if (needRerender) { send({ - type: 'vue-rerender', + type: 'js-update', path: publicPath, + changeSrcPath: `${publicPath}?type=template`, timestamp }) } - if (needReload || needRerender || didUpdateStyle) { - let updateType = needReload ? `reload` : needRerender ? `template` : `` + if (needRerender || didUpdateStyle) { + let updateType = needRerender ? `template` : `` if (didUpdateStyle) { updateType += ` & style` } @@ -308,7 +307,7 @@ export const hmrPlugin: ServerPlugin = ({ `${vueBoundary} reloaded due to change in ${relativeFile}.` ) send({ - type: 'vue-reload', + type: 'js-update', path: vueBoundary, changeSrcPath: publicPath, timestamp @@ -385,6 +384,7 @@ function isHmrAccepted(importer: string, dep: string): boolean { function isEqual(a: SFCBlock | null, b: SFCBlock | null) { if (!a && !b) return true if (!a || !b) return false + if (a.content.length !== b.content.length) return false if (a.content !== b.content) return false const keysA = Object.keys(a.attrs) const keysB = Object.keys(b.attrs) diff --git a/src/node/server/serverPluginModuleRewrite.ts b/src/node/server/serverPluginModuleRewrite.ts index db7241d4844f02..538dc2c877aa9f 100644 --- a/src/node/server/serverPluginModuleRewrite.ts +++ b/src/node/server/serverPluginModuleRewrite.ts @@ -150,10 +150,7 @@ export function rewriteImports( let resolved if (id === hmrClientId) { resolved = hmrClientPublicPath - if ( - /\bhot\b/.test(source.substring(ss, se)) && - !/.vue$|.vue\?type=/.test(importer) - ) { + if (/\bhot\b/.test(source.substring(ss, se))) { // the user explicit imports the HMR API in a js file // making the module hot. rewriteFileWithHMR(root, source, importer, resolver, s) diff --git a/src/node/server/serverPluginVue.ts b/src/node/server/serverPluginVue.ts index 635e1330b5d13a..b51bdadac12be2 100644 --- a/src/node/server/serverPluginVue.ts +++ b/src/node/server/serverPluginVue.ts @@ -28,7 +28,7 @@ import { Context } from 'koa' import { transform } from '../esbuildService' import { InternalResolver } from '../resolver' import { seenUrls } from './serverPluginServeStatic' -import { compileCss, rewriteCssUrls } from '../utils/cssUtils' +import { codegenCss, compileCss, rewriteCssUrls } from '../utils/cssUtils' const debug = require('debug')('vite:sfc') const getEtag = require('etag') @@ -114,6 +114,7 @@ export const vuePlugin: ServerPlugin = ({ if (styleBlock.src) { filename = await resolveSrcImport(styleBlock, ctx, resolver) } + const id = hash_sum(publicPath) const result = await compileSFCStyle( root, styleBlock, @@ -121,13 +122,8 @@ export const vuePlugin: ServerPlugin = ({ filename, publicPath ) - if (query.module != null) { - ctx.type = 'js' - ctx.body = `export default ${JSON.stringify(result.modules)}` - } else { - ctx.type = 'js' - ctx.body = `export default ${JSON.stringify(result.code)}` - } + ctx.type = 'js' + ctx.body = codegenCss(`${id}-${index}`, result.code, result.modules) return etagCacheCheck(ctx) } @@ -219,7 +215,8 @@ async function compileSFCMain( return cached.script } - let code = '' + const id = hash_sum(publicPath) + let code = `\nimport { updateStyle, hot } from "${hmrClientId}"\n` if (descriptor.script) { let content = descriptor.script.content if (descriptor.script.lang === 'ts') { @@ -231,11 +228,15 @@ async function compileSFCMain( code += `const __script = {}` } - const id = hash_sum(publicPath) + code += `\n if (__DEV__) { + hot.accept((m) => { + __VUE_HMR_RUNTIME__.reload("${id}", m.default) + }) +}` + let hasScoped = false let hasCSSModules = false if (descriptor.styles) { - code += `\nimport { updateStyle } from "${hmrClientId}"\n` descriptor.styles.forEach((s, i) => { const styleRequest = publicPath + `?type=style&index=${i}` if (s.scoped) hasScoped = true @@ -250,9 +251,9 @@ async function compileSFCMain( styleRequest + '&module' )}` code += `\n__cssModules[${JSON.stringify(moduleName)}] = ${styleVar}` + } else { + code += `\nimport ${JSON.stringify(styleRequest)}` } - code += `\nimport css_${i} from ${JSON.stringify(styleRequest)}` - code += `\nupdateStyle("${id}-${i}", css_${i})` }) if (hasScoped) { code += `\n__script.__scopeId = "data-v-${id}"` @@ -260,12 +261,18 @@ async function compileSFCMain( } if (descriptor.template) { + const templateRequest = publicPath + `?type=template` code += `\nimport { render as __render } from ${JSON.stringify( - publicPath + `?type=template` + templateRequest )}` code += `\n__script.render = __render` + code += `\n if (__DEV__) { + hot.accept(${JSON.stringify(templateRequest)}, (m) => { + __VUE_HMR_RUNTIME__.rerender("${id}", m.render) + }) +}` } - code += `\n__script.__hmrId = ${JSON.stringify(publicPath)}` + code += `\n__script.__hmrId = ${JSON.stringify(id)}` code += `\n__script.__file = ${JSON.stringify(filePath)}` code += `\nexport default __script` diff --git a/src/node/utils/cssUtils.ts b/src/node/utils/cssUtils.ts index d0610f589e9bb3..657efaeacf31ac 100644 --- a/src/node/utils/cssUtils.ts +++ b/src/node/utils/cssUtils.ts @@ -9,6 +9,7 @@ import { SFCAsyncStyleCompileOptions, SFCStyleCompileResults } from '@vue/compiler-sfc' +import { hmrClientPublicPath } from '../server/serverPluginHmr' export const urlRE = /(url\(\s*['"]?)([^"')]+)(["']?\s*\))/ export const cssPreprocessLangRE = /(.+).(less|sass|scss|styl|stylus)$/ @@ -81,6 +82,23 @@ export async function compileCss( }) } +export function codegenCss( + id: string, + css: string, + modules?: Record +): string { + let code = + `import { updateStyle } from "${hmrClientPublicPath}"\n` + + `const css = ${JSON.stringify(css)}\n` + + `updateStyle(${JSON.stringify(id)}, css)\n` + if (modules) { + code += `export default ${JSON.stringify(modules)}` + } else { + code += `export default css` + } + return code +} + // postcss-load-config doesn't expose Result type type PostCSSConfigResult = ReturnType extends Promise ? T