Skip to content

feat: support vite hmr #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.ZH-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ export interface Options {
1. 从 `sfc` 开始,分析 `style` 标签中引用的 `css` 文件,按照 `css` 文件中的引用顺序,深度优先依次提升并注入到 `sfc` 中。
2. 注入到 `sfc` 后,其优先级完全由 `@vue/compiler-dom` 的编译器决定。

## 关于热更新
目前只支持 vite 的热更新,webpack 将在将来支持

## Thanks
* [vue](https://github.com/vuejs/core)
* [vite](https://github.com/vitejs/vite)
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ if there is a variable conflict, `script setup` will take precedence
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`.
2. After being injected into `sfc`, its priority is completely determined by the compiler of `@vue/compiler-dom`.

## About Hot Update
Currently only supports hot update of vite, webpack will support it in the future

## Thanks
* [vue](https://github.com/vuejs/core)
* [vite](https://github.com/vitejs/vite)
Expand Down
2 changes: 1 addition & 1 deletion build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const baseConfig = {
noExternal: ['estree-walker'],
format: ['cjs', 'esm'],
clean: true,
minify: true,
minify: false,
dts: false,
outDir: path.resolve(process.cwd(), '../dist'),

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "🌀 A vue plugin that allows you to use vue's CSSVars feature in css files",
"private": false,
"type": "module",
"version": "1.3.3-beta.0",
"version": "1.3.3-beta.1",
"packageManager": "pnpm@6.32.4",
"keywords": [
"cssvars",
Expand Down
95 changes: 95 additions & 0 deletions packages/core/hmr/__test__/hmr.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { resolve } from 'path'
import { beforeEach, describe, expect, test } from 'vitest'
import { transformSymbol } from '@unplugin-vue-cssvars/utils'
import { triggerSFCUpdate, updatedCSSModules } from '../hmr'

const mockOption = {
rootDir: resolve(),
include: [/.vue/],
includeCompile: ['**/**.scss', '**/**.css'],
server: true,
}
const file = transformSymbol(`${resolve()}/packages/core/hmr/__test__/style/foo.css`)
const mockModuleNode = new Set<any>()
mockModuleNode.add({ id: 'foo.vue' })

const mockFileToModulesMap = new Map()
mockFileToModulesMap.set('../D/test', mockModuleNode)

let hmrModule = null
const mockServer = {
reloadModule: (m) => {
hmrModule = m
},
moduleGraph: {
fileToModulesMap: mockFileToModulesMap,
},
}
beforeEach(() => {
hmrModule = null
})
describe('HMR', () => {
test('HMR: updatedCSSModules', () => {
const CSSFileModuleMap = new Map()
CSSFileModuleMap.set(file, {
importer: new Set(),
vBindCode: ['foo'],
})
updatedCSSModules(CSSFileModuleMap, mockOption, file)
expect(CSSFileModuleMap.get(file).content).toBeTruthy()
expect(CSSFileModuleMap.get(file).vBindCode).toMatchObject(['test'])
})

test('HMR: triggerSFCUpdate basic', () => {
const CSSFileModuleMap = new Map()
CSSFileModuleMap.set(file, {
importer: new Set(),
vBindCode: ['foo'],
sfcPath: new Set(['../D/test']),
})

triggerSFCUpdate(CSSFileModuleMap, mockOption, {
importer: new Set(),
vBindCode: ['foo'],
sfcPath: new Set(['../D/test']),
} as any, file, mockServer as any)
expect(CSSFileModuleMap.get(file).content).toBeTruthy()
expect(CSSFileModuleMap.get(file).vBindCode).toMatchObject(['test'])
expect(hmrModule).toMatchObject({ id: 'foo.vue' })
})

test('HMR: triggerSFCUpdate sfcPath is undefined', () => {
const CSSFileModuleMap = new Map()
CSSFileModuleMap.set(file, {
importer: new Set(),
vBindCode: ['foo'],
sfcPath: new Set(['../D/test']),
})

triggerSFCUpdate(CSSFileModuleMap, mockOption, {
importer: new Set(),
vBindCode: ['foo'],
} as any, file, mockServer as any)
expect(CSSFileModuleMap.get(file).content).not.toBeTruthy()
expect(CSSFileModuleMap.get(file).vBindCode).toMatchObject(['foo'])
expect(hmrModule).not.toBeTruthy()
})

test('HMR: triggerSFCUpdate sfcPath is empty', () => {
const CSSFileModuleMap = new Map()
CSSFileModuleMap.set(file, {
importer: new Set(),
vBindCode: ['foo'],
sfcPath: new Set(['../D/test']),
})

triggerSFCUpdate(CSSFileModuleMap, mockOption, {
importer: new Set(),
vBindCode: ['foo'],
sfcPath: new Set(),
} as any, file, mockServer as any)
expect(CSSFileModuleMap.get(file).content).not.toBeTruthy()
expect(CSSFileModuleMap.get(file).vBindCode).toMatchObject(['foo'])
expect(hmrModule).not.toBeTruthy()
})
})
6 changes: 6 additions & 0 deletions packages/core/hmr/__test__/style/foo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#foo{
color: v-bind-m(test);
background: #ffebf8;
width: 200px;
height: 30px;
}
65 changes: 65 additions & 0 deletions packages/core/hmr/hmr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { setTArray } from '@unplugin-vue-cssvars/utils'
import { preProcessCSS } from '../runtime/pre-process-css'
import type { ICSSFile, ICSSFileMap, Options } from '../types'
import type { ViteDevServer } from 'vite'

export function viteHMR(
CSSFileModuleMap: ICSSFileMap,
userOptions: Options,
file: string,
server: ViteDevServer,
) {
// 获取变化的样式文件的 CSSFileMap上有使用它的
const sfcModulesPathList = CSSFileModuleMap.get(file)
triggerSFCUpdate(CSSFileModuleMap, userOptions, sfcModulesPathList, file, server)
}

/**
* update CSSModules
* @param CSSFileModuleMap
* @param userOptions
* @param file
*/

export function updatedCSSModules(
CSSFileModuleMap: ICSSFileMap,
userOptions: Options,
file: string) {
const updatedCSSMS = preProcessCSS(userOptions, userOptions.alias, [file]).get(file)
CSSFileModuleMap.set(file, updatedCSSMS)
}

// TODO: unit test
/**
* triggerSFCUpdate
* @param CSSFileModuleMap
* @param userOptions
* @param sfcModulesPathList
* @param file
* @param server
*/
export function triggerSFCUpdate(
CSSFileModuleMap: ICSSFileMap,
userOptions: Options,
sfcModulesPathList: ICSSFile,
file: string,
server: ViteDevServer) {
if (sfcModulesPathList && sfcModulesPathList.sfcPath) {
// 变化的样式文件的 CSSFileMap上有使用它的 sfc 的信息
const ls = setTArray(sfcModulesPathList.sfcPath)
ls.forEach((sfcp: string) => {
const modules = server.moduleGraph.fileToModulesMap.get(sfcp) || new Set()

// updatedCSSModules
updatedCSSModules(CSSFileModuleMap, userOptions, file)

// update sfc
const modulesList = setTArray(modules)
for (let i = 0; i < modulesList.length; i++) {
// ⭐TODO: 只支持 .vue ? jsx, tsx, js, ts ?
if (modulesList[i].id && (modulesList[i].id as string).endsWith('.vue'))
server.reloadModule(modulesList[i])
}
})
}
}
23 changes: 19 additions & 4 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createUnplugin } from 'unplugin'
import { NAME } from '@unplugin-vue-cssvars/utils'
import { NAME, SUPPORT_FILE_REG } from '@unplugin-vue-cssvars/utils'
import { createFilter } from '@rollup/pluginutils'
import { parse } from '@vue/compiler-sfc'
import chalk from 'chalk'
Expand All @@ -12,10 +12,12 @@ import {
injectCssOnBuild,
injectCssOnServer,
} from './inject'
import type { ResolvedConfig } from 'vite'
import { viteHMR } from './hmr/hmr'
import type { HmrContext, ResolvedConfig } from 'vite'

import type { TMatchVariable } from './parser'
import type { Options } from './types'

// TODO: webpack hmr
const unplugin = createUnplugin<Options>(
(options: Options = {}): any => {
const userOptions = initOption(options)
Expand All @@ -32,6 +34,7 @@ const unplugin = createUnplugin<Options>(
console.warn(chalk.yellowBright.bold(`[${NAME}] See: https://github.com/baiwusanyu-c/unplugin-vue-cssvars/blob/master/README.md#option`))
}
let isServer = !!userOptions.server
let isHmring = false
return [
{
name: NAME,
Expand Down Expand Up @@ -68,6 +71,17 @@ const unplugin = createUnplugin<Options>(
else
isServer = config.command === 'serve'
},
handleHotUpdate(hmr: HmrContext) {
if (SUPPORT_FILE_REG.test(hmr.file)) {
isHmring = true
viteHMR(
CSSFileModuleMap,
userOptions,
hmr.file,
hmr.server,
)
}
},
},
},
{
Expand All @@ -82,9 +96,10 @@ const unplugin = createUnplugin<Options>(
const injectRes = injectCSSVars(code, vbindVariableList.get(id), isScriptSetup)
code = injectRes.code
injectRes.vbindVariableList && vbindVariableList.set(id, injectRes.vbindVariableList)
isHmring = false
}
if (id.includes('type=style'))
code = injectCssOnServer(code, vbindVariableList.get(id.split('?vue')[0]))
code = injectCssOnServer(code, vbindVariableList.get(id.split('?vue')[0]), isHmring)
}
return code
} catch (err: unknown) {
Expand Down
8 changes: 7 additions & 1 deletion packages/core/inject/inject-css.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import hash from 'hash-sum'
import { transformInjectCSS } from '../transform/transform-inject-css'
import { parseImports } from '../parser'
import type { TInjectCSSContent } from '../runtime/process-css'
import type { SFCDescriptor } from '@vue/compiler-sfc'
import type { TMatchVariable } from '../parser'

export function injectCssOnServer(
code: string,
vbindVariableList: TMatchVariable | undefined,
isHmring: boolean,
) {
vbindVariableList && vbindVariableList.forEach((vbVar) => {
// 样式文件修改后,热更新会先于 sfc 热更新运行,这里先设置hash
// 详见 packages/core/index.ts的 handleHotUpdate
if (!vbVar.hash && isHmring)
vbVar.hash = hash(vbVar.value + vbVar.has)

code = code.replaceAll(`v-bind-m(${vbVar.value})`, `var(--${vbVar.hash})`)
})
return code
Expand Down
3 changes: 2 additions & 1 deletion packages/core/inject/inject-cssvars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export function createUseCssVarsCode(
isScriptSetup: boolean) {
let cssvarsObjectCode = ''
vbindVariableList.forEach((vbVar) => {
const hashVal = hash(vbVar.value + vbVar.has)
// 如果 hash 存在,则说明是由热更新引起的,不需要重新设置 hash
const hashVal = vbVar.hash || hash(vbVar.value + vbVar.has)
vbVar.hash = hashVal
let varStr = ''
// composition api 和 option api 一直帶 _ctx
Expand Down
7 changes: 0 additions & 7 deletions packages/core/runtime.md

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ exports[`pre process css > preProcessCSS: basic 2`] = `

exports[`pre process css > preProcessCSS: basic 3`] = `
[
"core/hmr/__test__/style/foo.css",
"core/runtime/__test__/style/test.css",
"core/runtime/__test__/style/test2.css",
]
Expand Down
1 change: 1 addition & 0 deletions packages/core/runtime/__test__/pre-process-css.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ describe('pre process css', () => {
test('preProcessCSS: basic', () => {
const files = getAllCSSFilePath(['**/**.css'], resolve('packages'))
expect(files).toMatchObject([
'core/hmr/__test__/style/foo.css',
'core/runtime/__test__/style/test.css',
'core/runtime/__test__/style/test2.css',
])
Expand Down
12 changes: 8 additions & 4 deletions packages/core/runtime/pre-process-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ import type { ICSSFileMap, SearchGlobOptions } from '../types'
* 预处理css文件
* @param options 选项参数 Options
* @param alias
* @param filesPath
*/
export function preProcessCSS(options: SearchGlobOptions, alias?: Record<string, string>): ICSSFileMap {
export function preProcessCSS(
options: SearchGlobOptions,
alias?: Record<string, string>,
filesPath?: string[]): ICSSFileMap {
const { rootDir, includeCompile } = options

// 获得文件列表
const files = getAllCSSFilePath(includeCompile!, rootDir!)
const files = filesPath || getAllCSSFilePath(includeCompile!, rootDir!)

return createCSSFileModuleMap(files, rootDir!, alias)
}

Expand Down Expand Up @@ -52,7 +56,7 @@ export function createCSSFileModuleMap(files: string[], rootDir: string, alias?:
const fileDirParse = parse(file)
const fileSuffix = fileDirParse.ext

const code = fs.readFileSync(resolve(rootDir!, file), { encoding: 'utf-8' })
const code = fs.readFileSync(transformSymbol(resolve(rootDir!, file)), { encoding: 'utf-8' })
const { imports } = parseImports(code, [transformQuotes])

const absoluteFilePath = transformSymbol(resolve(fileDirParse.dir, fileDirParse.base))
Expand Down
11 changes: 8 additions & 3 deletions packages/core/runtime/process-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const getCSSFileRecursion = (
key: string,
cssFiles: ICSSFileMap,
cb: (res: ICSSFile) => void,
sfcPath?: string,
matchedMark = new Set<string>()) => {
// 添加后缀
// sfc中规则:如果@import 指定了后缀,则根据后缀,否则根据当前 script 标签的 lang 属性(默认css)
Expand All @@ -20,11 +21,15 @@ export const getCSSFileRecursion = (
if (matchedMark.has(key)) return
const cssFile = cssFiles.get(key)
if (cssFile) {
if (!cssFile.sfcPath)
cssFile.sfcPath = new Set()

cssFile.sfcPath?.add(sfcPath)
matchedMark.add(key)
cb(cssFile)
if (cssFile.importer.size > 0) {
cssFile.importer.forEach((value) => {
getCSSFileRecursion(lang, value, cssFiles, cb, matchedMark)
getCSSFileRecursion(lang, value, cssFiles, cb, sfcPath, matchedMark)
})
}
} else {
Expand Down Expand Up @@ -66,7 +71,7 @@ export const getVBindVariableListByPath = (
vbindVariable.add(vb)
})
}
})
}, id)
} catch (e) {
if ((e as Error).message === 'path') {
const doc = 'https://github.com/baiwusanyu-c/unplugin-vue-cssvars/pull/29'
Expand Down Expand Up @@ -94,7 +99,7 @@ export function handleAlias(path: string, alias?: Record<string, string>, idDirP
}
}

if (importerPath) return importerPath
if (importerPath) return transformSymbol(importerPath)
importerPath = idDirPath ? resolve(idDirPath, path) : path
} else {
idDirPath && (importerPath = resolve(idDirPath, path))
Expand Down
Loading