From fa0f19041446d587169dfe836171caa1efedc6b9 Mon Sep 17 00:00:00 2001 From: baiwusanyu-c <740132583@qq.com> Date: Fri, 17 Mar 2023 13:13:50 +0800 Subject: [PATCH] test: added pre process css unit test --- package.json | 2 +- .../pre-process-css.spec.ts.snap | 142 ++++++++ .../core/css/__test__/pre-process-css.spec.ts | 316 +++++++++++++++++- .../core/css/__test__/process-css.spec.ts | 6 +- packages/core/css/__test__/test.css | 5 + packages/core/css/__test__/test2.css | 4 + packages/core/css/pre-process-css.ts | 18 +- packages/core/css/process-css.ts | 4 +- utils/constant.ts | 2 +- utils/index.ts | 2 + 10 files changed, 483 insertions(+), 18 deletions(-) create mode 100644 packages/core/css/__test__/__snapshots__/pre-process-css.spec.ts.snap create mode 100644 packages/core/css/__test__/test.css create mode 100644 packages/core/css/__test__/test2.css diff --git a/package.json b/package.json index b0b68b2..1a52e0c 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "prepare": "npx simple-git-hooks", "test": "vitest", "test:update": "vitest -u", - "test:coverage": "vitest --coverageu" + "test:coverage": "vitest --coverage" }, "peerDependencies": { "chalk": "^5.2.0", diff --git a/packages/core/css/__test__/__snapshots__/pre-process-css.spec.ts.snap b/packages/core/css/__test__/__snapshots__/pre-process-css.spec.ts.snap new file mode 100644 index 0000000..33af203 --- /dev/null +++ b/packages/core/css/__test__/__snapshots__/pre-process-css.spec.ts.snap @@ -0,0 +1,142 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`pre process css > getCSSVarsCode: generate code 1`] = ` +{ + "vBindCode": { + "color": Set { + " +/* created by @unplugin-vue-cssvars */ +/* */ +div{color:v-bind(color)} +/* */ +", + }, + }, + "vBindEntry": false, + "vBindPathNode": { + "block": { + "children": [ + { + "important": false, + "loc": null, + "property": "color", + "type": "Declaration", + "value": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "color", + "type": "Identifier", + }, + ], + "loc": null, + "name": "v-bind", + "type": "Function", + }, + ], + "loc": null, + "type": "Value", + }, + }, + ], + "loc": null, + "type": "Block", + }, + "loc": null, + "prelude": { + "children": [ + { + "children": [ + { + "loc": null, + "name": "div", + "type": "TypeSelector", + }, + ], + "loc": null, + "type": "Selector", + }, + ], + "loc": null, + "type": "SelectorList", + }, + "type": "Rule", + }, +} +`; + +exports[`pre process css > preProcessCSS: basic 1`] = ` +{ + "color": Set { + " +/* created by @unplugin-vue-cssvars */ +/* */ +div{color:v-bind(color)} +/* */ +", + }, +} +`; + +exports[`pre process css > preProcessCSS: basic 2`] = ` +{ + "appTheme2": Set { + " +/* created by @unplugin-vue-cssvars */ +/* */ +.test{color:v-bind(appTheme2)} +/* */ +", + }, +} +`; + +exports[`pre process css > walkCSSTree: basic 1`] = ` +{ + "importer": "./test", + "vBindCode": { + "bar": Set { + " +/* created by @unplugin-vue-cssvars */ +/* */ +.bar{color:v-bind(bar);font-size:22px} +/* */ +", + }, + "color": Set { + " +/* created by @unplugin-vue-cssvars */ +/* */ +.foo{color:v-bind(color);font-size:20px} +/* */ +", + }, + }, +} +`; + +exports[`pre process css > walkCSSTree: helper i is false 1`] = ` +{ + "importer": "", + "vBindCode": { + "bar": Set { + " +/* created by @unplugin-vue-cssvars */ +/* */ +.bar{color:v-bind(bar);font-size:22px} +/* */ +", + }, + "color": Set { + " +/* created by @unplugin-vue-cssvars */ +/* */ +.foo{color:v-bind(color);font-size:20px} +/* */ +", + }, + }, +} +`; diff --git a/packages/core/css/__test__/pre-process-css.spec.ts b/packages/core/css/__test__/pre-process-css.spec.ts index 2bc5697..5ea7b8b 100644 --- a/packages/core/css/__test__/pre-process-css.spec.ts +++ b/packages/core/css/__test__/pre-process-css.spec.ts @@ -1,6 +1,316 @@ -import { describe, expect, test } from 'vitest' +import { resolve } from 'path' +import { describe, expect, test, vi } from 'vitest' +import * as csstree from 'css-tree' +import { transformSymbol } from '@unplugin-vue-cssvars/utils' +import { getCSSImport, getCSSVarsCode, preProcessCSS, walkCSSTree } from '../pre-process-css' +const mockVBindPathNode = { + type: 'Rule', + loc: null, + prelude: { + type: 'SelectorList', + loc: null, + children: [ + { + type: 'Selector', + loc: null, + children: [ + { + type: 'TypeSelector', + loc: null, + name: 'div', + }, + ], + }, + ], + }, + block: { + type: 'Block', + loc: null, + children: [ + { + type: 'Declaration', + loc: null, + important: false, + property: 'color', + value: { + type: 'Value', + loc: null, + children: [ + { + type: 'Function', + loc: null, + name: 'v-bind', + children: [ + { + type: 'Identifier', + loc: null, + name: 'color', + }, + ], + }, + ], + }, + }, + ], + }, +} +const mockCSSContent = ` +@import "./test"; +.foo { + color: v-bind(color); + font-size: 20px; +} +.bar { + color: v-bind(bar); + font-size: 22px; +} +.test { + background: blue; +} +` describe('pre process css', () => { - test('basic', () => { - expect(1).toBe(1) + test('getCSSImport: isAtrule', () => { + const mockNode = { + type: 'Atrule', + name: 'import', + value: 'foo', + } + const res = getCSSImport(mockNode as any, false, false) + expect(res).toMatchObject({ + value: '', + isAtrule: true, + isAtrulePrelude: false, + }) + }) + + test('getCSSImport: AtrulePrelude & isAtrule is true', () => { + const mockNode = { + type: 'AtrulePrelude', + name: 'import', + value: 'foo', + } + const res = getCSSImport(mockNode as any, true, false) + expect(res).toMatchObject({ + value: '', + isAtrule: true, + isAtrulePrelude: true, + }) + }) + + test('getCSSImport: AtrulePrelude & isAtrule is false', () => { + const mockNode = { + type: 'AtrulePrelude', + name: 'import', + value: 'foo', + } + const res = getCSSImport(mockNode as any, false, false) + expect(res).toMatchObject({ + value: '', + isAtrule: false, + isAtrulePrelude: false, + }) + }) + + test('getCSSImport: string & isAtrule & isAtrule', () => { + const mockNode = { + type: 'String', + name: 'import', + value: 'foo', + } + const res = getCSSImport(mockNode as any, true, true) + expect(res).toMatchObject({ + value: 'foo', + isAtrule: false, + isAtrulePrelude: false, + }) + + const res2 = getCSSImport(mockNode as any, false, false) + expect(res2).toMatchObject({ + value: '', + isAtrule: false, + isAtrulePrelude: false, + }) + + const res3 = getCSSImport(mockNode as any, false, true) + expect(res3).toMatchObject({ + value: '', + isAtrule: false, + isAtrulePrelude: true, + }) + + const res4 = getCSSImport(mockNode as any, true, false) + expect(res4).toMatchObject({ + value: '', + isAtrule: true, + isAtrulePrelude: false, + }) + }) + + test('getCSSVarsCode: Rule', () => { + const mockNode = { + type: 'Rule', + } + const res = getCSSVarsCode(mockNode as any, mockNode as any, null, false) + expect(res).toMatchObject({ + vBindCode: {}, + vBindPathNode: mockNode, + vBindEntry: false, + }) + }) + + test('getCSSVarsCode: unmatched logic', () => { + const mockNode = { + type: 'foo', + } + const res = getCSSVarsCode( + mockNode as any, + null, + { foo: new Set(['foo']) }, + false) + expect(res).toMatchObject({ + vBindCode: { foo: new Set(['foo']) }, + vBindPathNode: null, + vBindEntry: false, + }) + }) + + test('getCSSVarsCode: v-bind Function', () => { + const mockNode = { + type: 'Function', + name: 'v-bind', + } + const res = getCSSVarsCode( + mockNode as any, + mockNode as any, + null, + false) + expect(res).toMatchObject({ + vBindCode: {}, + vBindPathNode: mockNode, + vBindEntry: true, + }) + }) + + test('getCSSVarsCode: generate code', () => { + const mockNode = { + type: 'Identifier', + loc: null, + name: 'color', + } + const mockVBindCode = { color: new Set() } + const res = getCSSVarsCode( + mockNode as any, + mockVBindPathNode as any, + mockVBindCode, + true) + expect(res).toMatchObject({ + vBindCode: { + color: new Set(['\n' + + '/* created by @unplugin-vue-cssvars */\n' + + '/* */\n' + + 'div{color:v-bind(color)}\n' + + '/* */\n']), + }, + vBindPathNode: mockVBindPathNode, + vBindEntry: false, + }) + expect(res).toMatchSnapshot() + }) + + test('walkCSSTree: basic', () => { + let res + const mockEvt = vi.fn() + const mockCallback = ( + importer: string, + vBindCode: Record> | null) => { + mockEvt() + res = { + importer, + vBindCode, + } + } + const ast = csstree.parse(mockCSSContent) + walkCSSTree(ast, mockCallback) + expect(mockEvt).toBeCalledTimes(1) + expect(res).toMatchSnapshot() + }) + + test('walkCSSTree: helper i is false', () => { + let res + const mockEvt = vi.fn() + const mockCallback = ( + importer: string, + vBindCode: Record> | null) => { + mockEvt() + res = { + importer, + vBindCode, + } + } + const ast = csstree.parse(mockCSSContent) + walkCSSTree(ast, mockCallback, { i: false, v: true }) + expect(mockEvt).toBeCalledTimes(1) + expect(res).toMatchSnapshot() + }) + + test('walkCSSTree: helper v is false', () => { + let res + const mockEvt = vi.fn() + const mockCallback = ( + importer: string, + vBindCode: Record> | null) => { + mockEvt() + res = { + importer, + vBindCode, + } + } + const ast = csstree.parse(mockCSSContent) + walkCSSTree(ast, mockCallback, { i: true, v: false }) + expect(mockEvt).toBeCalledTimes(1) + expect(res).toMatchObject({ + importer: './test', + vBindCode: null, + }) + }) + + test('walkCSSTree: helper v & i are false', () => { + let res + const mockEvt = vi.fn() + const mockCallback = ( + importer: string, + vBindCode: Record> | null) => { + mockEvt() + res = { + importer, + vBindCode, + } + } + const ast = csstree.parse(mockCSSContent) + walkCSSTree(ast, mockCallback, { i: false, v: false }) + expect(mockEvt).toBeCalledTimes(1) + expect(res).toMatchObject({ + importer: '', + vBindCode: null, + }) + }) + + test('preProcessCSS: basic', () => { + const res = preProcessCSS({ rootDir: resolve('packages') }) + const mockPathTest1 = transformSymbol(`${resolve()}/core/css/__test__/test.css`) + const mockPathTest2 = transformSymbol(`${resolve()}/core/css/__test__/test2.css`) + const resTest1 = res.get(mockPathTest1) + const resTest2 = res.get(mockPathTest2) + + console.log([...resTest1!.importer][0]) + expect(resTest1).toBeTruthy() + expect([...resTest1!.importer][0]).toBe(mockPathTest2) + expect(resTest1!.vBindCode?.color).toBeTruthy() + expect(resTest1!.vBindCode).toMatchSnapshot() + + expect(resTest2).toBeTruthy() + expect(resTest2!.importer.size).toBe(0) + expect(resTest2!.vBindCode?.appTheme2).toBeTruthy() + expect(resTest2!.vBindCode).toMatchSnapshot() }) }) diff --git a/packages/core/css/__test__/process-css.spec.ts b/packages/core/css/__test__/process-css.spec.ts index f53babd..6e3bebf 100644 --- a/packages/core/css/__test__/process-css.spec.ts +++ b/packages/core/css/__test__/process-css.spec.ts @@ -106,7 +106,7 @@ describe('process css', () => { foo: new Set(['v-bind(foo)']), }, } - mockCssFiles.set('D:\\project-github\\unplugin-vue-cssvars\\play\\src\\assets\\test.css', mockCSSFilesContent) + mockCssFiles.set('D:/project-github/unplugin-vue-cssvars/play/src/assets/test.css', mockCSSFilesContent) const mockDescriptor = { styles: [{ content: '@import "./assets/test";\n' @@ -129,14 +129,14 @@ describe('process css', () => { foo: new Set(['v-bind(foo)']), }, } - mockCssFiles.set('D:\\project-github\\unplugin-vue-cssvars\\play\\src\\assets\\test.css', mockCSSFilesContent) + mockCssFiles.set('D:/project-github/unplugin-vue-cssvars/play/src/assets/test.css', mockCSSFilesContent) const mockCSSFilesContent2 = { importer: new Set(), vBindCode: { bar: new Set(['v-bind(bar)']), }, } - mockCssFiles.set('D:\\project-github\\unplugin-vue-cssvars\\play\\src\\assets\\test2.css', mockCSSFilesContent2) + mockCssFiles.set('D:/project-github/unplugin-vue-cssvars/play/src/assets/test2.css', mockCSSFilesContent2) const mockDescriptor = { styles: [{ content: '@import "./assets/test";\n' diff --git a/packages/core/css/__test__/test.css b/packages/core/css/__test__/test.css new file mode 100644 index 0000000..8e69859 --- /dev/null +++ b/packages/core/css/__test__/test.css @@ -0,0 +1,5 @@ +@import "./test2"; +div { + color: v-bind(color); +} + diff --git a/packages/core/css/__test__/test2.css b/packages/core/css/__test__/test2.css new file mode 100644 index 0000000..81490a0 --- /dev/null +++ b/packages/core/css/__test__/test2.css @@ -0,0 +1,4 @@ + +.test { + color: v-bind(appTheme2); +} \ No newline at end of file diff --git a/packages/core/css/pre-process-css.ts b/packages/core/css/pre-process-css.ts index c8378a3..77515af 100644 --- a/packages/core/css/pre-process-css.ts +++ b/packages/core/css/pre-process-css.ts @@ -1,4 +1,4 @@ -import * as path from 'path' +import { parse, resolve } from 'path' import * as csstree from 'css-tree' import fg from 'fast-glob' import fs from 'fs-extra' @@ -8,7 +8,9 @@ import { INJECT_PREFIX_FLAG, INJECT_SUFFIX_FLAG, SUPPORT_FILE, - SUPPORT_FILE_LIST, completeSuffix, + SUPPORT_FILE_LIST, + completeSuffix, + transformSymbol, } from '@unplugin-vue-cssvars/utils' import type { ICSSFileMap, SearchGlobOptions } from '../types' @@ -145,12 +147,12 @@ export function preProcessCSS(options: SearchGlobOptions): ICSSFileMap { // ⭐TODO: 同名文件,不同後綴怎麽處理? 優先級怎麽定? for (const file of files) { // ⭐⭐TODO: 读取内容,後綴怎麽處理? - const code = fs.readFileSync(file, { encoding: 'utf-8' }) - + const code = fs.readFileSync(resolve(rootDir!, file), { encoding: 'utf-8' }) // parse css ast if (file.endsWith(`.${SUPPORT_FILE.CSS}`)) { const cssAst = csstree.parse(code) - const absoluteFilePath = path.resolve(path.parse(file).dir, path.parse(file).base) + let absoluteFilePath = resolve(parse(file).dir, parse(file).base) + absoluteFilePath = transformSymbol(absoluteFilePath) if (!cssFiles.get(absoluteFilePath)) { cssFiles.set(absoluteFilePath, { importer: new Set(), @@ -162,7 +164,7 @@ export function preProcessCSS(options: SearchGlobOptions): ICSSFileMap { const cssF = cssFiles.get(absoluteFilePath)! // 设置 importer if (importer) { - const value = completeSuffix(path.resolve(path.parse(file).dir, importer)) + const value = completeSuffix(transformSymbol(resolve(parse(file).dir, importer))) cssF.importer.add(value) } cssFiles.set(absoluteFilePath, { @@ -173,10 +175,10 @@ export function preProcessCSS(options: SearchGlobOptions): ICSSFileMap { } // ⭐TODO: 支持 sass - if (file.endsWith(`.${SUPPORT_FILE.SASS}`)) { /* empty */ } + // if (file.endsWith(`.${SUPPORT_FILE.SASS}`)) { /* empty */ } // ⭐TODO: 支持 less - if (file.endsWith(`.${SUPPORT_FILE.LESS}`)) { /* empty */ } + // if (file.endsWith(`.${SUPPORT_FILE.LESS}`)) { /* empty */ } } return cssFiles } diff --git a/packages/core/css/process-css.ts b/packages/core/css/process-css.ts index 0cc6dc9..331e9db 100644 --- a/packages/core/css/process-css.ts +++ b/packages/core/css/process-css.ts @@ -1,6 +1,6 @@ import path from 'path' import * as csstree from 'css-tree' -import { completeSuffix } from '@unplugin-vue-cssvars/utils' +import { completeSuffix, transformSymbol } from '@unplugin-vue-cssvars/utils' import { walkCSSTree } from './pre-process-css' import type { ICSSFile, ICSSFileMap } from '../types' import type { SFCDescriptor } from '@vue/compiler-sfc' @@ -36,7 +36,7 @@ export const createCSSModule = (descriptor: SFCDescriptor, id: string, cssFiles: // 根据其 ast,获取 @import 信息 walkCSSTree(cssAst, (importer) => { // 添加后缀 - const key = completeSuffix(path.resolve(path.parse(id).dir, importer)) + const key = completeSuffix(transformSymbol(path.resolve(path.parse(id).dir, importer))) // 根据 @import 信息,从 cssFiles 中,递归的获取所有在预处理时生成的 cssvars 样式 getCSSFileRecursion(key, cssFiles, (res: ICSSFile) => { importModule.push(res) diff --git a/utils/constant.ts b/utils/constant.ts index 8a0e4ad..5fae6a2 100644 --- a/utils/constant.ts +++ b/utils/constant.ts @@ -1,6 +1,6 @@ export const NAME = 'unplugin-vue-cssvars' export const SUPPORT_FILE_LIST = ['**/**.css', '**/**.less', '**/**.scss'] -export const FG_IGNORE_LIST = ['node_modules', 'dist', '.git'] +export const FG_IGNORE_LIST = ['**/node_modules/**', '**/dist/**', '**/.git/**'] export const SUPPORT_FILE = { CSS: 'css', LESS: 'less', diff --git a/utils/index.ts b/utils/index.ts index ef4b354..af95dde 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -14,3 +14,5 @@ export const extend = < export const setTArray = (set: Set): Array => { return [...set] } export const isEmptyObj = (val: unknown) => JSON.stringify(val) === '{}' + +export const transformSymbol = (path: string) => path.replaceAll('\\', '/')