Skip to content

Commit

Permalink
feat: support vite hmr (#48)
Browse files Browse the repository at this point in the history
* chore: temp commit

* chore: temp commit

* feat: support vite hmr

* test: added unit test

* docs: update readme

* chore: release v1.3.3-beta.1
  • Loading branch information
baiwusanyu-c authored Apr 18, 2023
1 parent 29c19ea commit 4399659
Show file tree
Hide file tree
Showing 21 changed files with 1,607 additions and 1,353 deletions.
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

0 comments on commit 4399659

Please sign in to comment.