From 62e340f0035ee2f5bedb31c6d9305e53887c0a86 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 14 Dec 2020 23:06:09 -0500 Subject: [PATCH 1/8] feat: hmr support for vite 2.0 --- package.json | 2 +- src/handleHotUpdate.ts | 148 +++++++++++++++++++++++++++++++++++++++++ src/index.ts | 8 ++- src/script.ts | 2 +- src/sfc.ts | 19 +++++- src/template.ts | 9 ++- src/utils/error.ts | 1 + yarn.lock | 8 +++ 8 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 src/handleHotUpdate.ts diff --git a/package.json b/package.json index ee254b9..eaa6bd0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "dependencies": { "debug": "^4.1.1", "hash-sum": "^2.0.0", - "rollup-pluginutils": "^2.8.2" + "@rollup/pluginutils": "^4.1.0" }, "peerDependencies": { "@vue/compiler-sfc": "*" diff --git a/src/handleHotUpdate.ts b/src/handleHotUpdate.ts new file mode 100644 index 0000000..4da6f19 --- /dev/null +++ b/src/handleHotUpdate.ts @@ -0,0 +1,148 @@ +import fs from 'fs' +import { parse, SFCBlock } from '@vue/compiler-sfc' +import { getDescriptor, setDescriptor } from './utils/descriptorCache' + +/** + * Vite-specific HMR handling + */ +export async function handleHotUpdate(file: string, modules: any[]) { + if (!file.endsWith('.vue')) { + return + } + + const prevDescriptor = getDescriptor(file) + if (!prevDescriptor) { + // file hasn't been requested yet (e.g. async component) + return + } + + let content = fs.readFileSync(file, 'utf-8') + if (!content) { + await untilModified(file) + content = fs.readFileSync(file, 'utf-8') + } + + const { descriptor } = parse(content, { + filename: file, + sourceMap: true, + sourceRoot: process.cwd(), + }) + setDescriptor(file, descriptor) + + let needRerender = false + const filteredModules = [] + + const reload = () => { + console.log(`[vue:reload] ${file}`) + return modules.filter((m) => /type=script/.test(m.id)) + } + + if ( + !isEqualBlock(descriptor.script, prevDescriptor.script) || + !isEqualBlock(descriptor.scriptSetup, prevDescriptor.scriptSetup) + ) { + return reload() + } + + if (!isEqualBlock(descriptor.template, prevDescriptor.template)) { + needRerender = true + } + + let didUpdateStyle = false + const prevStyles = prevDescriptor.styles || [] + const nextStyles = descriptor.styles || [] + + // 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 + // event so the client busts the sw cache. + if ( + prevStyles.some((s) => s.module != null) || + nextStyles.some((s) => s.module != null) + ) { + return reload() + } + + // force reload if CSS vars injection changed + if (descriptor.cssVars) { + if (prevDescriptor.cssVars.join('') !== descriptor.cssVars.join('')) { + return reload() + } + } + + // force reload if scoped status has changed + if (prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped)) { + return reload() + } + + // only need to update styles if not reloading, since reload forces + // style updates as well. + nextStyles.forEach((_, i) => { + if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) { + didUpdateStyle = true + filteredModules.push(modules.find((m) => m.id.includes(`index=${i}`))) + } + }) + + const prevCustoms = prevDescriptor.customBlocks || [] + const nextCustoms = descriptor.customBlocks || [] + + // custom blocks update causes a reload + // because the custom block contents is changed and it may be used in JS. + if ( + nextCustoms.some( + (_, i) => !prevCustoms[i] || !isEqualBlock(prevCustoms[i], nextCustoms[i]) + ) + ) { + return reload() + } + + if (needRerender) { + filteredModules.push(modules.find((m) => /type=template/.test(m.id))) + } + + let updateType = [] + if (needRerender) { + updateType.push(`template`) + } + if (didUpdateStyle) { + updateType.push(`style`) + } + if (updateType.length) { + console.log(`[vue:update(${updateType.join('&')})] ${file}`) + } + return filteredModules +} + +// vitejs/vite#610 when hot-reloading Vue files, we read immediately on file +// change event and sometimes this can be too early and get an empty buffer. +// Poll until the file's modified time has changed before reading again. +async function untilModified(file: string) { + const mtime = fs.statSync(file).mtimeMs + return new Promise((r) => { + let n = 0 + const poll = async () => { + n++ + const newMtime = fs.statSync(file).mtimeMs + if (newMtime !== mtime || n > 10) { + r(0) + } else { + setTimeout(poll, 10) + } + } + setTimeout(poll, 10) + }) +} + +function isEqualBlock(a: SFCBlock | null, b: SFCBlock | null) { + if (!a && !b) return true + if (!a || !b) return false + // src imports will trigger their own updates + if (a.src && b.src && a.src === b.src) return true + if (a.content !== b.content) return false + const keysA = Object.keys(a.attrs) + const keysB = Object.keys(b.attrs) + if (keysA.length !== keysB.length) { + return false + } + return keysA.every((key) => a.attrs[key] === b.attrs[key]) +} diff --git a/src/index.ts b/src/index.ts index f89a059..cb4afee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ import { import fs from 'fs' import createDebugger from 'debug' import { Plugin } from 'rollup' -import { createFilter } from 'rollup-pluginutils' +import { createFilter } from '@rollup/pluginutils' import { transformSFCEntry } from './sfc' import { transformTemplate } from './template' import { transformStyle } from './style' @@ -23,6 +23,7 @@ import { getDescriptor, setDescriptor } from './utils/descriptorCache' import { parseVuePartRequest } from './utils/query' import { normalizeSourceMap } from './utils/sourceMap' import { getResolvedScript } from './script' +import { handleHotUpdate } from './handleHotUpdate' const debug = createDebugger('rollup-plugin-vue') @@ -30,6 +31,7 @@ export interface Options { include: string | RegExp | (string | RegExp)[] exclude: string | RegExp | (string | RegExp)[] target: 'node' | 'browser' + hmr: boolean exposeFilename: boolean customBlocks?: string[] @@ -58,6 +60,7 @@ export interface Options { const defaultOptions: Options = { include: /\.vue$/, exclude: [], + hmr: false, target: 'browser', exposeFilename: false, customBlocks: [], @@ -173,6 +176,9 @@ export default function PluginVue(userOptions: Partial = {}): Plugin { } return null }, + + // @ts-ignore + handleHotUpdate, } } diff --git a/src/script.ts b/src/script.ts index 4e9c9b6..99b3077 100644 --- a/src/script.ts +++ b/src/script.ts @@ -40,7 +40,7 @@ export function resolveScript( resolved = compileScript(descriptor, { id: scopeId, isProd, - inlineTemplate: true, + inlineTemplate: !options.hmr, templateOptions: getTemplateCompilerOptions( options, descriptor, diff --git a/src/sfc.ts b/src/sfc.ts index d52c00d..3bce678 100644 --- a/src/sfc.ts +++ b/src/sfc.ts @@ -42,9 +42,11 @@ export function transformSFCEntry( // feature information const hasScoped = descriptor.styles.some((s) => s.scoped) - const isTemplateInlined = - descriptor.scriptSetup && !(descriptor.template && descriptor.template.src) - const hasTemplateImport = descriptor.template && !isTemplateInlined + const useInlineTemplate = + !options.hmr && + descriptor.scriptSetup && + !(descriptor.template && descriptor.template.src) + const hasTemplateImport = descriptor.template && !useInlineTemplate const templateImport = hasTemplateImport ? genTemplateCode(descriptor, scopeId, isServer) @@ -84,6 +86,17 @@ export function transformSFCEntry( ) } output.push('export default script') + + if (options.hmr) { + output.push(`script.__hmrId = ${JSON.stringify(scopeId)}`) + output.push(`__VUE_HMR_RUNTIME__.createRecord(script.__hmrId, script)`) + output.push( + `import.meta.hot.accept(({ default: script }) => { + __VUE_HMR_RUNTIME__.reload(script.__hmrId, script) +})` + ) + } + return { code: output.join('\n'), map: { diff --git a/src/template.ts b/src/template.ts index b23a8f9..6d919cb 100644 --- a/src/template.ts +++ b/src/template.ts @@ -46,8 +46,15 @@ export function transformTemplate( ) } + let returnCode = result.code + if (options.hmr) { + returnCode += `\nimport.meta.hot.accept(({ render }) => { + __VUE_HMR_RUNTIME__.rerender(${JSON.stringify(query.id)}, render) + })` + } + return { - code: result.code, + code: returnCode, map: normalizeSourceMap(result.map!, request), } } diff --git a/src/utils/error.ts b/src/utils/error.ts index 4977e1b..ed904c8 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -5,6 +5,7 @@ export function createRollupError( id: string, error: CompilerError | SyntaxError ): RollupError { + debugger if ('code' in error) { return { id, diff --git a/yarn.lock b/yarn.lock index 1a4bb89..5a41a9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -523,6 +523,14 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@rollup/pluginutils@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.0.tgz#0dcc61c780e39257554feb7f77207dceca13c838" + integrity sha512-TrBhfJkFxA+ER+ew2U2/fHbebhLT/l/2pRk0hfj9KusXUuRXd2v0R58AfaZK9VXDQ4TogOSEmICVrQAA3zFnHQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" From 53b0e4053c7146e35048927c53e5527c2a391f69 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 15 Dec 2020 15:58:47 -0500 Subject: [PATCH 2/8] wip: more efficient style handling for vite --- src/handleHotUpdate.ts | 36 ++++++++++++++++++++---------------- src/index.ts | 7 ++++++- src/sfc.ts | 33 ++++++++++++++++++++++++--------- src/style.ts | 24 +++++++++++++++--------- src/utils/error.ts | 1 - 5 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/handleHotUpdate.ts b/src/handleHotUpdate.ts index 4da6f19..595c8c5 100644 --- a/src/handleHotUpdate.ts +++ b/src/handleHotUpdate.ts @@ -1,7 +1,10 @@ import fs from 'fs' +import _debug from 'debug' import { parse, SFCBlock } from '@vue/compiler-sfc' import { getDescriptor, setDescriptor } from './utils/descriptorCache' +const debug = _debug('vite:hmr') + /** * Vite-specific HMR handling */ @@ -33,8 +36,10 @@ export async function handleHotUpdate(file: string, modules: any[]) { const filteredModules = [] const reload = () => { - console.log(`[vue:reload] ${file}`) - return modules.filter((m) => /type=script/.test(m.id)) + debug(`[vue:reload] ${file}`) + return modules.filter( + (m) => !/type=/.test(m.id) || /type=script/.test(m.id) + ) } if ( @@ -52,16 +57,6 @@ export async function handleHotUpdate(file: string, modules: any[]) { const prevStyles = prevDescriptor.styles || [] const nextStyles = descriptor.styles || [] - // 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 - // event so the client busts the sw cache. - if ( - prevStyles.some((s) => s.module != null) || - nextStyles.some((s) => s.module != null) - ) { - return reload() - } - // force reload if CSS vars injection changed if (descriptor.cssVars) { if (prevDescriptor.cssVars.join('') !== descriptor.cssVars.join('')) { @@ -76,12 +71,21 @@ export async function handleHotUpdate(file: string, modules: any[]) { // only need to update styles if not reloading, since reload forces // style updates as well. - nextStyles.forEach((_, i) => { - if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) { + for (let i = 0; i < nextStyles.length; i++) { + const prev = prevStyles[i] + const next = nextStyles[i] + if (!prev || !isEqualBlock(prev, next)) { + // css modules update causes a reload because the $style object is changed + // and it may be used in JS. + // if (prev.module != null || next.module != null) { + // return modules.filter( + // (m) => !/type=/.test(m.id) || /type=script/.test(m.id) + // ) + // } didUpdateStyle = true filteredModules.push(modules.find((m) => m.id.includes(`index=${i}`))) } - }) + } const prevCustoms = prevDescriptor.customBlocks || [] const nextCustoms = descriptor.customBlocks || [] @@ -108,7 +112,7 @@ export async function handleHotUpdate(file: string, modules: any[]) { updateType.push(`style`) } if (updateType.length) { - console.log(`[vue:update(${updateType.join('&')})] ${file}`) + debug(`[vue:update(${updateType.join('&')})] ${file}`) } return filteredModules } diff --git a/src/index.ts b/src/index.ts index cb4afee..c61475a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,9 +31,9 @@ export interface Options { include: string | RegExp | (string | RegExp)[] exclude: string | RegExp | (string | RegExp)[] target: 'node' | 'browser' + vite: boolean hmr: boolean exposeFilename: boolean - customBlocks?: string[] // if true, handle preprocessors directly instead of delegating to other @@ -60,6 +60,7 @@ export interface Options { const defaultOptions: Options = { include: /\.vue$/, exclude: [], + vite: false, hmr: false, target: 'browser', exposeFilename: false, @@ -72,6 +73,10 @@ export default function PluginVue(userOptions: Partial = {}): Plugin { ...userOptions, } + if (options.vite) { + options.preprocessStyles = false + } + const isServer = options.target === 'node' const isProduction = process.env.NODE_ENV === 'production' || process.env.BUILD === 'production' diff --git a/src/sfc.ts b/src/sfc.ts index 3bce678..2ea39b3 100644 --- a/src/sfc.ts +++ b/src/sfc.ts @@ -66,7 +66,12 @@ export function transformSFCEntry( options, pluginContext ) - const stylesCode = genStyleCode(descriptor, scopeId, options.preprocessStyles) + const stylesCode = genStyleCode( + descriptor, + scopeId, + options.preprocessStyles, + options.vite + ) const customBlocksCode = getCustomBlock(descriptor, filterCustomBlock) const output = [ scriptImport, @@ -158,7 +163,8 @@ function genScriptCode( function genStyleCode( descriptor: SFCDescriptor, scopeId: string, - preprocessStyles?: boolean + preprocessStyles?: boolean, + isVite?: boolean ) { let stylesCode = `` let hasCSSModules = false @@ -188,7 +194,8 @@ function genStyleCode( i, styleRequest, styleRequestWithoutModule, - style.module + style.module, + isVite ) } else { stylesCode += `\nimport ${JSON.stringify(styleRequest)}` @@ -224,14 +231,22 @@ function genCSSModulesCode( index: number, request: string, requestWithoutModule: string, - moduleName: string | boolean + moduleName: string | boolean, + isVite?: boolean ): string { const styleVar = `style${index}` - let code = - // first import the CSS for extraction - `\nimport ${JSON.stringify(requestWithoutModule)}` + - // then import the json file to expose to component... - `\nimport ${styleVar} from ${JSON.stringify(request + '.js')}` + let code + if (!isVite) { + code = + // first import the CSS for extraction + `\nimport ${JSON.stringify(requestWithoutModule)}` + + // then import the json file to expose to component... + `\nimport ${styleVar} from ${JSON.stringify(request + '.js')}` + } else { + // vite handles module.ext in a single import + request = request.replace(/\.(\w+)$/, '.module.$1.js') + code = `\n import ${styleVar} from ${JSON.stringify(request)}` + } // inject variable const name = typeof moduleName === 'string' ? moduleName : '$style' diff --git a/src/style.ts b/src/style.ts index d00660d..5f66382 100644 --- a/src/style.ts +++ b/src/style.ts @@ -20,7 +20,7 @@ export async function transformStyle( const block = descriptor.styles[query.index]! let preprocessOptions = options.preprocessOptions || {} - const preprocessLang = (options.preprocessStyles + const preprocessLang = (options.preprocessStyles && !options.vite ? block.lang : undefined) as SFCAsyncStyleCompileOptions['preprocessLang'] @@ -52,7 +52,8 @@ export async function transformStyle( isProd: isProduction, source: code, scoped: block.scoped, - modules: !!block.module, + // vite handle CSS modules + modules: !!block.module && !options.vite, postcssOptions: options.postcssOptions, postcssPlugins: options.postcssPlugins, modulesOptions: options.cssModulesOptions, @@ -62,16 +63,21 @@ export async function transformStyle( }) if (result.errors.length) { - result.errors.forEach((error) => - pluginContext.error({ - id: query.filename, - message: error.message, - }) - ) + result.errors.forEach((error: any) => { + if (error.line && error.column) { + error.loc = { + file: query.filename, + line: error.line + block.loc.start.line, + column: error.column, + } + } + pluginContext.error(error) + }) return null } - if (query.module) { + if (query.module && !options.vite) { + // vite handles css modules code generation down the stream return { code: `export default ${JSON.stringify(result.modules)}`, map: null, diff --git a/src/utils/error.ts b/src/utils/error.ts index ed904c8..4977e1b 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -5,7 +5,6 @@ export function createRollupError( id: string, error: CompilerError | SyntaxError ): RollupError { - debugger if ('code' in error) { return { id, From 29bbe7ab499d787d7d5bfd435c03ace323469348 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 15 Dec 2020 16:23:22 -0500 Subject: [PATCH 3/8] wip: reused compiled script on template hmr --- src/handleHotUpdate.ts | 13 ++++++------- src/script.ts | 10 +++++++++- src/utils/descriptorCache.ts | 1 - 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/handleHotUpdate.ts b/src/handleHotUpdate.ts index 595c8c5..ae57f26 100644 --- a/src/handleHotUpdate.ts +++ b/src/handleHotUpdate.ts @@ -2,6 +2,7 @@ import fs from 'fs' import _debug from 'debug' import { parse, SFCBlock } from '@vue/compiler-sfc' import { getDescriptor, setDescriptor } from './utils/descriptorCache' +import { getResolvedScript, setResolvedScript } from './script' const debug = _debug('vite:hmr') @@ -50,6 +51,11 @@ export async function handleHotUpdate(file: string, modules: any[]) { } if (!isEqualBlock(descriptor.template, prevDescriptor.template)) { + // when a