Skip to content

Commit

Permalink
feat: support css @import hmr (vitejs#281)
Browse files Browse the repository at this point in the history
  • Loading branch information
underfin committed May 29, 2020
1 parent 8c8bb5f commit 9bc3fbd
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 39 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions playground/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<TestPostCss />
<TestScopedCss />
<TestCssModules />
<TestCssAtImport/>
<TestPreprocessors />
<TestAssets />
<TestSrcImport />
Expand Down Expand Up @@ -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: () => ({
Expand All @@ -56,6 +58,7 @@ export default {
TestScopedCss,
TestCssModules,
TestPreprocessors,
TestCssAtImport,
TestSrcImport,
TestAssets,
TestJsonImport,
Expand Down
19 changes: 19 additions & 0 deletions playground/css-@import/TestCssAtImport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<h2>CSS @import</h2>
<div class="sfc-style-at-import">
&lt;style @import &gt; this should be red
</div>
<div class="script-at-import">
&lt;script @import &gt; this should be green
</div>
</template>

<style scoped>
@import './testCssAtImportFromStyle.css';
</style>

<script>
import './testCssAtImportFromScript.css'
export default {}
</script>
3 changes: 3 additions & 0 deletions playground/css-@import/imported.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.script-at-import {
color: green;
}
1 change: 1 addition & 0 deletions playground/css-@import/testCssAtImportFromScript.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import './imported.css'
3 changes: 3 additions & 0 deletions playground/css-@import/testCssAtImportFromStyle.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.sfc-style-at-import {
color: red;
}
2 changes: 1 addition & 1 deletion src/node/build/buildPluginCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 54 additions & 24 deletions src/node/server/serverPluginCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { srcImportMap, vueCache } from './serverPluginVue'
import {
codegenCss,
compileCss,
cssImportMap,
cssPreprocessLangRE,
getCssImportBoundaries,
rewriteCssUrls
} from '../utils/cssUtils'
import qs from 'querystring'
Expand Down Expand Up @@ -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`
)
Expand All @@ -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
})

Expand Down
3 changes: 1 addition & 2 deletions src/node/server/serverPluginVue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
63 changes: 53 additions & 10 deletions src/node/utils/cssUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,24 @@ export async function compileCss(
}: SFCAsyncStyleCompileOptions
): Promise<SFCStyleCompileResults | string> {
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}`,
Expand All @@ -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(
Expand Down Expand Up @@ -126,3 +149,23 @@ async function loadPostcssConfig(
return (cachedPostcssConfig = null)
}
}

export const cssImportMap = new Map<
string /*filePath*/,
Set<string /*filePath*/>
>()

export function getCssImportBoundaries(
filePath: string,
boundaries = new Set<string>()
) {
if (!cssImportMap.has(filePath)) {
return boundaries
}
const importers = cssImportMap.get(filePath)!
for (const importer of importers) {
boundaries.add(importer)
getCssImportBoundaries(importer, boundaries)
}
return boundaries
}
23 changes: 23 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <style> w/ @import', async () => {
const el = await page.$('.sfc-style-at-import')
expect(await getComputedColor(el)).toBe('rgb(255, 0, 0)')
if (!isBuild) {
await updateFile(
'css-@import/testCssAtImportFromStyle.css',
(content) => content.replace('red', 'rgb(0, 0, 0)')
)
await expectByPolling(() => getComputedColor(el), 'rgb(0, 0, 0)')
}
})

test('import *.module.css', async () => {
const el = await page.$('.css-modules-import')
expect(await getComputedColor(el)).toBe('rgb(255, 140, 0)')
Expand Down
28 changes: 26 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,13 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==

"@types/postcss-import@^12.0.0":
version "12.0.0"
resolved "https://registry.npmjs.org/@types/postcss-import/-/postcss-import-12.0.0.tgz#d7c584b2651258ed5b3657d25467df1c4aec33e1"
integrity sha512-AFgOR4zOOp/izuY7n5aEr/0SJ5e8fNYVxMkx49IA3Jf/Bs6Hhd6staMoSkdiTr9fi0z3Q1Ts5SqpFI5vv4k7BQ==
dependencies:
postcss "^7.0.27"

"@types/postcss-load-config@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/postcss-load-config/-/postcss-load-config-2.0.1.tgz#76c789032dfc6823eab1900ad7cc95ebadf24d49"
Expand Down Expand Up @@ -5467,6 +5474,16 @@ postcss-discard-overridden@^4.0.1:
dependencies:
postcss "^7.0.0"

postcss-import@^12.0.1:
version "12.0.1"
resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153"
integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==
dependencies:
postcss "^7.0.1"
postcss-value-parser "^3.2.3"
read-cache "^1.0.0"
resolve "^1.1.7"

postcss-load-config@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003"
Expand Down Expand Up @@ -5733,7 +5750,7 @@ postcss-unique-selectors@^4.0.1:
postcss "^7.0.0"
uniqs "^2.0.0"

postcss-value-parser@^3.0.0:
postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3:
version "3.3.1"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
Expand Down Expand Up @@ -6000,6 +6017,13 @@ react-is@^16.12.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==

read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=
dependencies:
pify "^2.3.0"

read-pkg-up@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
Expand Down Expand Up @@ -6276,7 +6300,7 @@ resolve@1.1.7:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=

resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.3.2:
resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.3.2:
version "1.17.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
Expand Down

0 comments on commit 9bc3fbd

Please sign in to comment.