From 9bc3fbde36d8a526da9a328f603e34d9e1a8081f Mon Sep 17 00:00:00 2001 From: underfin <2218301630@qq.com> Date: Sat, 30 May 2020 07:12:47 +0800 Subject: [PATCH] feat: support css `@import` hmr (#281) --- package.json | 2 + playground/App.vue | 3 + playground/css-@import/TestCssAtImport.vue | 19 +++++ playground/css-@import/imported.css | 3 + .../css-@import/testCssAtImportFromScript.css | 1 + .../css-@import/testCssAtImportFromStyle.css | 3 + src/node/build/buildPluginCss.ts | 2 +- src/node/server/serverPluginCss.ts | 78 +++++++++++++------ src/node/server/serverPluginVue.ts | 3 +- src/node/utils/cssUtils.ts | 63 ++++++++++++--- test/test.js | 23 ++++++ yarn.lock | 28 ++++++- 12 files changed, 189 insertions(+), 39 deletions(-) create mode 100644 playground/css-@import/TestCssAtImport.vue create mode 100644 playground/css-@import/imported.css create mode 100644 playground/css-@import/testCssAtImportFromScript.css create mode 100644 playground/css-@import/testCssAtImportFromStyle.css diff --git a/package.json b/package.json index 8f9490fab1d170..c483342e8f4c63 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "open": "^7.0.3", "ora": "^4.0.4", "postcss": "^7.0.28", + "postcss-import": "^12.0.1", "postcss-load-config": "^2.1.0", "postcss-modules": "^2.0.0", "resolve": "^1.17.0", @@ -103,6 +104,7 @@ "@types/hash-sum": "^1.0.0", "@types/jest": "^25.2.1", "@types/node": "^13.13.1", + "@types/postcss-import": "^12.0.0", "@types/postcss-load-config": "^2.0.1", "@types/serve-handler": "^6.1.0", "@types/ws": "^7.2.4", diff --git a/playground/App.vue b/playground/App.vue index 9e1211b393e390..81b380ce05cb46 100644 --- a/playground/App.vue +++ b/playground/App.vue @@ -14,6 +14,7 @@ + @@ -42,6 +43,7 @@ import TestJsx from './TestJsx.vue' import TestAlias from './TestAlias.vue' import TestTransform from './TestTransform.vue' import TestRewriteOptimized from "./rewrite-optimized/TestRewriteOptimized.vue"; +import TestCssAtImport from './css-@import/TestCssAtImport.vue' export default { data: () => ({ @@ -56,6 +58,7 @@ export default { TestScopedCss, TestCssModules, TestPreprocessors, + TestCssAtImport, TestSrcImport, TestAssets, TestJsonImport, diff --git a/playground/css-@import/TestCssAtImport.vue b/playground/css-@import/TestCssAtImport.vue new file mode 100644 index 00000000000000..2d0057863a9ed0 --- /dev/null +++ b/playground/css-@import/TestCssAtImport.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/playground/css-@import/imported.css b/playground/css-@import/imported.css new file mode 100644 index 00000000000000..954e5708f09d0c --- /dev/null +++ b/playground/css-@import/imported.css @@ -0,0 +1,3 @@ +.script-at-import { + color: green; +} diff --git a/playground/css-@import/testCssAtImportFromScript.css b/playground/css-@import/testCssAtImportFromScript.css new file mode 100644 index 00000000000000..83dcc223c86630 --- /dev/null +++ b/playground/css-@import/testCssAtImportFromScript.css @@ -0,0 +1 @@ +@import './imported.css' diff --git a/playground/css-@import/testCssAtImportFromStyle.css b/playground/css-@import/testCssAtImportFromStyle.css new file mode 100644 index 00000000000000..1557d3b3c0d5a5 --- /dev/null +++ b/playground/css-@import/testCssAtImportFromStyle.css @@ -0,0 +1,3 @@ +.sfc-style-at-import { + color: red; +} diff --git a/src/node/build/buildPluginCss.ts b/src/node/build/buildPluginCss.ts index a5441c0a497e22..319674f4eb0177 100644 --- a/src/node/build/buildPluginCss.ts +++ b/src/node/build/buildPluginCss.ts @@ -34,7 +34,7 @@ export const createBuildCssPlugin = ( const result = await compileCss(root, id, { id: '', source: css, - filename: path.basename(id), + filename: id, scoped: false, modules: id.endsWith('.module.css'), preprocessLang: id.replace(cssPreprocessLangRE, '$2') as any diff --git a/src/node/server/serverPluginCss.ts b/src/node/server/serverPluginCss.ts index 3cd4c63ed3e368..da5356ab3c0b1d 100644 --- a/src/node/server/serverPluginCss.ts +++ b/src/node/server/serverPluginCss.ts @@ -7,7 +7,9 @@ import { srcImportMap, vueCache } from './serverPluginVue' import { codegenCss, compileCss, + cssImportMap, cssPreprocessLangRE, + getCssImportBoundaries, rewriteCssUrls } from '../utils/cssUtils' import qs from 'querystring' @@ -55,7 +57,11 @@ export const cssPlugin: ServerPlugin = ({ root, app, watcher, resolver }) => { const publicPath = resolver.fileToRequest(filePath) /** filter unused files */ - if (!processedCSS.has(publicPath) && !srcImportMap.has(filePath)) { + if ( + !cssImportMap.has(filePath) && + !processedCSS.has(publicPath) && + !srcImportMap.has(filePath) + ) { return debugCSS( `${basename(publicPath)} has changed, but it is not currently in use` ) @@ -66,45 +72,69 @@ export const cssPlugin: ServerPlugin = ({ root, app, watcher, resolver }) => { // it cannot be handled as simple css import because it may be scoped const styleImport = srcImportMap.get(filePath) vueCache.del(filePath) - const publicPath = cleanUrl(styleImport) - const index = qs.parse(styleImport.split('?', 2)[1]).index - console.log( - chalk.green(`[vite:hmr] `) + `${publicPath} updated. (style)` - ) - watcher.send({ - type: 'style-update', - path: `${publicPath}?type=style&index=${index}`, - timestamp: Date.now() - }) + vueStyleUpdate(styleImport) return } // handle HMR for module.css - // it cannot process with normal css, the class which in module.css maybe removed + // it cannot be handled as normal css because the js exports may change if (filePath.endsWith('.module.css')) { - watcher.handleJSReload(filePath, Date.now()) + moduleCssUpdate(filePath) return } - // bust process cache - processedCSS.delete(publicPath) - - watcher.send({ - type: 'style-update', - path: publicPath, - timestamp: Date.now() - }) + const boundaries = getCssImportBoundaries(filePath) + if (boundaries.size) { + for (let boundary of boundaries) { + if (boundary.includes('.module')) { + moduleCssUpdate(boundary) + } else if (boundary.includes('.vue')) { + vueCache.del(cleanUrl(boundary)) + vueStyleUpdate(resolver.fileToRequest(boundary)) + } else { + normalCssUpdate(resolver.fileToRequest(boundary)) + } + } + return + } + // no boundaries + normalCssUpdate(publicPath) } }) - async function processCss(root: string, ctx: Context) { - let css = (await readBody(ctx.body))! + function vueStyleUpdate(styleImport: string) { + const publicPath = cleanUrl(styleImport) + const index = qs.parse(styleImport.split('?', 2)[1]).index + console.log(chalk.green(`[vite:hmr] `) + `${publicPath} updated. (style)`) + watcher.send({ + type: 'style-update', + path: `${publicPath}?type=style&index=${index}`, + timestamp: Date.now() + }) + } + function moduleCssUpdate(filePath: string) { + watcher.handleJSReload(filePath) + } + + function normalCssUpdate(publicPath: string) { + // bust process cache + processedCSS.delete(publicPath) + + watcher.send({ + type: 'style-update', + path: publicPath, + timestamp: Date.now() + }) + } + + async function processCss(root: string, ctx: Context) { + const css = (await readBody(ctx.body))! const result = await compileCss(root, ctx.path, { id: '', source: css, filename: resolver.requestToFile(ctx.path), scoped: false, - modules: ctx.path.endsWith('.module.css'), + modules: ctx.path.includes('.module'), preprocessLang: ctx.path.replace(cssPreprocessLangRE, '$2') as any }) diff --git a/src/node/server/serverPluginVue.ts b/src/node/server/serverPluginVue.ts index fa162579dcd3a5..6ba6e0f2d2d51a 100644 --- a/src/node/server/serverPluginVue.ts +++ b/src/node/server/serverPluginVue.ts @@ -514,10 +514,9 @@ async function compileSFCStyle( const start = Date.now() const { generateCodeFrame } = resolveCompiler(root) - const result = (await compileCss(root, publicPath, { source: style.content, - filename: filePath, + filename: filePath + `?type=style&index=${index}`, id: ``, // will be computed in compileCss scoped: style.scoped != null, modules: style.module != null, diff --git a/src/node/utils/cssUtils.ts b/src/node/utils/cssUtils.ts index cbccd2d7746e40..abd066257b8eff 100644 --- a/src/node/utils/cssUtils.ts +++ b/src/node/utils/cssUtils.ts @@ -54,15 +54,24 @@ export async function compileCss( }: SFCAsyncStyleCompileOptions ): Promise { const id = hash_sum(publicPath) - const postcssConfig = await loadPostcssConfig(root) + let postcssConfig = await loadPostcssConfig(root) const { compileStyleAsync } = resolveCompiler(root) - if (publicPath.endsWith('.css') && !modules && !postcssConfig) { + if ( + publicPath.endsWith('.css') && + !modules && + !postcssConfig && + !source.includes('@import') + ) { // no need to invoke compile for plain css if no postcss config is present return source } - return await compileStyleAsync({ + const postcssOptions = postcssConfig && postcssConfig.options + const postcssPlugins = postcssConfig ? postcssConfig.plugins : [] + postcssPlugins.push(require('postcss-import')()) + + const res = await compileStyleAsync({ source, filename, id: `data-v-${id}`, @@ -71,18 +80,32 @@ export async function compileCss( modulesOptions: { generateScopedName: `[local]_${id}` }, + preprocessLang: preprocessLang, preprocessCustomRequire: (id: string) => require(resolveFrom(root, id)), - ...(postcssConfig - ? { - postcssOptions: postcssConfig.options, - postcssPlugins: postcssConfig.plugins - } - : {}), preprocessOptions: { includePaths: ['node_modules'] - } + }, + + postcssOptions, + postcssPlugins }) + + // record css import dependencies + if (res.rawResult) { + res.rawResult.messages.forEach((msg) => { + let { type, file, parent } = msg + if (type === 'dependency') { + if (cssImportMap.has(file)) { + cssImportMap.get(file)!.add(parent) + } else { + cssImportMap.set(file, new Set([parent])) + } + } + }) + } + + return res } export function codegenCss( @@ -126,3 +149,23 @@ async function loadPostcssConfig( return (cachedPostcssConfig = null) } } + +export const cssImportMap = new Map< + string /*filePath*/, + Set +>() + +export function getCssImportBoundaries( + filePath: string, + boundaries = new Set() +) { + if (!cssImportMap.has(filePath)) { + return boundaries + } + const importers = cssImportMap.get(filePath)! + for (const importer of importers) { + boundaries.add(importer) + getCssImportBoundaries(importer, boundaries) + } + return boundaries +} diff --git a/test/test.js b/test/test.js index 10f063585e9efb..7f5f335199730a 100644 --- a/test/test.js +++ b/test/test.js @@ -234,6 +234,29 @@ describe('vite', () => { } }) + test('CSS @import', async () => { + const el = await page.$('.script-at-import') + expect(await getComputedColor(el)).toBe('rgb(0, 128, 0)') + if (!isBuild) { + await updateFile('css-@import/imported.css', (content) => + content.replace('green', 'rgb(0, 0, 0)') + ) + await expectByPolling(() => getComputedColor(el), 'rgb(0, 0, 0)') + } + }) + + test('SFC