Skip to content

Commit 4399659

Browse files
authored
feat: support vite hmr (#48)
* chore: temp commit * chore: temp commit * feat: support vite hmr * test: added unit test * docs: update readme * chore: release v1.3.3-beta.1
1 parent 29c19ea commit 4399659

File tree

21 files changed

+1607
-1353
lines changed

21 files changed

+1607
-1353
lines changed

README.ZH-CN.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ export interface Options {
281281
1.`sfc` 开始,分析 `style` 标签中引用的 `css` 文件,按照 `css` 文件中的引用顺序,深度优先依次提升并注入到 `sfc` 中。
282282
2. 注入到 `sfc` 后,其优先级完全由 `@vue/compiler-dom` 的编译器决定。
283283

284+
## 关于热更新
285+
目前只支持 vite 的热更新,webpack 将在将来支持
286+
284287
## Thanks
285288
* [vue](https://github.com/vuejs/core)
286289
* [vite](https://github.com/vitejs/vite)

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,9 @@ if there is a variable conflict, `script setup` will take precedence
287287
1. Starting from `sfc`, analyze the `css` files referenced in the `style` tag, and in accordance with the order of references in the `css` files, they will be promoted in depth-first order and injected into `sfc`.
288288
2. After being injected into `sfc`, its priority is completely determined by the compiler of `@vue/compiler-dom`.
289289

290+
## About Hot Update
291+
Currently only supports hot update of vite, webpack will support it in the future
292+
290293
## Thanks
291294
* [vue](https://github.com/vuejs/core)
292295
* [vite](https://github.com/vitejs/vite)

build/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const baseConfig = {
1616
noExternal: ['estree-walker'],
1717
format: ['cjs', 'esm'],
1818
clean: true,
19-
minify: true,
19+
minify: false,
2020
dts: false,
2121
outDir: path.resolve(process.cwd(), '../dist'),
2222

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "🌀 A vue plugin that allows you to use vue's CSSVars feature in css files",
44
"private": false,
55
"type": "module",
6-
"version": "1.3.3-beta.0",
6+
"version": "1.3.3-beta.1",
77
"packageManager": "[email protected]",
88
"keywords": [
99
"cssvars",
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { resolve } from 'path'
2+
import { beforeEach, describe, expect, test } from 'vitest'
3+
import { transformSymbol } from '@unplugin-vue-cssvars/utils'
4+
import { triggerSFCUpdate, updatedCSSModules } from '../hmr'
5+
6+
const mockOption = {
7+
rootDir: resolve(),
8+
include: [/.vue/],
9+
includeCompile: ['**/**.scss', '**/**.css'],
10+
server: true,
11+
}
12+
const file = transformSymbol(`${resolve()}/packages/core/hmr/__test__/style/foo.css`)
13+
const mockModuleNode = new Set<any>()
14+
mockModuleNode.add({ id: 'foo.vue' })
15+
16+
const mockFileToModulesMap = new Map()
17+
mockFileToModulesMap.set('../D/test', mockModuleNode)
18+
19+
let hmrModule = null
20+
const mockServer = {
21+
reloadModule: (m) => {
22+
hmrModule = m
23+
},
24+
moduleGraph: {
25+
fileToModulesMap: mockFileToModulesMap,
26+
},
27+
}
28+
beforeEach(() => {
29+
hmrModule = null
30+
})
31+
describe('HMR', () => {
32+
test('HMR: updatedCSSModules', () => {
33+
const CSSFileModuleMap = new Map()
34+
CSSFileModuleMap.set(file, {
35+
importer: new Set(),
36+
vBindCode: ['foo'],
37+
})
38+
updatedCSSModules(CSSFileModuleMap, mockOption, file)
39+
expect(CSSFileModuleMap.get(file).content).toBeTruthy()
40+
expect(CSSFileModuleMap.get(file).vBindCode).toMatchObject(['test'])
41+
})
42+
43+
test('HMR: triggerSFCUpdate basic', () => {
44+
const CSSFileModuleMap = new Map()
45+
CSSFileModuleMap.set(file, {
46+
importer: new Set(),
47+
vBindCode: ['foo'],
48+
sfcPath: new Set(['../D/test']),
49+
})
50+
51+
triggerSFCUpdate(CSSFileModuleMap, mockOption, {
52+
importer: new Set(),
53+
vBindCode: ['foo'],
54+
sfcPath: new Set(['../D/test']),
55+
} as any, file, mockServer as any)
56+
expect(CSSFileModuleMap.get(file).content).toBeTruthy()
57+
expect(CSSFileModuleMap.get(file).vBindCode).toMatchObject(['test'])
58+
expect(hmrModule).toMatchObject({ id: 'foo.vue' })
59+
})
60+
61+
test('HMR: triggerSFCUpdate sfcPath is undefined', () => {
62+
const CSSFileModuleMap = new Map()
63+
CSSFileModuleMap.set(file, {
64+
importer: new Set(),
65+
vBindCode: ['foo'],
66+
sfcPath: new Set(['../D/test']),
67+
})
68+
69+
triggerSFCUpdate(CSSFileModuleMap, mockOption, {
70+
importer: new Set(),
71+
vBindCode: ['foo'],
72+
} as any, file, mockServer as any)
73+
expect(CSSFileModuleMap.get(file).content).not.toBeTruthy()
74+
expect(CSSFileModuleMap.get(file).vBindCode).toMatchObject(['foo'])
75+
expect(hmrModule).not.toBeTruthy()
76+
})
77+
78+
test('HMR: triggerSFCUpdate sfcPath is empty', () => {
79+
const CSSFileModuleMap = new Map()
80+
CSSFileModuleMap.set(file, {
81+
importer: new Set(),
82+
vBindCode: ['foo'],
83+
sfcPath: new Set(['../D/test']),
84+
})
85+
86+
triggerSFCUpdate(CSSFileModuleMap, mockOption, {
87+
importer: new Set(),
88+
vBindCode: ['foo'],
89+
sfcPath: new Set(),
90+
} as any, file, mockServer as any)
91+
expect(CSSFileModuleMap.get(file).content).not.toBeTruthy()
92+
expect(CSSFileModuleMap.get(file).vBindCode).toMatchObject(['foo'])
93+
expect(hmrModule).not.toBeTruthy()
94+
})
95+
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#foo{
2+
color: v-bind-m(test);
3+
background: #ffebf8;
4+
width: 200px;
5+
height: 30px;
6+
}

packages/core/hmr/hmr.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { setTArray } from '@unplugin-vue-cssvars/utils'
2+
import { preProcessCSS } from '../runtime/pre-process-css'
3+
import type { ICSSFile, ICSSFileMap, Options } from '../types'
4+
import type { ViteDevServer } from 'vite'
5+
6+
export function viteHMR(
7+
CSSFileModuleMap: ICSSFileMap,
8+
userOptions: Options,
9+
file: string,
10+
server: ViteDevServer,
11+
) {
12+
// 获取变化的样式文件的 CSSFileMap上有使用它的
13+
const sfcModulesPathList = CSSFileModuleMap.get(file)
14+
triggerSFCUpdate(CSSFileModuleMap, userOptions, sfcModulesPathList, file, server)
15+
}
16+
17+
/**
18+
* update CSSModules
19+
* @param CSSFileModuleMap
20+
* @param userOptions
21+
* @param file
22+
*/
23+
24+
export function updatedCSSModules(
25+
CSSFileModuleMap: ICSSFileMap,
26+
userOptions: Options,
27+
file: string) {
28+
const updatedCSSMS = preProcessCSS(userOptions, userOptions.alias, [file]).get(file)
29+
CSSFileModuleMap.set(file, updatedCSSMS)
30+
}
31+
32+
// TODO: unit test
33+
/**
34+
* triggerSFCUpdate
35+
* @param CSSFileModuleMap
36+
* @param userOptions
37+
* @param sfcModulesPathList
38+
* @param file
39+
* @param server
40+
*/
41+
export function triggerSFCUpdate(
42+
CSSFileModuleMap: ICSSFileMap,
43+
userOptions: Options,
44+
sfcModulesPathList: ICSSFile,
45+
file: string,
46+
server: ViteDevServer) {
47+
if (sfcModulesPathList && sfcModulesPathList.sfcPath) {
48+
// 变化的样式文件的 CSSFileMap上有使用它的 sfc 的信息
49+
const ls = setTArray(sfcModulesPathList.sfcPath)
50+
ls.forEach((sfcp: string) => {
51+
const modules = server.moduleGraph.fileToModulesMap.get(sfcp) || new Set()
52+
53+
// updatedCSSModules
54+
updatedCSSModules(CSSFileModuleMap, userOptions, file)
55+
56+
// update sfc
57+
const modulesList = setTArray(modules)
58+
for (let i = 0; i < modulesList.length; i++) {
59+
// ⭐TODO: 只支持 .vue ? jsx, tsx, js, ts ?
60+
if (modulesList[i].id && (modulesList[i].id as string).endsWith('.vue'))
61+
server.reloadModule(modulesList[i])
62+
}
63+
})
64+
}
65+
}

packages/core/index.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createUnplugin } from 'unplugin'
2-
import { NAME } from '@unplugin-vue-cssvars/utils'
2+
import { NAME, SUPPORT_FILE_REG } from '@unplugin-vue-cssvars/utils'
33
import { createFilter } from '@rollup/pluginutils'
44
import { parse } from '@vue/compiler-sfc'
55
import chalk from 'chalk'
@@ -12,10 +12,12 @@ import {
1212
injectCssOnBuild,
1313
injectCssOnServer,
1414
} from './inject'
15-
import type { ResolvedConfig } from 'vite'
15+
import { viteHMR } from './hmr/hmr'
16+
import type { HmrContext, ResolvedConfig } from 'vite'
17+
1618
import type { TMatchVariable } from './parser'
1719
import type { Options } from './types'
18-
20+
// TODO: webpack hmr
1921
const unplugin = createUnplugin<Options>(
2022
(options: Options = {}): any => {
2123
const userOptions = initOption(options)
@@ -32,6 +34,7 @@ const unplugin = createUnplugin<Options>(
3234
console.warn(chalk.yellowBright.bold(`[${NAME}] See: https://github.com/baiwusanyu-c/unplugin-vue-cssvars/blob/master/README.md#option`))
3335
}
3436
let isServer = !!userOptions.server
37+
let isHmring = false
3538
return [
3639
{
3740
name: NAME,
@@ -68,6 +71,17 @@ const unplugin = createUnplugin<Options>(
6871
else
6972
isServer = config.command === 'serve'
7073
},
74+
handleHotUpdate(hmr: HmrContext) {
75+
if (SUPPORT_FILE_REG.test(hmr.file)) {
76+
isHmring = true
77+
viteHMR(
78+
CSSFileModuleMap,
79+
userOptions,
80+
hmr.file,
81+
hmr.server,
82+
)
83+
}
84+
},
7185
},
7286
},
7387
{
@@ -82,9 +96,10 @@ const unplugin = createUnplugin<Options>(
8296
const injectRes = injectCSSVars(code, vbindVariableList.get(id), isScriptSetup)
8397
code = injectRes.code
8498
injectRes.vbindVariableList && vbindVariableList.set(id, injectRes.vbindVariableList)
99+
isHmring = false
85100
}
86101
if (id.includes('type=style'))
87-
code = injectCssOnServer(code, vbindVariableList.get(id.split('?vue')[0]))
102+
code = injectCssOnServer(code, vbindVariableList.get(id.split('?vue')[0]), isHmring)
88103
}
89104
return code
90105
} catch (err: unknown) {

packages/core/inject/inject-css.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1+
import hash from 'hash-sum'
12
import { transformInjectCSS } from '../transform/transform-inject-css'
23
import { parseImports } from '../parser'
34
import type { TInjectCSSContent } from '../runtime/process-css'
45
import type { SFCDescriptor } from '@vue/compiler-sfc'
56
import type { TMatchVariable } from '../parser'
6-
77
export function injectCssOnServer(
88
code: string,
99
vbindVariableList: TMatchVariable | undefined,
10+
isHmring: boolean,
1011
) {
1112
vbindVariableList && vbindVariableList.forEach((vbVar) => {
13+
// 样式文件修改后,热更新会先于 sfc 热更新运行,这里先设置hash
14+
// 详见 packages/core/index.ts的 handleHotUpdate
15+
if (!vbVar.hash && isHmring)
16+
vbVar.hash = hash(vbVar.value + vbVar.has)
17+
1218
code = code.replaceAll(`v-bind-m(${vbVar.value})`, `var(--${vbVar.hash})`)
1319
})
1420
return code

packages/core/inject/inject-cssvars.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export function createUseCssVarsCode(
6060
isScriptSetup: boolean) {
6161
let cssvarsObjectCode = ''
6262
vbindVariableList.forEach((vbVar) => {
63-
const hashVal = hash(vbVar.value + vbVar.has)
63+
// 如果 hash 存在,则说明是由热更新引起的,不需要重新设置 hash
64+
const hashVal = vbVar.hash || hash(vbVar.value + vbVar.has)
6465
vbVar.hash = hashVal
6566
let varStr = ''
6667
// composition api 和 option api 一直帶 _ctx

0 commit comments

Comments
 (0)