diff --git a/package.json b/package.json index 26d3da0..9f2a172 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ } }, "scripts": { - "pa": "esno packages/core/parser/parser-import.ts", "init": "pnpm i", "lint:fix": "eslint --fix ./ --ext .vue,.js,.ts,.jsx,.tsx,.json ", "dev": "pnpm run --filter @unplugin-vue-cssvars/build dev", 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 index d572319..512b78b 100644 --- a/packages/core/css/__test__/__snapshots__/pre-process-css.spec.ts.snap +++ b/packages/core/css/__test__/__snapshots__/pre-process-css.spec.ts.snap @@ -76,10 +76,7 @@ div{color:v-bind(color)} `; exports[`pre process css > getCurFileContent: basic 1`] = ` -" - - -#app { +"#app { div { color: v-bind(fooColor); } @@ -90,9 +87,7 @@ exports[`pre process css > getCurFileContent: basic 1`] = ` `; exports[`pre process css > getCurFileContent: no ; 1`] = ` -" - -#app { +"#app { div { color: v-bind(fooColor); } diff --git a/packages/core/css/pre-process-css.ts b/packages/core/css/pre-process-css.ts index 21ca23c..0a99a7b 100644 --- a/packages/core/css/pre-process-css.ts +++ b/packages/core/css/pre-process-css.ts @@ -17,6 +17,7 @@ import sass from 'sass' import less from 'less' import stylus from 'stylus' import { parseImports } from '../parser/parser-import' +import { transformQuotes } from '../transform/transform-quotes' import type { ImportStatement } from '../parser/parser-import' import type { ICSSFileMap, SearchGlobOptions } from '../types' @@ -200,26 +201,31 @@ export function generateCSSCode(path: string, suffix: string) { const code = fs.readFileSync(path, { encoding: 'utf-8' }) let res = '' switch (suffix) { - case `.${SUPPORT_FILE.SCSS}`: // scss / sass + case `.${SUPPORT_FILE.SCSS}`: // scss // @import 有 css 和 scss的同名文件,会编译 scss // @import 编译 scss,会一直编译,一直到遇到 import 了一个 css 或没有 import 为止 // 这里先分析出 imports,在根据其内容将 sass 中 import 删除 // 编译 sass 为 css,再复原 // eslint-disable-next-line no-case-declarations - const parseSassImporter = parseImports(code) + const parseScssImporter = parseImports(code, [transformQuotes]) // eslint-disable-next-line no-case-declarations - const codeNoImporter = getCurFileContent(code, parseSassImporter.imports) + const codeScssNoImporter = getCurFileContent(code, parseScssImporter.imports) // eslint-disable-next-line no-case-declarations - const sassParseRes = sass.compileString(codeNoImporter) - res = setImportToCompileRes(sassParseRes.css, parseSassImporter.imports) + const scssParseRes = sass.compileString(codeScssNoImporter) + res = setImportToCompileRes(scssParseRes.css, parseScssImporter.imports) break case `.${SUPPORT_FILE.SASS}`: // sass - // ⭐TODO: 支持 sass - res = '' + // eslint-disable-next-line no-case-declarations + const parseSassImporter = parseImports(code, [transformQuotes]) + // eslint-disable-next-line no-case-declarations + const codeNoImporter = getCurFileContent(code, parseSassImporter.imports) + // eslint-disable-next-line no-case-declarations + const sassParseRes = sass.compileString(codeNoImporter, { syntax: 'indented' }) + res = setImportToCompileRes(sassParseRes.css, parseSassImporter.imports) break case `.${SUPPORT_FILE.LESS}`: // less // eslint-disable-next-line no-case-declarations - const parseLessImporter = parseImports(code) + const parseLessImporter = parseImports(code, [transformQuotes]) // eslint-disable-next-line no-case-declarations const codeLessNoImporter = getCurFileContent(code, parseLessImporter.imports) less.render(codeLessNoImporter, {}, (error, output) => { @@ -230,9 +236,8 @@ export function generateCSSCode(path: string, suffix: string) { }) break case `.${SUPPORT_FILE.STYL}`: // stylus - // TODO unit test // eslint-disable-next-line no-case-declarations - const parseStylusImporter = parseImports(code) + const parseStylusImporter = parseImports(code, [transformQuotes]) // eslint-disable-next-line no-case-declarations const codeStylusNoImporter = getCurFileContent(code, parseStylusImporter.imports) stylus.render(codeStylusNoImporter, {}, (error: Error, css: string) => { @@ -262,7 +267,7 @@ export function getCurFileContent(content: string, parseRes: ImportStatement[]) mgcStr.replaceAll('@require', '') } }) - return mgcStr.toString() + return mgcStr.toString().trimStart() } export function setImportToCompileRes(content: string, parseRes: ImportStatement[]) { diff --git a/packages/core/css/process-css.ts b/packages/core/css/process-css.ts index d604886..82d5fa6 100644 --- a/packages/core/css/process-css.ts +++ b/packages/core/css/process-css.ts @@ -1,7 +1,6 @@ import path from 'path' -import * as csstree from 'css-tree' import { SUPPORT_FILE, completeSuffix, transformSymbol } from '@unplugin-vue-cssvars/utils' -import { walkCSSTree } from './pre-process-css' +import { parseImports } from '../parser/parser-import' import type { ICSSFile, ICSSFileMap } from '../types' import type { SFCDescriptor } from '@vue/compiler-sfc' @@ -32,10 +31,11 @@ export const createCSSModule = (descriptor: SFCDescriptor, id: string, cssFiles: // 遍历 sfc 的 style 标签内容 for (let i = 0; i < descriptor.styles.length; i++) { const content = descriptor.styles[i].content - const cssAst = csstree.parse(content) - // 根据其 ast,获取 @import 信息 - walkCSSTree(cssAst, (importer) => { - const lang = descriptor.styles[i].lang === SUPPORT_FILE.STYLUS ? SUPPORT_FILE.STYL : descriptor.styles[i].lang + const lang = descriptor.styles[i].lang === SUPPORT_FILE.STYLUS ? SUPPORT_FILE.STYL : descriptor.styles[i].lang + + const parseImporterRes = parseImports(content) + parseImporterRes.imports.forEach((res) => { + const importer = res.path // 添加后缀 // sfc中规则:如果@import 指定了后缀,则根据后缀,否则根据当前 script 标签的 lang 属性(默认css) let key = completeSuffix(transformSymbol(path.resolve(path.parse(id).dir, importer)), lang) @@ -47,7 +47,7 @@ export const createCSSModule = (descriptor: SFCDescriptor, id: string, cssFiles: getCSSFileRecursion(key, cssFiles, (res: ICSSFile) => { importModule.push(res) }) - }, { i: true, v: false }) + }) } return importModule } diff --git a/packages/core/parser/__test__/parser-import.spec.ts b/packages/core/parser/__test__/parser-import.spec.ts index 6500350..f873cdf 100644 --- a/packages/core/parser/__test__/parser-import.spec.ts +++ b/packages/core/parser/__test__/parser-import.spec.ts @@ -2,152 +2,509 @@ import { describe, expect, test } from 'vitest' import { ParserState, parseImports } from '../parser-import' describe('parse import', () => { - test('parseImports: Initial -> At', () => { - const { getCurState } = parseImports('@') - expect(getCurState()).toBe(ParserState.At) + test('parseImports: basic', () => { + const input = '@import "./test";\n' + + '@use \'./test-use\';\n' + + '@require \'./test-require\';\n' + + '#app {\n' + + ' div {\n' + + ' color: v-bind(fooColor);\n' + + ' }\n' + + ' .foo {\n' + + ' color: red\n' + + ' }\n' + + '}' + const { + imports, + getCurState, + getCurImport, + } = parseImports(input) + + expect(getCurState()).toBe(ParserState.Initial) + expect(getCurImport()).toBe(undefined) + expect(imports).toMatchObject([ + { type: 'import', path: './test', start: 8, end: 16 }, + { type: 'use', path: './test-use', start: 23, end: 35 }, + { type: 'require', path: './test-require', start: 46, end: 62 }, + ]) }) - test('parseImports: At -> AtImport', () => { - const { getCurState } = parseImports('@i') - expect(getCurState()).toBe(ParserState.AtImport) + test('parseImports: test1', () => { + const test1 = ' @import \'./test.css\';' + expect(parseImports(test1).imports).toMatchObject([{ type: 'import', path: './test.css' }]) + }) + test('parseImports: test2', () => { + const test2 = '@import \'test\';\n@import \'test2\';\n' + expect(parseImports(test2).imports).toMatchObject([{ type: 'import', path: 'test' }, { type: 'import', path: 'test2' }]) + }) + test('parseImports: test3', () => { + const test3 = '@import \\"test.css\\";' + expect(parseImports(test3).imports).toMatchObject([{ type: 'import', path: 'test.css' }]) + }) + test('parseImports: test4', () => { + const test4 = '@import \\"test\\";\n' + expect(parseImports(test4).imports).toMatchObject([{ type: 'import', path: 'test' }]) }) - test('parseImports: At -> AtUse', () => { - const { getCurState } = parseImports('@u') - expect(getCurState()).toBe(ParserState.AtUse) + test('parseImports: test5', () => { + const test5 = '@import \'test.css\'\n' + expect(parseImports(test5).imports).toMatchObject([{ type: 'import', path: 'test.css' }]) }) - test('parseImports: At -> AtRequire', () => { - const { getCurState } = parseImports('@r') - expect(getCurState()).toBe(ParserState.AtRequire) + test('parseImports: test6', () => { + const test6 = '@import \'test\'\n' + expect(parseImports(test6).imports).toMatchObject([{ type: 'import', path: 'test' }]) }) - test('parseImports: At -> Initial', () => { - const { getCurState } = parseImports('@a') - expect(getCurState()).toBe(ParserState.Initial) + test('parseImports: test7', () => { + const test7 = '@import \\"test.css\\"' + expect(parseImports(test7).imports).toMatchObject([{ type: 'import', path: 'test.css' }]) }) - test('parseImports: AtUse -> Initial', () => { - const { getCurState } = parseImports('@use;') - expect(getCurState()).toBe(ParserState.Initial) + test('parseImports: test8', () => { + const test8 = '@import \\"test\\"' + expect(parseImports(test8).imports).toMatchObject([{ type: 'import', path: 'test' }]) }) - test('parseImports: AtImport -> Initial', () => { - const { getCurState } = parseImports('@import;') - expect(getCurState()).toBe(ParserState.Initial) + test('parseImports: test9', () => { + const test9 = '@importB' // 不解析 + expect(parseImports(test9).imports).toMatchObject([]) }) - test('parseImports: AtRequire -> Initial', () => { - const { getCurState } = parseImports('@require;') - expect(getCurState()).toBe(ParserState.Initial) + test('parseImports: test10', () => { + const test10 = '@use \'test.css\';' + expect(parseImports(test10).imports).toMatchObject([{ type: 'use', path: 'test.css' }]) + }) + test('parseImports: test11', () => { + const test11 = '@use \'test\';' + expect(parseImports(test11).imports).toMatchObject([{ type: 'use', path: 'test' }]) + }) + test('parseImports: test12', () => { + const test12 = '@use \\"./test.css\\";' + expect(parseImports(test12).imports).toMatchObject([{ type: 'use', path: './test.css' }]) + }) + test('parseImports: test13', () => { + const test13 = '@use \\"./test\\";\n' + expect(parseImports(test13).imports).toMatchObject([{ type: 'use', path: './test' }]) + }) + test('parseImports: test14', () => { + const test14 = '@use \'test.css\'' + expect(parseImports(test14).imports).toMatchObject([{ type: 'use', path: 'test.css' }]) + }) + test('parseImports: test15', () => { + const test15 = '@use \'test\'' + expect(parseImports(test15).imports).toMatchObject([{ type: 'use', path: 'test' }]) + }) + test('parseImports: test16', () => { + const test16 = '@use \\"./test.css\\"' + expect(parseImports(test16).imports).toMatchObject([{ type: 'use', path: './test.css' }]) + }) + test('parseImports: test17', () => { + const test17 = '@use \\"test\\"' + expect(parseImports(test17).imports).toMatchObject([{ type: 'use', path: 'test' }]) + }) + test('parseImports: test18', () => { + const test18 = '@usetest' // 不解析 + expect(parseImports(test18).imports).toMatchObject([]) + }) + test('parseImports: test19', () => { + const test19 = '@require \'test.css\';' // { type: 'import', path: '\'./test.css\''} + expect(parseImports(test19).imports).toMatchObject([{ type: 'require', path: 'test.css' }]) + }) + test('parseImports: test20', () => { + const test20 = '@require \'test\';' // { type: 'import', path: '\'./test\''} + expect(parseImports(test20).imports).toMatchObject([{ type: 'require', path: 'test' }]) + }) + test('parseImports: test21', () => { + const test21 = '@require \\"test.css\\";' // { type: 'import', path: '\\"./test.css\\"'} + expect(parseImports(test21).imports).toMatchObject([{ type: 'require', path: 'test.css' }]) + }) + test('parseImports: test22', () => { + const test22 = '@require \\"test\\";' + expect(parseImports(test22).imports).toMatchObject([{ type: 'require', path: 'test' }]) + }) + test('parseImports: test23', () => { + const test23 = '@require \'test.css\'' + expect(parseImports(test23).imports).toMatchObject([{ type: 'require', path: 'test.css' }]) + }) + test('parseImports: test24', () => { + const test24 = '@require \'test\'' + expect(parseImports(test24).imports).toMatchObject([{ type: 'require', path: 'test' }]) + }) + test('parseImports: test25', () => { + const test25 = '@require \\"test.css\\"' + expect(parseImports(test25).imports).toMatchObject([{ type: 'require', path: 'test.css' }]) + }) + test('parseImports: test26', () => { + const test26 = '@require \\"test\\"' + expect(parseImports(test26).imports).toMatchObject([{ type: 'require', path: 'test' }]) + }) + test('parseImports: test27', () => { + const test27 = '@requiretest' + expect(parseImports(test27).imports).toMatchObject([]) + }) + test('parseImports: test28', () => { + const test28 = '@require test.css' + expect(parseImports(test28).imports).toMatchObject([{ type: 'require', path: 'test.css' }]) + }) + test('parseImports: test29', () => { + const test29 = '@require ./test' + expect(parseImports(test29).imports).toMatchObject([{ type: 'require', path: './test' }]) + }) + test('parseImports: test30', () => { + const test30 = '@require test;' + expect(parseImports(test30).imports).toMatchObject([{ type: 'require', path: 'test' }]) }) + test('parseImports: test31', () => { + const test31 = '@require ./test\n' + expect(parseImports(test31).imports).toMatchObject([{ type: 'require', path: './test' }]) + }) + test('parseImports: test32', () => { + const test32 = '@require test.css@require test2.css' + expect(parseImports(test32).imports).toMatchObject([ + { type: 'require', path: 'test.css@require' }, + { type: 'require', path: 'test2.css' }, + ]) + }) + test('parseImports: test33', () => { + const test33 = '@require test.css,@require test2.css' + expect(parseImports(test33).imports).toMatchObject([ + { type: 'require', path: 'test.css' }, + { type: 'require', path: 'test2.css' }, + ]) + }) + test('parseImports: test34', () => { + const test34 = '@require test.css;@require test2.css' + expect(parseImports(test34).imports).toMatchObject([ + { type: 'require', path: 'test.css' }, + { type: 'require', path: 'test2.css' }, + ]) + }) + test('parseImports: test35', () => { + const test35 = '@require test.css; @require test2.css' + expect(parseImports(test35).imports).toMatchObject([ + { type: 'require', path: 'test.css' }, + { type: 'require', path: 'test2.css' }, + ]) + }) + test('parseImports: test36', () => { + const test36 = '@require test.css, @require test2.css' + expect(parseImports(test36).imports).toMatchObject([ + { type: 'require', path: 'test.css' }, + { type: 'require', path: 'test2.css' }, + ]) + }) + test('parseImports: test37', () => { + const test37 = '@require test.css @require test2.css' + expect(parseImports(test37).imports).toMatchObject([ + { type: 'require', path: 'test.css' }, + { type: 'require', path: 'test2.css' }, + ]) + }) + test('parseImports: test38', () => { + const test38 = '@require test.css @use test2.css' + expect(parseImports(test38).imports).toMatchObject([ + { type: 'require', path: 'test.css' }, + { type: 'use', path: 'test2.css' }, + ]) + }) + test('parseImports: test39', () => { + const test39 = '@import ./test1, ./test2' + expect(parseImports(test39).imports).toMatchObject([ + { type: 'import', path: './test1' }, + { type: 'import', path: './test2' }, + ]) + }) + test('parseImports: test40', () => { + const test40 = '@import ./test1, ./test2;\n' + expect(parseImports(test40).imports).toMatchObject([ + { type: 'import', path: './test1' }, + { type: 'import', path: './test2' }, + ]) + }) + test('parseImports: test41', () => { + const test41 = '@use ./test1,./test2' + const res = parseImports(test41).imports + expect(res).toMatchObject([ + { type: 'use', path: './test1' }, + { type: 'use', path: './test2' }, + ]) + }) + test('parseImports: test42', () => { + const test42 = '@use ./test1,./test2;' + expect(parseImports(test42).imports).toMatchObject([ + { type: 'use', path: './test1' }, + { type: 'use', path: './test2' }, + ]) + }) + test('parseImports: test43', () => { + const test43 = '@require ./test1,./test2'// { type: 'import', path: '\'./test\''}, { type: 'import', path: '\'./test2\''} + expect(parseImports(test43).imports).toMatchObject([ + { type: 'require', path: './test1' }, + { type: 'require', path: './test2' }, + ]) + }) + test('parseImports: test44', () => { + const test44 = '@require ./test1,./test2;'// { type: 'import', path: '\'./test\''}, { type: 'import', path: '\'./test2\''} + expect(parseImports(test44).imports).toMatchObject([ + { type: 'require', path: './test1' }, + { type: 'require', path: './test2' }, + ]) + }) + test('parseImports: test45', () => { + const test45 = '@import \\"./test1\\",\\"./test2\\"' // { type: 'import', path: '\\"./test\\"'}, { type: 'import', path: '\\"./test2\\"'} + expect(parseImports(test45).imports).toMatchObject([ + { type: 'import', path: './test1' }, + { type: 'import', path: './test2' }, + ]) + }) + test('parseImports: test46', () => { + const test46 = '@import \\"./test1\\",\\"./test2\\";'// { type: 'import', path: '\\"./test\\"'}, { type: 'import', path: '\\"./test2\\"'} + expect(parseImports(test46).imports).toMatchObject([ + { type: 'import', path: './test1' }, + { type: 'import', path: './test2' }, + ]) + }) + test('parseImports: test47', () => { + const test47 = '@use \\"./test1\\",\\"./test2\\"' // { type: 'import', path: '\\"./test\\"'}, { type: 'import', path: '\\"./test2\\"'} + expect(parseImports(test47).imports).toMatchObject([ + { type: 'use', path: './test1' }, + { type: 'use', path: './test2' }, + ]) + }) + test('parseImports: test48', () => { + const test48 = '@use \\"./test1\\",\\"./test2\\";'// { type: 'import', path: '\\"./test\\"'}, { type: 'import', path: '\\"./test2\\"'} + expect(parseImports(test48).imports).toMatchObject([ + { type: 'use', path: './test1' }, + { type: 'use', path: './test2' }, + ]) + }) + test('parseImports: test49', () => { + const test49 = '@require \\"./test1\\", \\"./test2\\"' // { type: 'import', path: '\\"./test\\"'}, { type: 'import', path: '\\"./test2\\"'} + expect(parseImports(test49).imports).toMatchObject([ + { type: 'require', path: './test1' }, + { type: 'require', path: './test2' }, + ]) + }) + test('parseImports: test50', () => { + const test50 = '@require \\"./test1\\", \\"./test2\\";'// { type: 'import', path: '\\"./test\\"'}, { type: 'import', path: '\\"./test2\\"'} + expect(parseImports(test50).imports).toMatchObject([ + { type: 'require', path: './test1' }, + { type: 'require', path: './test2' }, + ]) + }) + test('parseImports: test51', () => { + const test51 = '@import \'./test1\',\'./test2\'' // { type: 'import', path: '\'./test\''}, { type: 'import', path: '\'./test2\''} + expect(parseImports(test51).imports).toMatchObject([ + { type: 'import', path: './test1' }, + { type: 'import', path: './test2' }, + ]) + }) + test('parseImports: test52', () => { + const test52 = '@import \'./test1\',\'./test2\';'// { type: 'import', path: '\'./test\''}, { type: 'import', path: '\'./test2\''} + expect(parseImports(test52).imports).toMatchObject([ + { type: 'import', path: './test1' }, + { type: 'import', path: './test2' }, + ]) + }) + test('parseImports: test53', () => { + const test53 = '@use \'./test1\',\'./test2\'' // { type: 'import', path: '\'./test\''}, { type: 'import', path: '\'./test2\''} + expect(parseImports(test53).imports).toMatchObject([ + { type: 'use', path: './test1' }, + { type: 'use', path: './test2' }, + ]) + }) + test('parseImports: test54', () => { + const test54 = '@use \'./test1\',\'./test2\';'// { type: 'import', path: '\'./test\''}, { type: 'import', path: '\'./test2\''} + expect(parseImports(test54).imports).toMatchObject([ + { type: 'use', path: './test1' }, + { type: 'use', path: './test2' }, + ]) + }) + test('parseImports: test55', () => { + const test55 = '@use \'./test1\',\'./test2\';'// { type: 'import', path: '\'./test\''}, { type: 'import', path: '\'./test2\''} + expect(parseImports(test55).imports).toMatchObject([ + { type: 'use', path: './test1' }, + { type: 'use', path: './test2' }, + ]) + }) + test('parseImports: test56', () => { + const test56 = '@require \'./test1\', \'./test2\'' // { type: 'import', path: '\'./test\''}, { type: 'import', path: '\'./test2\''} + expect(parseImports(test56).imports).toMatchObject([ + { type: 'require', path: './test1' }, + { type: 'require', path: './test2' }, + ]) + }) + test('parseImports: test57', () => { + const test57 = '@require \'./test1\', \'./test2\';' + expect(parseImports(test57).imports).toMatchObject([ + { type: 'require', path: './test1' }, + { type: 'require', path: './test2' }, + ]) + }) + test('parseImports: test58', () => { + const test58_1 = '@requiretest\\"' + expect(() => parseImports(test58_1)).toThrowError('syntax error: unmatched quotes') + const test58_2 = '@requiretest\'' + expect(() => parseImports(test58_2)).toThrowError('syntax error: unmatched quotes') + + const test58_3 = 'e@require\\"st' + expect(() => parseImports(test58_3)).toThrowError('syntax error') - test('parseImports: AtUse -> StringLiteral', () => { - const { getCurState, getCurImport } = parseImports('@use "') - expect(getCurState()).toBe(ParserState.StringLiteral) - expect(getCurImport()).toMatchObject({ type: 'use', path: '"', start: 5 }) + const test58_4 = '@requiretests @require test' + expect(() => parseImports(test58_4)).toThrowError('syntax error: unknown At Rule') - const { getCurState: getCurState1, getCurImport: getCurImport1 } = parseImports('@use \'') - expect(getCurState1()).toBe(ParserState.StringLiteral) - expect(getCurImport1()).toMatchObject({ type: 'use', path: '\'', start: 5 }) + const test58_5 = '@requ\\"iretest' + expect(() => parseImports(test58_5)).toThrowError('syntax error') - const { getCurState: getCurState2, getCurImport: getCurRequire1 } = parseImports('@require \'') - expect(getCurState2()).toBe(ParserState.StringLiteral) - expect(getCurRequire1()).toMatchObject({ type: 'require', path: '\'', start: 9 }) + const test58_6 = 'e@requireete\'st' + expect(() => parseImports(test58_6)).toThrowError('syntax error') + + const test58_7 = '@requ\'iretest' + expect(() => parseImports(test58_7)).toThrowError('syntax error') + + const test58_8 = '@require \'teasd' + expect(() => parseImports(test58_8)).toThrowError('syntax error: unmatched quotes') + + const test58_9 = 'adwad @require testadwad' + expect(parseImports(test58_9).imports).toMatchObject([{ type: 'require', path: 'testadwad' }]) + + const test58_10 = '@require tea\'sd' + expect(() => { parseImports(test58_10) }).toThrowError('syntax error: unmatched quotes') + + const test58_11 = '@require teasd\'' + expect(() => parseImports(test58_11)).toThrowError('syntax error: unmatched quotes') + + const test58_12 = '@require \\"teasd' + expect(() => parseImports(test58_12)).toThrowError('syntax error: unmatched quotes') + + const test58_13 = '@require tea\\"sd' + expect(() => parseImports(test58_13)).toThrowError('syntax error: unmatched quotes') + + const test58_14 = '@require teasd\\"' + expect(() => parseImports(test58_14)).toThrowError('syntax error: unmatched quotes') + + const test58_15 = '@at-root teasd;@require foo' + expect(parseImports(test58_15).imports).toMatchObject([ + { type: 'require', path: 'foo' }, + ]) + }) + test('parseImports: test59', () => { + const test59 = '@require tea;"sd"' + expect(parseImports(test59).imports).toMatchObject([ + { type: 'require', path: 'tea' }, + ]) + }) + test('parseImports: test60', () => { + const test60 = '@require "tea";sd' + expect(parseImports(test60).imports).toMatchObject([ + { type: 'require', path: 'tea' }, + ]) + }) + test('parseImports: test61', () => { + const test61 = '@require "tea",sd' + expect(parseImports(test61).imports).toMatchObject([ + { type: 'require', path: 'tea' }, + { type: 'require', path: 'sd' }, + ]) + }) + test('parseImports: test62', () => { + const test62 = '@require tea,"sd"' + expect(parseImports(test62).imports).toMatchObject([ + { type: 'require', path: 'tea' }, + { type: 'require', path: 'sd' }, + ]) + }) + test('parseImports: test63', () => { + const test63 = '@require tea "sd"' + expect(parseImports(test63).imports).toMatchObject([ + { type: 'require', path: 'tea' }, + { type: 'require', path: 'sd' }, + ]) + }) + test('parseImports: test64', () => { + const test64 = '@require "sd" tea' + expect(parseImports(test64).imports).toMatchObject([ + { type: 'require', path: 'sd' }, + { type: 'require', path: 'tea' }, + ]) + }) + test('parseImports: test65', () => { + const test65 = '@require \'sd\',tea' + expect(parseImports(test65).imports).toMatchObject([ + { type: 'require', path: 'sd' }, + { type: 'require', path: 'tea' }, + ]) + }) + test('parseImports: test66', () => { + const test66 = '@require sd,\'tea\'' + expect(parseImports(test66).imports).toMatchObject([ + { type: 'require', path: 'sd' }, + { type: 'require', path: 'tea' }, + ]) }) - test('parseImports: StringLiteral -> concat string', () => { - const { getCurState, getCurImport } = parseImports('@import "test') - expect(getCurState()).toBe(ParserState.StringLiteral) - expect(getCurImport()).toMatchObject({ type: 'import', path: '"test', start: 8 }) + test('parseImports: test67', () => { + const test67 = '@require sd;\'tea\'' + expect(parseImports(test67).imports).toMatchObject([ + { type: 'require', path: 'sd' }, + ]) + }) - const { getCurState: getCurState1, getCurImport: getCurImport1 } = parseImports('@use "test') - expect(getCurState1()).toBe(ParserState.StringLiteral) - expect(getCurImport1()).toMatchObject({ type: 'use', path: '"test', start: 5 }) + test('parseImports: test68', () => { + const test68 = '@require \'tea\';sd' + expect(parseImports(test68).imports).toMatchObject([ + { type: 'require', path: 'tea' }, + ]) + }) - const { getCurState: getCurState2, getCurImport: getCurRequire1 } = parseImports('@require "test') - expect(getCurState2()).toBe(ParserState.StringLiteral) - expect(getCurRequire1()).toMatchObject({ type: 'require', path: '"test', start: 9 }) + test('parseImports: test69', () => { + const test69 = '// @require \'tea\';sd' // 无输出 + expect(parseImports(test69).imports).toMatchObject([]) }) - test('parseImports: AtImport -> end', () => { - const { - imports: imports1, - getCurState: getCurState1, - getCurImport: getCurImport1, - } = parseImports('@use "test";') - expect(getCurState1()).toBe(ParserState.Initial) - expect(getCurImport1()).toBe(undefined) - expect(imports1).toMatchObject([{ type: 'use', path: '"test"', start: 5, end: 11 }]) + test('parseImports: test70', () => { + const test70 = '// @require \'tea\';sd\n@require \'redtea\';suda' + expect(parseImports(test70).imports).toMatchObject([ + { type: 'require', path: 'redtea' }, + ]) + }) - const { - imports: imports2, - getCurState: getCurState2, - getCurImport: getCurImport2, - } = parseImports('@use "test"') - expect(getCurState2()).toBe(ParserState.AtUse) - expect(getCurImport2()).toMatchObject({ type: 'use', path: '"test"', start: 5 }) - expect(imports2.length).toBe(0) + test('parseImports: test71', () => { + expect(() => parseImports('@require // \'tea\';sd')) + .toThrowError('syntax error') + }) - const { - imports: imports4, - getCurState: getCurState4, - getCurImport: getCurImport4, - } = parseImports('@import "test";') - expect(getCurState4()).toBe(ParserState.Initial) - expect(getCurImport4()).toBe(undefined) - expect(imports4).toMatchObject([{ type: 'import', path: '"test"', start: 8, end: 14 }]) + test('parseImports: test72', () => { + const test72 = '@require \'tea\';sd\n//@require \'tea\';sd' - const { - imports: imports3, - getCurState: getCurState3, - getCurImport: getCurImport3, - } = parseImports('@import "test"') - expect(getCurState3()).toBe(ParserState.AtImport) - expect(getCurImport3()).toMatchObject({ type: 'import', path: '"test"', start: 8 }) - expect(imports3.length).toBe(0) + expect(parseImports(test72).imports).toMatchObject([ + { type: 'require', path: 'tea' }, + ]) + }) - const { - imports: imports5, - getCurState: getCurState5, - getCurImport: getCurImport5, - } = parseImports('@require "test";') - expect(getCurState5()).toBe(ParserState.Initial) - expect(getCurImport5()).toBe(undefined) - expect(imports5).toMatchObject([{ type: 'require', path: '"test"', start: 9, end: 15 }]) + test('parseImports: test73', () => { + const test73 = '/*@require \'tea\';sd\n*/@require \'tea\';sd' - const { - imports: imports6, - getCurState: getCurState6, - getCurImport: getCurImport6, - } = parseImports('@require "test"') - expect(getCurState6()).toBe(ParserState.AtRequire) - expect(getCurImport6()).toMatchObject({ type: 'require', path: '"test"', start: 9 }) - expect(imports6.length).toBe(0) + expect(parseImports(test73).imports).toMatchObject([ + { type: 'require', path: 'tea' }, + ]) }) - test('parseImports: basic', () => { - const { - imports, - getCurState, - getCurImport, - } = parseImports('@import "./test";\n' - + '@use \'./test-use\';\n' - + '@require \'./test-require\';\n' - + '#app {\n' - + ' div {\n' - + ' color: v-bind(fooColor);\n' - + ' }\n' - + ' .foo {\n' - + ' color: red\n' - + ' }\n' - + '}') - expect(getCurState()).toBe(ParserState.Initial) - expect(getCurImport()).toBe(undefined) - expect(imports).toMatchObject([ - { type: 'import', path: '"./test"', start: 8, end: 16 }, - { type: 'use', path: '\'./test-use\'', start: 23, end: 35 }, - { type: 'require', path: '\'./test-require\'', start: 46, end: 62 }, + test('parseImports: test74', () => { + const test74 = '@require \'tea\';sd\n/*@require */' + expect(parseImports(test74).imports).toMatchObject([ + { type: 'require', path: 'tea' }, ]) }) + + test('parseImports: test75', () => { + const test75 = '/*@require \'tea\';sd\n//@require */\'tea\';sd' + expect(parseImports(test75).imports).toMatchObject([]) + }) + + test('parseImports: test76', () => { + expect(() => parseImports('@require /* \'tea\';sd')) + .toThrowError('syntax error') + }) }) diff --git a/packages/core/parser/parser-import.ts b/packages/core/parser/parser-import.ts index e182e1e..0232af8 100644 --- a/packages/core/parser/parser-import.ts +++ b/packages/core/parser/parser-import.ts @@ -1,9 +1,15 @@ +const innerAtRule = 'media,extend,at-root,debug,warn,forward,mixin,include,function,error' export enum ParserState { Initial, - At, + InlineComment, + Comment, + AtStart, + AtEnd, AtImport, AtUse, AtRequire, + QuotesStart, + QuotesEnd, StringLiteral, } @@ -14,7 +20,9 @@ export interface ImportStatement { end?: number suffix?: string } -export function parseImports(source: string): { +const delTransformSymbol = (content: string) => content.replace(/[\r\t\f\v\\]/g, '') + +export function parseImports(content: string, helper?: Array): { imports: ImportStatement[] getCurState: () => ParserState getCurImport: () => undefined | ImportStatement @@ -23,67 +31,191 @@ export function parseImports(source: string): { let currentImport: ImportStatement | undefined let state = ParserState.Initial let i = 0 - + let AtPath = '' + const source = delTransformSymbol(content) while (i < source.length) { const char = source[i] switch (state) { case ParserState.Initial: if (char === '@') - state = ParserState.At - + state = ParserState.AtStart + if (char === '/' && source[i + 1] === '/') + state = ParserState.InlineComment + if (char === '/' && source[i + 1] === '*') + state = ParserState.Comment break - case ParserState.At: - if (char === 'i') { - state = ParserState.AtImport - currentImport = { type: 'import', path: '' } - i++ // skip over "i" to next character - } else if (char === 'u') { - state = ParserState.AtUse - currentImport = { type: 'use', path: '' } - i++ // skip over "u" to next character - } else if (char === 'r') { - state = ParserState.AtRequire - currentImport = { type: 'require', path: '' } - i++ // skip over "u" to next character - } else { + case ParserState.InlineComment: + if (char === '\n') state = ParserState.Initial + break + case ParserState.Comment: + if (char === '*' && source[i + 1] === '/') + state = ParserState.Initial + break + case ParserState.AtStart: + if (/[A-Za-z]$/.test(char)) + AtPath = AtPath + char + else + state = ParserState.AtEnd + + if (i === source.length - 1) { + if (char === '"' || char === "'") + throw new Error('syntax error: unmatched quotes') + else + walkContentEnd(i) + } + if (!(/[A-Za-z]$/.test(char)) + && char !== '\n' + && char !== ' ' + && char !== '-') + throw new Error('syntax error') + + break + case ParserState.AtEnd: + if (char !== '\n' && char !== ' ' && char !== '-') { + if (char === '/') + throw new Error('syntax error') + + if (AtPath === 'import') { + AtPath = '' + state = ParserState.AtImport + currentImport = { type: 'import', path: '' } + i-- + } else if (AtPath === 'use') { + AtPath = '' + state = ParserState.AtUse + currentImport = { type: 'use', path: '' } + i-- + } else if (AtPath === 'require') { + AtPath = '' + state = ParserState.AtRequire + currentImport = { type: 'require', path: '' } + i-- + } else { + if (!innerAtRule.includes(AtPath)) + throw new Error('syntax error: unknown At Rule') + + AtPath = '' + state = ParserState.Initial + } } + break case ParserState.AtImport: case ParserState.AtUse: case ParserState.AtRequire: + // '@require test.css;@require test2.css' + if (char === '@' && !(/[A-Za-z]$/.test(source[i - 1]))) { + i-- + state = ParserState.Initial + break + } + + if (char === '/' && (source[i + 1] === '/' || source[i + 1] === '*')) { + i-- + state = ParserState.Initial + break + } + if (char === "'" || char === '"') { - state = ParserState.StringLiteral + currentImport!.start = i + state = ParserState.QuotesStart + break + } + if (char !== '\n' && char !== ' ') { currentImport!.start = i currentImport!.path += char - } else if (char === ';') { - if (currentImport && currentImport.start !== undefined) { - currentImport.end = i - imports.push(currentImport) - currentImport = undefined - } - state = ParserState.Initial + state = ParserState.StringLiteral + break } break - case ParserState.StringLiteral: + case ParserState.QuotesStart: if (char === "'" || char === '"') { - if (currentImport!.type === 'import') - state = ParserState.AtImport + currentImport!.end = i + state = ParserState.QuotesEnd + if (i === source.length - 1) + walkContentEnd(i) + break + } + if (i === source.length - 1) + throw new Error('syntax error: unmatched quotes') - if (currentImport!.type === 'use') - state = ParserState.AtUse + currentImport!.path += char + break + case ParserState.QuotesEnd: + if (i === source.length - 1 || char === ';' || char === '\n') { + walkContentEnd(i) + } else { + i-- + state = ParserState.StringLiteral + } + break + case ParserState.StringLiteral: + if (char === ';' || char === '\n') { + walkContentEnd(i) + break + } - if (currentImport!.type === 'require') + // '@require test.css@require test2.css' + // '@import ./test1, ./test2' + if ( + char !== '@' + && (char === ' ' + || char === ',' + || char === '"' + || char === "'")) { + const curType = currentImport?.type + walkContentEnd(i) + if (curType === 'import') { + state = ParserState.AtImport + currentImport = { type: 'import', path: '' } + } else if (curType === 'use') { + state = ParserState.AtUse + currentImport = { type: 'use', path: '' } + } else if (curType === 'require') { state = ParserState.AtRequire + currentImport = { type: 'require', path: '' } + } - currentImport!.path += char - } else { currentImport!.path += char } + if (char === "'" || char === '"') { + currentImport!.start = i + state = ParserState.QuotesStart + if (i === source.length - 1 && (char === '"' || char === "'")) + throw new Error('syntax error: unmatched quotes') + + break + } + + break + } + + currentImport!.path += char + if (i === source.length - 1) + walkContentEnd(i) break } i++ } + function walkContentEnd(index: number) { + pushCurrentImport(index) + state = ParserState.Initial + } + + function pushCurrentImport(index: number) { + if (currentImport && currentImport.start !== undefined) { + currentImport.end = index + if (helper) { + helper.forEach((fn) => { + currentImport = fn(currentImport) + }) + } + imports.push(currentImport) + currentImport = undefined + } + } + function getCurState() { return state } diff --git a/packages/core/transform/__test__/transform-quotes.spec.ts b/packages/core/transform/__test__/transform-quotes.spec.ts new file mode 100644 index 0000000..c720560 --- /dev/null +++ b/packages/core/transform/__test__/transform-quotes.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from 'vitest' +import { transformQuotes } from '../transform-quotes' + +describe('transform', () => { + test('transformQuotes', () => { + const testCases = [ + { input: 'hello', expected: '"hello"' }, + { input: '"hello"', expected: '"hello"' }, + { input: "'world'", expected: '"world"' }, + ] + testCases.forEach(({ input, expected }) => { + const result = transformQuotes({ path: input } as any) + expect(result.path).toBe(expected) + }) + }) +}) diff --git a/packages/core/transform/transform-quotes.ts b/packages/core/transform/transform-quotes.ts new file mode 100644 index 0000000..dc12119 --- /dev/null +++ b/packages/core/transform/transform-quotes.ts @@ -0,0 +1,11 @@ +import type { ImportStatement } from '../parser/parser-import' + +export function transformQuotes(importer: ImportStatement) { + if (!importer.path.startsWith('"') && !importer.path.endsWith('"')) { + if (importer.path.startsWith("'") && importer.path.endsWith("'")) + importer.path = `"${importer.path.slice(1, -1)}"` + else + importer.path = `"${importer.path}"` + } + return importer +} diff --git a/play/src/App.vue b/play/src/App.vue index 3da8ab2..088dea1 100644 --- a/play/src/App.vue +++ b/play/src/App.vue @@ -6,6 +6,7 @@ const appAsd = () => 'red' const fooColor = appAsd() const appTheme2 = 'blue' const lessColor = 'greenyellow' +const sassColor = '#94c9ff' const stylColor = '#fd1d7c' const appTheme3 = ref('red') const appTheme4 = reactive({ color: 'red' }) @@ -63,13 +64,13 @@ export default { --> - diff --git a/play/src/assets/sass/foo.sass b/play/src/assets/sass/foo.sass new file mode 100644 index 0000000..7202027 --- /dev/null +++ b/play/src/assets/sass/foo.sass @@ -0,0 +1,4 @@ +@import test.sass +#app + div + color: v-bind(sassColor) diff --git a/play/src/assets/sass/test.sass b/play/src/assets/sass/test.sass new file mode 100644 index 0000000..e804da3 --- /dev/null +++ b/play/src/assets/sass/test.sass @@ -0,0 +1,2 @@ +.sass + color: v-bind(color) diff --git a/play/src/assets/scss/bar.scss b/play/src/assets/scss/bar.scss index 6543921..448776a 100644 --- a/play/src/assets/scss/bar.scss +++ b/play/src/assets/scss/bar.scss @@ -1 +1 @@ -@import "./foo.scss.scss"; +@import "./foo.scss"; diff --git a/play/src/assets/styuls/test.styl b/play/src/assets/styuls/test.styl index eaf6608..06272a4 100644 --- a/play/src/assets/styuls/test.styl +++ b/play/src/assets/styuls/test.styl @@ -1,3 +1,2 @@ -.styl { +.styl color: v-bind(color); -}