diff --git a/playground/css-modules/__tests__/css-modules.spec.ts b/playground/css-modules/__tests__/css-modules.spec.ts new file mode 100644 index 000000000..7b0f5f12b --- /dev/null +++ b/playground/css-modules/__tests__/css-modules.spec.ts @@ -0,0 +1,458 @@ +import path from 'node:path' +import { describe, expect, test } from 'vitest' +import { type InlineConfig, type Rollup, build } from 'vite' +import { base64Module, isBuild, isServe, page } from '~utils' + +async function getStyleMatchingId(id: string) { + const styleTags = page.locator(`style[data-vite-dev-id*=${id}]`) + let code = '' + for (const style of await styleTags.all()) { + code += await style.textContent() + } + return code +} + +async function viteBuild(root: string, inlineConfig?: InlineConfig) { + const built = await build({ + root: path.resolve(__dirname, root), + configFile: false, + envFile: false, + logLevel: 'error', + ...inlineConfig, + build: { + // Prevents CSS minification from handling the de-duplication of classes + minify: false, + write: false, + lib: { + entry: 'index.js', + formats: ['es'], + }, + ...inlineConfig?.build, + }, + }) + + if (!Array.isArray(built)) { + throw new TypeError('Build result is not an array') + } + + const { output } = built[0]! + const css = output.find( + (file) => file.type === 'asset' && file.fileName.endsWith('.css'), + ) as Rollup.OutputAsset | undefined + + return { + js: output[0].code, + css: css?.source.toString(), + } +} + +test.runIf(isBuild)('Multi CSS modules', async () => { + const { js, css } = await viteBuild('../multi-css-modules', { + build: { + target: 'es2022', + }, + css: { + modules: { + generateScopedName: 'asdf_[local]', + }, + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + style1: { + className1: expect.stringMatching(/^asdf_className1\s+asdf_util-class$/), + }, + style2: { + 'class-name2': expect.stringMatching( + /^asdf_class-name2\s+asdf_util-class$/, + ), + }, + }) + + expect(css).toMatch('--file: "style1.module.css"') + expect(css).toMatch('--file: "style2.module.css"') + + // Ensure that PostCSS is applied to the composed files + expect(css).toMatch('--file: "utils1.css?.module.css"') + expect(css).toMatch('--file: "utils2.css?.module.css"') + + // Util is not duplicated + const utilClass = Array.from(css!.matchAll(/foo/g)) + expect(utilClass.length).toBe(1) +}) + +describe.runIf(isBuild)('localsConvention', () => { + test('camelCase', async () => { + const { js } = await viteBuild('../multi-css-modules', { + build: { + target: 'es2022', + }, + css: { + modules: { + localsConvention: 'camelCase', + }, + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + style1: { + 'class-name2': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + className1: expect.stringMatching(/_className1_\w+ _util-class_\w+/), + className2: expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + default: { + className1: expect.stringMatching(/_className1_\w+ _util-class_\w+/), + 'class-name2': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + className2: expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + }, + }, + style2: { + 'class-name2': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+/, + ), + className2: expect.stringMatching(/_class-name2_\w+ _util-class_\w+/), + default: { + 'class-name2': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+/, + ), + className2: expect.stringMatching(/_class-name2_\w+ _util-class_\w+/), + }, + }, + }) + }) + + test('camelCaseOnly', async () => { + const { js } = await viteBuild('../multi-css-modules', { + css: { + modules: { + localsConvention: 'camelCaseOnly', + }, + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + style1: { + className1: expect.stringMatching(/_className1_\w+ _util-class_\w+/), + className2: expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + default: { + className1: expect.stringMatching(/_className1_\w+ _util-class_\w+/), + className2: expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + }, + }, + style2: { + className2: expect.stringMatching(/_class-name2_\w+ _util-class_\w+/), + default: { + className2: expect.stringMatching(/_class-name2_\w+ _util-class_\w+/), + }, + }, + }) + }) + + test('dashes', async () => { + const { js } = await viteBuild('../multi-css-modules', { + build: { + target: 'es2022', + }, + css: { + modules: { + localsConvention: 'dashes', + }, + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + style1: { + 'class-name2': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + className1: expect.stringMatching(/_className1_\w+ _util-class_\w+/), + className2: expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + default: { + className1: expect.stringMatching(/_className1_\w+ _util-class_\w+/), + 'class-name2': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + className2: expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + }, + }, + style2: { + 'class-name2': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+/, + ), + className2: expect.stringMatching(/_class-name2_\w+ _util-class_\w+/), + default: { + 'class-name2': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+/, + ), + className2: expect.stringMatching(/_class-name2_\w+ _util-class_\w+/), + }, + }, + }) + }) + + test('dashesOnly', async () => { + const { js } = await viteBuild('../multi-css-modules', { + css: { + modules: { + localsConvention: 'dashesOnly', + }, + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + style1: { + className1: expect.stringMatching(/_className1_\w+ _util-class_\w+/), + className2: expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + default: { + className1: expect.stringMatching(/_className1_\w+ _util-class_\w+/), + className2: expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + }, + }, + style2: { + className2: expect.stringMatching(/_class-name2_\w+ _util-class_\w+/), + default: { + className2: expect.stringMatching(/_class-name2_\w+ _util-class_\w+/), + }, + }, + }) + }) + + test('function', async () => { + const { js } = await viteBuild('../multi-css-modules', { + build: { + target: 'es2022', + }, + css: { + modules: { + localsConvention: (originalClassname) => `${originalClassname}123`, + }, + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + style1: { + className1123: expect.stringMatching(/_className1_\w+ _util-class_\w+/), + 'class-name2123': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + default: { + className1123: expect.stringMatching( + /_className1_\w+ _util-class_\w+/, + ), + 'class-name2123': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + }, + }, + style2: { + 'class-name2123': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+/, + ), + default: { + 'class-name2123': expect.stringMatching( + /_class-name2_\w+ _util-class_\w+/, + ), + }, + }, + }) + }) +}) + +test.runIf(isBuild)('globalModulePaths', async () => { + const { js, css } = await viteBuild('../global-module', { + css: { + modules: { + globalModulePaths: [/global\.module\.css/], + }, + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + default: { + title: expect.stringMatching(/^_title_\w{5}/), + }, + title: expect.stringMatching(/^_title_\w{5}/), + }) + + expect(css).toMatch('.page {') +}) + +test.runIf(isBuild)('inline', async () => { + const { js } = await viteBuild('../inline-query') + const exported = await import(base64Module(js)) + + expect(typeof exported.default).toBe('string') + expect(exported.default).toMatch('--file: "style.module.css?inline"') +}) + +test.runIf(isBuild)('getJSON', async () => { + type JSON = { + inputFile: string + exports: Record + outputFile: string + } + const jsons: JSON[] = [] + + await viteBuild('../multi-css-modules', { + css: { + modules: { + localsConvention: 'camelCaseOnly', + getJSON: (inputFile, exports, outputFile) => { + jsons.push({ + inputFile, + exports, + outputFile, + }) + }, + }, + }, + }) + + // This plugin treats each CSS Module as a JS module so it emits on each module + // rather than the final "bundle" which postcss-module emits on + expect(jsons).toHaveLength(4) + jsons.sort((a, b) => a.inputFile.localeCompare(b.inputFile)) + + const [style1, style2, utils1, utils2] = jsons + expect(style1).toMatchObject({ + inputFile: expect.stringMatching(/style1\.module\.css$/), + exports: { + className1: expect.stringMatching(/_className1_\w+ _util-class_\w+/), + className2: expect.stringMatching( + /_class-name2_\w+ _util-class_\w+ _util-class_\w+/, + ), + }, + outputFile: expect.stringMatching(/style1\.module\.css$/), + }) + + expect(style2).toMatchObject({ + inputFile: expect.stringMatching(/style2\.module\.css$/), + exports: { + className2: expect.stringMatching(/_class-name2_\w+ _util-class_\w+/), + }, + outputFile: expect.stringMatching(/style2\.module\.css$/), + }) + + expect(utils1).toMatchObject({ + inputFile: expect.stringMatching(/utils1\.css\?\.module\.css$/), + exports: { + unusedClass: expect.stringMatching(/_unused-class_\w+/), + utilClass: expect.stringMatching(/_util-class_\w+/), + }, + outputFile: expect.stringMatching(/utils1\.css\?\.module\.css$/), + }) + + expect(utils2).toMatchObject({ + inputFile: expect.stringMatching(/utils2\.css\?\.module\.css$/), + exports: { + utilClass: expect.stringMatching(/_util-class_\w+/), + }, + outputFile: expect.stringMatching(/utils2\.css\?\.module\.css$/), + }) +}) + +test.runIf(isBuild)('Empty CSS Module', async () => { + const { js, css } = await viteBuild('../empty-css-module', { + css: { + postcss: {}, + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + default: {}, + }) + expect(css).toBeUndefined() +}) + +describe('@value', () => { + test.runIf(isBuild)('build', async () => { + const { js, css } = await viteBuild('../css-modules-value', { + build: { + target: 'es2022', + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + default: { + 'class-name1': expect.stringMatching( + /^_class-name1_\w+ _util-class_\w+ _util-class_\w+$/, + ), + 'class-name2': expect.stringMatching(/^_class-name2_\w+$/), + }, + 'class-name1': expect.stringMatching( + /_class-name1_\w+ _util-class_\w+ _util-class_\w+/, + ), + 'class-name2': expect.stringMatching(/^_class-name2_\w+$/), + }) + + expect(css).toMatch('color: #fff') + expect(css).toMatch('border: #fff') + expect(css).toMatch('color: #000') + expect(css).toMatch('border: #000') + expect(css).toMatch('border: 1px solid black') + + // Ensure that PostCSS is applied to the composed files + expect(css).toMatch('--file: "style.module.css"') + expect(css).toMatch('--file: "utils1.css?.module.css"') + expect(css).toMatch('--file: "utils2.css?.module.css"') + }) + + test.runIf(isServe)('dev server', async () => { + const code = await getStyleMatchingId('css-modules-value') + + expect(code).toMatch('color: #fff') + expect(code).toMatch('border: #fff') + expect(code).toMatch('color: #000') + expect(code).toMatch('border: #000') + expect(code).toMatch('border: 1px solid black') + + // Ensure that PostCSS is applied to the composed files + expect(code).toMatch('--file: "style.module.css"') + expect(code).toMatch('--file: "utils1.css?.module.css"') + expect(code).toMatch('--file: "utils2.css?.module.css"') + }) +}) + +describe.runIf(isBuild)('error handling', () => { + test('missing class export', async () => { + await expect(() => + viteBuild('../missing-class-export', { + logLevel: 'silent', + }), + ).rejects.toThrow( + '[vite:css-modules] Cannot resolve "non-existent" from "./utils.css"', + ) + }) + + test('exporting a non-safe class name via esm doesnt throw', async () => { + await viteBuild('../module-namespace') + }) +}) diff --git a/playground/css-modules/__tests__/lightningcss/css-modules-lightningcss.spec.ts b/playground/css-modules/__tests__/lightningcss/css-modules-lightningcss.spec.ts new file mode 100644 index 000000000..4e4f53189 --- /dev/null +++ b/playground/css-modules/__tests__/lightningcss/css-modules-lightningcss.spec.ts @@ -0,0 +1,306 @@ +import path from 'node:path' +import { describe, expect, test } from 'vitest' +import { type InlineConfig, type Rollup, build } from 'vite' +import { Features } from 'lightningcss' +import { + base64Module, + getCssSourceMaps, + isBuild, + isServe, + page, + viteTestUrl, +} from '~utils' + +/* +Skipped most tests for now because: +1. For some reason, dev and build exports classes as `"l9uHSq_button": "l9uHSq_l9uHSq_button"` (double hash) +2. I don't undertstand how sourcemaps is being tested +3. The `vite-css-modules` implementation sidesteps the initial parsing to postcss, which we don't want when + using lightningcss. postcss should be completely not used. +*/ + +async function getStyleMatchingId(id: string) { + const styleTags = page.locator(`style[data-vite-dev-id*=${id}]`) + let code = '' + for (const style of await styleTags.all()) { + code += await style.textContent() + } + return code +} + +async function viteBuild(root: string, inlineConfig?: InlineConfig) { + const built = await build({ + root: path.resolve(__dirname, root), + configFile: false, + envFile: false, + logLevel: 'error', + ...inlineConfig, + build: { + // Prevents CSS minification from handling the de-duplication of classes + minify: false, + write: false, + lib: { + entry: 'index.js', + formats: ['es'], + }, + ...inlineConfig?.build, + }, + css: { + postcss: {}, + ...inlineConfig?.css, + }, + }) + + if (!Array.isArray(built)) { + throw new TypeError('Build result is not an array') + } + + const { output } = built[0]! + const css = output.find( + (file) => file.type === 'asset' && file.fileName.endsWith('.css'), + ) as Rollup.OutputAsset | undefined + + return { + js: output[0].code, + css: css?.source.toString(), + } +} + +test.runIf(isBuild).skip('Configured', async () => { + const { js, css } = await viteBuild('../../multi-css-modules', { + css: { + transformer: 'lightningcss', + }, + build: { + target: 'es2022', + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + style1: { + className1: expect.stringMatching( + /^[\w-]+_className1\s+[\w-]+_util-class$/, + ), + default: { + className1: expect.stringMatching( + /^[\w-]+_className1\s+[\w-]+_util-class$/, + ), + 'class-name2': expect.stringMatching( + /^[\w-]+_class-name2\s+[\w-]+_util-class\s+[\w-]+_util-class$/, + ), + }, + }, + style2: { + 'class-name2': expect.stringMatching( + /^[\w-]+_class-name2\s+[\w-]+_util-class$/, + ), + default: { + 'class-name2': expect.stringMatching( + /^[\w-]+_class-name2\s+[\w-]+_util-class$/, + ), + }, + }, + }) + + // Util is not duplicated + const utilClass = Array.from(css!.matchAll(/foo/g)) + expect(utilClass.length).toBe(1) +}) + +test.runIf(isBuild).skip('Empty CSS Module', async () => { + const { js, css } = await viteBuild('../../empty-css-module', { + css: { + transformer: 'lightningcss', + }, + }) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + default: {}, + }) + expect(css).toBe('\n') +}) + +test('reserved keywords', async () => { + const { js } = await viteBuild('../../reserved-keywords', { + build: { + target: 'es2022', + }, + css: { + transformer: 'lightningcss', + }, + }) + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + style: { + default: { + export: 'fk9XWG_export V_YH-W_with', + import: 'fk9XWG_import V_YH-W_if', + }, + export: 'fk9XWG_export V_YH-W_with', + import: 'fk9XWG_import V_YH-W_if', + }, + }) +}) + +describe.skip('Custom property dependencies', () => { + test.runIf(isBuild)('build', async () => { + const { js, css } = await viteBuild( + '../../lightningcss-custom-properties-from', + { + css: { + transformer: 'lightningcss', + lightningcss: { + cssModules: { + dashedIdents: true, + }, + }, + }, + }, + ) + + console.log(js) + + const exported = await import(base64Module(js)) + expect(exported).toMatchObject({ + style1: { + button: expect.stringMatching(/^[\w-]+_button$/), + }, + style2: { + input: expect.stringMatching(/^[\w-]+input$/), + }, + }) + + const variableNameMatches = Array.from(css!.matchAll(/(\S+): hotpink/g))! + expect(variableNameMatches.length).toBe(1) + + const variableName = variableNameMatches[0]![1] + expect(css).toMatch(`color: var(${variableName})`) + expect(css).toMatch(`background: var(${variableName})`) + }) + + test.runIf(isServe)('serve', async () => { + await page.goto(viteTestUrl + '/lightningcss.html') + const code = await getStyleMatchingId('lightningcss-custom-properties-from') + + const variableNameMatches = Array.from(code.matchAll(/(\S+): hotpink/g))! + expect(variableNameMatches.length).toBe(1) + + const variableName = variableNameMatches[0]![1] + expect(code).toMatch(`color: var(${variableName})`) + expect(code).toMatch(`background: var(${variableName})`) + }) +}) + +describe.skip('Other configs', () => { + test.runIf(isBuild)('build', async () => { + const { css } = await viteBuild('../../lightningcss-features', { + css: { + transformer: 'lightningcss', + lightningcss: { + include: Features.Nesting, + }, + }, + }) + + expect(css).toMatch(/\.[\w-]+_button\.[\w-]+_primary/) + }) + + test.runIf(isServe).skip('dev server', async () => { + await page.goto(viteTestUrl + '/lightningcss.html') + const code = await getStyleMatchingId('lightningcss-features') + + const cssSourcemaps = getCssSourceMaps(code) + expect(cssSourcemaps.length).toBe(0) + + expect(code).toMatch(/\.[\w-]+_button\.[\w-]+_primary/) + }) + + test.skip('devSourcemap', async () => { + const code = await getStyleMatchingId('lightningcss-custom-properties-from') + + const cssSourcemaps = getCssSourceMaps(code) + expect(cssSourcemaps.length).toBe(3) + // I'm skeptical these source maps are correct + // Seems lightningCSS is providing these source maps + expect(cssSourcemaps).toMatchObject([ + { + version: 3, + file: expect.stringMatching(/^style1\.module\.css$/), + mappings: 'AAAA', + names: [], + ignoreList: [], + sources: [expect.stringMatching(/^style1\.module\.css$/)], + sourcesContent: [ + '.button {\n\tbackground: var(--accent-color from "./vars.module.css");\n}', + ], + }, + { + version: 3, + file: expect.stringMatching(/^style2\.module\.css$/), + mappings: 'AAAA', + names: [], + ignoreList: [], + sources: [expect.stringMatching(/^style2\.module\.css$/)], + sourcesContent: [ + '.input {\n\tcolor: var(--accent-color from "./vars.module.css");\n}', + ], + }, + { + version: 3, + sourceRoot: null, + mappings: 'AAAA', + sources: [expect.stringMatching(/^vars\.module\.css$/)], + sourcesContent: [':root {\n\t--accent-color: hotpink;\n}'], + names: [], + }, + ]) + }) + + test.skip('devSourcemap with Vue.js', async () => { + const code = await getStyleMatchingId('vue') + const cssSourcemaps = getCssSourceMaps(code) + expect(cssSourcemaps.length).toBe(2) + expect(cssSourcemaps).toMatchObject([ + { + version: 3, + mappings: 'AAKA;;;;ACLA', + names: [], + sources: [expect.stringMatching(/\/comp\.vue$/), '\u0000'], + sourcesContent: [ + '\n' + + '\n' + + '', + null, + ], + file: expect.stringMatching(/\/comp\.vue$/), + }, + { + version: 3, + mappings: 'AAAA;;;;;AAKA;;;;ACLA', + names: [], + sources: [expect.stringMatching(/\/utils\.css$/), '\u0000'], + sourcesContent: [ + '.util-class {\n' + + "\t--name: 'foo';\n" + + '\tcolor: blue;\n' + + '}\n' + + '\n' + + '.unused-class {\n' + + '\tcolor: yellow;\n' + + '}', + null, + ], + file: expect.stringMatching(/\/utils\.css$/), + }, + ]) + }) +}) diff --git a/playground/css-modules/css-modules-value/index.js b/playground/css-modules/css-modules-value/index.js new file mode 100644 index 000000000..46f57acc7 --- /dev/null +++ b/playground/css-modules/css-modules-value/index.js @@ -0,0 +1,2 @@ +export * from './style.module.css' +export { default } from './style.module.css' diff --git a/playground/css-modules/css-modules-value/style.module.css b/playground/css-modules/css-modules-value/style.module.css new file mode 100644 index 000000000..ef6d96e45 --- /dev/null +++ b/playground/css-modules/css-modules-value/style.module.css @@ -0,0 +1,12 @@ +@value primary as p1, simple-border from './utils1.css'; +@value primary as p2 from './utils2.css'; + +.class-name1 { + color: p1; + border: simple-border; + composes: util-class from './utils1.css'; + composes: util-class from './utils2.css'; +} +.class-name2 { + color: p2; +} diff --git a/playground/css-modules/css-modules-value/utils1.css b/playground/css-modules/css-modules-value/utils1.css new file mode 100644 index 000000000..6d906955d --- /dev/null +++ b/playground/css-modules/css-modules-value/utils1.css @@ -0,0 +1,6 @@ +@value primary: #fff; +@value simple-border: 1px solid black; + +.util-class { + border: primary; +} diff --git a/playground/css-modules/css-modules-value/utils2.css b/playground/css-modules/css-modules-value/utils2.css new file mode 100644 index 000000000..97af2a554 --- /dev/null +++ b/playground/css-modules/css-modules-value/utils2.css @@ -0,0 +1,5 @@ +@value primary: #000; + +.util-class { + border: primary; +} diff --git a/playground/css-modules/empty-css-module/index.js b/playground/css-modules/empty-css-module/index.js new file mode 100644 index 000000000..46f57acc7 --- /dev/null +++ b/playground/css-modules/empty-css-module/index.js @@ -0,0 +1,2 @@ +export * from './style.module.css' +export { default } from './style.module.css' diff --git a/playground/css-modules/empty-css-module/style.module.css b/playground/css-modules/empty-css-module/style.module.css new file mode 100644 index 000000000..e69de29bb diff --git a/playground/css-modules/global-module/global.module.css b/playground/css-modules/global-module/global.module.css new file mode 100644 index 000000000..d2ee06d7a --- /dev/null +++ b/playground/css-modules/global-module/global.module.css @@ -0,0 +1,6 @@ +.page { + padding: 20px; +} +:local(.title) { + color: green; +} diff --git a/playground/css-modules/global-module/index.js b/playground/css-modules/global-module/index.js new file mode 100644 index 000000000..dcc5d2ad3 --- /dev/null +++ b/playground/css-modules/global-module/index.js @@ -0,0 +1,2 @@ +export * from './global.module.css' +export { default } from './global.module.css' diff --git a/playground/css-modules/index.html b/playground/css-modules/index.html new file mode 100644 index 000000000..4d1ebf249 --- /dev/null +++ b/playground/css-modules/index.html @@ -0,0 +1,67 @@ +

CSS Modules

+ +

css-modules-value

+

+
+

empty-css-module

+

+
+

global-module

+

+
+

inline-query

+

+
+

module-namespace

+

+
+

multi-css-modules

+

+
+

reserved-keywords

+

+
+

scss-modules

+

+
+

scss-modules-mixed

+

+
+

vue

+

+
+
diff --git a/playground/css-modules/inline-query/index.js b/playground/css-modules/inline-query/index.js
new file mode 100644
index 000000000..e2bafddfb
--- /dev/null
+++ b/playground/css-modules/inline-query/index.js
@@ -0,0 +1,2 @@
+export * from './style.module.css?inline'
+export { default } from './style.module.css?inline'
diff --git a/playground/css-modules/inline-query/style.module.css b/playground/css-modules/inline-query/style.module.css
new file mode 100644
index 000000000..3665babf2
--- /dev/null
+++ b/playground/css-modules/inline-query/style.module.css
@@ -0,0 +1,4 @@
+.class-name1 {
+  composes: util-class from './utils.css';
+  color: red;
+}
diff --git a/playground/css-modules/inline-query/utils.css b/playground/css-modules/inline-query/utils.css
new file mode 100644
index 000000000..088f77c8c
--- /dev/null
+++ b/playground/css-modules/inline-query/utils.css
@@ -0,0 +1,8 @@
+.util-class {
+  --name: 'foo';
+  color: blue;
+}
+
+.unused-class {
+  color: yellow;
+}
diff --git a/playground/css-modules/lightningcss-custom-properties-from/index.js b/playground/css-modules/lightningcss-custom-properties-from/index.js
new file mode 100644
index 000000000..b766717ca
--- /dev/null
+++ b/playground/css-modules/lightningcss-custom-properties-from/index.js
@@ -0,0 +1,2 @@
+export { default as style1 } from './style1.module.css'
+export { default as style2 } from './style2.module.css'
diff --git a/playground/css-modules/lightningcss-custom-properties-from/style1.module.css b/playground/css-modules/lightningcss-custom-properties-from/style1.module.css
new file mode 100644
index 000000000..d6d1d36e4
--- /dev/null
+++ b/playground/css-modules/lightningcss-custom-properties-from/style1.module.css
@@ -0,0 +1,3 @@
+.button {
+  background: var(--accent-color from './vars.module.css');
+}
diff --git a/playground/css-modules/lightningcss-custom-properties-from/style2.module.css b/playground/css-modules/lightningcss-custom-properties-from/style2.module.css
new file mode 100644
index 000000000..3adb970ad
--- /dev/null
+++ b/playground/css-modules/lightningcss-custom-properties-from/style2.module.css
@@ -0,0 +1,3 @@
+.input {
+  color: var(--accent-color from './vars.module.css');
+}
diff --git a/playground/css-modules/lightningcss-custom-properties-from/vars.module.css b/playground/css-modules/lightningcss-custom-properties-from/vars.module.css
new file mode 100644
index 000000000..0168423c6
--- /dev/null
+++ b/playground/css-modules/lightningcss-custom-properties-from/vars.module.css
@@ -0,0 +1,3 @@
+:root {
+  --accent-color: hotpink;
+}
diff --git a/playground/css-modules/lightningcss-features/index.js b/playground/css-modules/lightningcss-features/index.js
new file mode 100644
index 000000000..46f57acc7
--- /dev/null
+++ b/playground/css-modules/lightningcss-features/index.js
@@ -0,0 +1,2 @@
+export * from './style.module.css'
+export { default } from './style.module.css'
diff --git a/playground/css-modules/lightningcss-features/style.module.css b/playground/css-modules/lightningcss-features/style.module.css
new file mode 100644
index 000000000..c986bab2f
--- /dev/null
+++ b/playground/css-modules/lightningcss-features/style.module.css
@@ -0,0 +1,5 @@
+.button {
+  &.primary {
+    color: red;
+  }
+}
diff --git a/playground/css-modules/lightningcss.html b/playground/css-modules/lightningcss.html
new file mode 100644
index 000000000..6a9113658
--- /dev/null
+++ b/playground/css-modules/lightningcss.html
@@ -0,0 +1,22 @@
+

CSS Modules lightningcss

+ +

lightningcss-custom-properties-from

+

+
+

lightningcss-features

+

+
+
diff --git a/playground/css-modules/missing-class-export/index.js b/playground/css-modules/missing-class-export/index.js
new file mode 100644
index 000000000..46f57acc7
--- /dev/null
+++ b/playground/css-modules/missing-class-export/index.js
@@ -0,0 +1,2 @@
+export * from './style.module.css'
+export { default } from './style.module.css'
diff --git a/playground/css-modules/missing-class-export/style.module.css b/playground/css-modules/missing-class-export/style.module.css
new file mode 100644
index 000000000..bc5571d0e
--- /dev/null
+++ b/playground/css-modules/missing-class-export/style.module.css
@@ -0,0 +1,4 @@
+.className1 {
+  composes: non-existent from './utils.css';
+  color: red;
+}
diff --git a/playground/css-modules/missing-class-export/utils.css b/playground/css-modules/missing-class-export/utils.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/playground/css-modules/module-namespace/index.js b/playground/css-modules/module-namespace/index.js
new file mode 100644
index 000000000..fc6ecf3d6
--- /dev/null
+++ b/playground/css-modules/module-namespace/index.js
@@ -0,0 +1 @@
+import('./style.module.css')
diff --git a/playground/css-modules/module-namespace/style.module.css b/playground/css-modules/module-namespace/style.module.css
new file mode 100644
index 000000000..a7355d84b
--- /dev/null
+++ b/playground/css-modules/module-namespace/style.module.css
@@ -0,0 +1,3 @@
+.class-name {
+  color: red;
+}
diff --git a/playground/css-modules/multi-css-modules/index.js b/playground/css-modules/multi-css-modules/index.js
new file mode 100644
index 000000000..3a875519d
--- /dev/null
+++ b/playground/css-modules/multi-css-modules/index.js
@@ -0,0 +1,2 @@
+export * as style1 from './style1.module.css'
+export * as style2 from './style2.module.css'
diff --git a/playground/css-modules/multi-css-modules/style1.module.css b/playground/css-modules/multi-css-modules/style1.module.css
new file mode 100644
index 000000000..286935e3e
--- /dev/null
+++ b/playground/css-modules/multi-css-modules/style1.module.css
@@ -0,0 +1,9 @@
+.className1 {
+  composes: util-class from './utils1.css';
+  color: red;
+}
+
+.class-name2 {
+  composes: util-class from './utils1.css';
+  composes: util-class from './utils2.css';
+}
diff --git a/playground/css-modules/multi-css-modules/style2.module.css b/playground/css-modules/multi-css-modules/style2.module.css
new file mode 100644
index 000000000..f0e72fc36
--- /dev/null
+++ b/playground/css-modules/multi-css-modules/style2.module.css
@@ -0,0 +1,4 @@
+.class-name2 {
+  composes: util-class from './utils1.css';
+  color: red;
+}
diff --git a/playground/css-modules/multi-css-modules/utils1.css b/playground/css-modules/multi-css-modules/utils1.css
new file mode 100644
index 000000000..088f77c8c
--- /dev/null
+++ b/playground/css-modules/multi-css-modules/utils1.css
@@ -0,0 +1,8 @@
+.util-class {
+  --name: 'foo';
+  color: blue;
+}
+
+.unused-class {
+  color: yellow;
+}
diff --git a/playground/css-modules/multi-css-modules/utils2.css b/playground/css-modules/multi-css-modules/utils2.css
new file mode 100644
index 000000000..950866513
--- /dev/null
+++ b/playground/css-modules/multi-css-modules/utils2.css
@@ -0,0 +1,4 @@
+.util-class {
+  --name: 'bar';
+  color: green;
+}
diff --git a/playground/css-modules/package.json b/playground/css-modules/package.json
new file mode 100644
index 000000000..0c1bbf4ec
--- /dev/null
+++ b/playground/css-modules/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "@vitejs/test-css-modules",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview",
+    "dev:lightningcss": "vite --config=vite.config-lightningcss.js",
+    "build:lightningcss": "vite --config=vite.config-lightningcss.js build",
+    "preview:lightningcss": "vite --config=vite.config-lightningcss.js preview"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.0.4",
+    "lightningcss": "^1.24.1",
+    "postcss": "^8.4.36",
+    "sass": "^1.72.0",
+    "vue": "^3.4.21"
+  }
+}
diff --git a/playground/css-modules/postcss.config.js b/playground/css-modules/postcss.config.js
new file mode 100644
index 000000000..dd4b0e961
--- /dev/null
+++ b/playground/css-modules/postcss.config.js
@@ -0,0 +1,19 @@
+import path from 'node:path'
+import postcss from 'postcss'
+
+export default {
+  plugins: [
+    /**
+     * PostCSS plugin that adds a "--file" CSS variable to indicate PostCSS
+     * has been successfully applied
+     */
+    (root) => {
+      const newRule = postcss.rule({ selector: ':root' })
+      newRule.append({
+        prop: '--file',
+        value: JSON.stringify(path.basename(root.source.input.file)),
+      })
+      root.append(newRule)
+    },
+  ],
+}
diff --git a/playground/css-modules/reserved-keywords/index.js b/playground/css-modules/reserved-keywords/index.js
new file mode 100644
index 000000000..b45ea7fdc
--- /dev/null
+++ b/playground/css-modules/reserved-keywords/index.js
@@ -0,0 +1 @@
+export * as style from './style.module.css'
diff --git a/playground/css-modules/reserved-keywords/style.module.css b/playground/css-modules/reserved-keywords/style.module.css
new file mode 100644
index 000000000..f52163ddd
--- /dev/null
+++ b/playground/css-modules/reserved-keywords/style.module.css
@@ -0,0 +1,8 @@
+.import {
+  composes: if from './utils.css';
+  color: red;
+}
+
+.export {
+  composes: with from './utils.css';
+}
diff --git a/playground/css-modules/reserved-keywords/utils.css b/playground/css-modules/reserved-keywords/utils.css
new file mode 100644
index 000000000..77b35d896
--- /dev/null
+++ b/playground/css-modules/reserved-keywords/utils.css
@@ -0,0 +1,8 @@
+.if {
+  --name: 'foo';
+  color: blue;
+}
+
+.with {
+  color: yellow;
+}
diff --git a/playground/css-modules/scss-modules-mixed/css.module.css b/playground/css-modules/scss-modules-mixed/css.module.css
new file mode 100644
index 000000000..a7b2031a0
--- /dev/null
+++ b/playground/css-modules/scss-modules-mixed/css.module.css
@@ -0,0 +1,3 @@
+.text-primary {
+  composes: text-primary from './scss.module.scss';
+}
diff --git a/playground/css-modules/scss-modules-mixed/index.js b/playground/css-modules/scss-modules-mixed/index.js
new file mode 100644
index 000000000..db2451f9d
--- /dev/null
+++ b/playground/css-modules/scss-modules-mixed/index.js
@@ -0,0 +1,2 @@
+export * from './css.module.css'
+export { default } from './css.module.css'
diff --git a/playground/css-modules/scss-modules-mixed/scss.module.scss b/playground/css-modules/scss-modules-mixed/scss.module.scss
new file mode 100644
index 000000000..c9c6dbda6
--- /dev/null
+++ b/playground/css-modules/scss-modules-mixed/scss.module.scss
@@ -0,0 +1,7 @@
+$primary: #cc0000;
+
+// comment
+
+.text-primary {
+  color: $primary;
+}
diff --git a/playground/css-modules/scss-modules/index.js b/playground/css-modules/scss-modules/index.js
new file mode 100644
index 000000000..d68529432
--- /dev/null
+++ b/playground/css-modules/scss-modules/index.js
@@ -0,0 +1,2 @@
+export * from './style.module.scss'
+export { default } from './style.module.scss'
diff --git a/playground/css-modules/scss-modules/style.module.scss b/playground/css-modules/scss-modules/style.module.scss
new file mode 100644
index 000000000..c9c6dbda6
--- /dev/null
+++ b/playground/css-modules/scss-modules/style.module.scss
@@ -0,0 +1,7 @@
+$primary: #cc0000;
+
+// comment
+
+.text-primary {
+  color: $primary;
+}
diff --git a/playground/css-modules/vite.config-lightningcss.js b/playground/css-modules/vite.config-lightningcss.js
new file mode 100644
index 000000000..07c727eed
--- /dev/null
+++ b/playground/css-modules/vite.config-lightningcss.js
@@ -0,0 +1,27 @@
+import { resolve } from 'node:path'
+import { Features } from 'lightningcss'
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+  plugins: [vue()],
+  css: {
+    devSourcemap: true,
+    transformer: 'lightningcss',
+    lightningcss: {
+      include: Features.Nesting,
+      cssModules: {
+        dashedIdents: true,
+      },
+    },
+  },
+  build: {
+    // Prevents CSS minification from handling the de-duplication of classes
+    minify: false,
+    rollupOptions: {
+      input: {
+        lightningcss: resolve(__dirname, 'lightningcss.html'),
+      },
+    },
+  },
+})
diff --git a/playground/css-modules/vite.config.js b/playground/css-modules/vite.config.js
new file mode 100644
index 000000000..d2e324a6c
--- /dev/null
+++ b/playground/css-modules/vite.config.js
@@ -0,0 +1,16 @@
+import { resolve } from 'node:path'
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+  plugins: [vue()],
+  build: {
+    // Prevents CSS minification from handling the de-duplication of classes
+    minify: false,
+    rollupOptions: {
+      input: {
+        main: resolve(__dirname, 'index.html'),
+      },
+    },
+  },
+})
diff --git a/playground/css-modules/vue/Comp.vue b/playground/css-modules/vue/Comp.vue
new file mode 100644
index 000000000..993a9232e
--- /dev/null
+++ b/playground/css-modules/vue/Comp.vue
@@ -0,0 +1,10 @@
+
+
+
diff --git a/playground/css-modules/vue/index.js b/playground/css-modules/vue/index.js
new file mode 100644
index 000000000..d2c938294
--- /dev/null
+++ b/playground/css-modules/vue/index.js
@@ -0,0 +1 @@
+export { default as Comp } from './Comp.vue'
diff --git a/playground/css-modules/vue/utils.css b/playground/css-modules/vue/utils.css
new file mode 100644
index 000000000..088f77c8c
--- /dev/null
+++ b/playground/css-modules/vue/utils.css
@@ -0,0 +1,8 @@
+.util-class {
+  --name: 'foo';
+  color: blue;
+}
+
+.unused-class {
+  color: yellow;
+}
diff --git a/playground/test-utils.ts b/playground/test-utils.ts
index 2916c350d..be0eca96a 100644
--- a/playground/test-utils.ts
+++ b/playground/test-utils.ts
@@ -9,7 +9,7 @@ import type {
   ElementHandle,
   Locator,
 } from 'playwright-chromium'
-import type { DepOptimizationMetadata, Manifest } from 'vite'
+import type { DepOptimizationMetadata, Manifest, Rollup } from 'vite'
 import { normalizePath } from 'vite'
 import { fromComment } from 'convert-source-map'
 import type { Assertion } from 'vitest'
@@ -408,3 +408,25 @@ export function promiseWithResolvers(): PromiseWithResolvers {
   })
   return { promise, resolve, reject }
 }
+
+export const base64Module = (code: string) =>
+  `data:text/javascript;base64,${Buffer.from(code).toString('base64')}`
+
+export const getCssSourceMaps = (code: string) => {
+  const cssSourcemaps = Array.from(
+    code.matchAll(
+      /\/*# sourceMappingURL=data:application\/json;base64,(.+?) \*\//g,
+    ),
+  )
+
+  const maps = cssSourcemaps.map(
+    ([, base64]) =>
+      JSON.parse(
+        Buffer.from(base64!, 'base64').toString('utf8'),
+      ) as Rollup.SourceMap,
+  )
+
+  maps.sort((a, b) => a.sources[0]!.localeCompare(b.sources[0]!))
+
+  return maps
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6b654967f..6edaa404e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -173,7 +173,7 @@ importers:
         version: 4.17.21
       vitepress:
         specifier: 1.0.0-rc.45
-        version: 1.0.0-rc.45(typescript@5.2.2)
+        version: 1.0.0-rc.45(@types/node@20.11.28)(typescript@5.2.2)
       vue:
         specifier: ^3.4.21
         version: 3.4.21(typescript@5.2.2)
@@ -613,6 +613,24 @@ importers:
         specifier: ^1.24.1
         version: 1.24.1
 
+  playground/css-modules:
+    devDependencies:
+      '@vitejs/plugin-vue':
+        specifier: ^5.0.4
+        version: 5.0.4(vite@packages+vite)(vue@3.4.21)
+      lightningcss:
+        specifier: ^1.24.1
+        version: 1.24.1
+      postcss:
+        specifier: ^8.4.36
+        version: 8.4.36
+      sass:
+        specifier: ^1.72.0
+        version: 1.72.0
+      vue:
+        specifier: ^3.4.21
+        version: 3.4.21(typescript@5.2.2)
+
   playground/css-sourcemap:
     devDependencies:
       less:
@@ -3941,21 +3959,6 @@ packages:
       typescript: 5.2.2
     dev: true
 
-  /@rollup/pluginutils@5.0.4(rollup@3.29.4):
-    resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==}
-    engines: {node: '>=14.0.0'}
-    peerDependencies:
-      rollup: ^1.20.0||^2.0.0||^3.0.0
-    peerDependenciesMeta:
-      rollup:
-        optional: true
-    dependencies:
-      '@types/estree': 1.0.5
-      estree-walker: 2.0.2
-      picomatch: 2.3.1
-      rollup: 3.29.4
-    dev: true
-
   /@rollup/pluginutils@5.1.0(rollup@3.29.4):
     resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
     engines: {node: '>=14.0.0'}
@@ -4318,12 +4321,6 @@ packages:
     resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
     dev: true
 
-  /@types/node@20.10.0:
-    resolution: {integrity: sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==}
-    dependencies:
-      undici-types: 5.26.5
-    dev: true
-
   /@types/node@20.11.28:
     resolution: {integrity: sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==}
     dependencies:
@@ -4344,7 +4341,7 @@ packages:
   /@types/prompts@2.4.9:
     resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==}
     dependencies:
-      '@types/node': 20.10.0
+      '@types/node': 20.11.28
       kleur: 3.0.3
     dev: true
 
@@ -4549,6 +4546,17 @@ packages:
     resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
     dev: true
 
+  /@vitejs/plugin-vue@5.0.4(vite@5.2.2)(vue@3.4.21):
+    resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    peerDependencies:
+      vite: '*'
+      vue: ^3.2.25
+    dependencies:
+      vite: 5.2.2(@types/node@20.11.28)
+      vue: 3.4.21(typescript@5.2.2)
+    dev: true
+
   /@vitejs/plugin-vue@5.0.4(vite@packages+vite)(vue@3.4.21):
     resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==}
     engines: {node: ^18.0.0 || >=20.0.0}
@@ -7500,13 +7508,6 @@ packages:
       '@jridgewell/sourcemap-codec': 1.4.15
     dev: true
 
-  /magic-string@0.30.4:
-    resolution: {integrity: sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==}
-    engines: {node: '>=12'}
-    dependencies:
-      '@jridgewell/sourcemap-codec': 1.4.15
-    dev: true
-
   /magic-string@0.30.8:
     resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
     engines: {node: '>=12'}
@@ -8581,7 +8582,7 @@ packages:
     dependencies:
       icss-utils: 5.1.0(postcss@8.4.36)
       postcss: 8.4.36
-      postcss-selector-parser: 6.0.11
+      postcss-selector-parser: 6.0.16
       postcss-value-parser: 4.2.0
     dev: true
 
@@ -8595,7 +8596,7 @@ packages:
         optional: true
     dependencies:
       postcss: 8.4.36
-      postcss-selector-parser: 6.0.11
+      postcss-selector-parser: 6.0.16
     dev: true
 
   /postcss-modules-values@4.0.0(postcss@8.4.36):
@@ -8621,10 +8622,10 @@ packages:
         optional: true
     dependencies:
       postcss: 8.4.36
-      postcss-selector-parser: 6.0.11
+      postcss-selector-parser: 6.0.16
 
-  /postcss-selector-parser@6.0.11:
-    resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==}
+  /postcss-selector-parser@6.0.16:
+    resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==}
     engines: {node: '>=4'}
     dependencies:
       cssesc: 3.0.0
@@ -9589,7 +9590,7 @@ packages:
       postcss-js: 4.0.1(postcss@8.4.36)
       postcss-load-config: 4.0.2(postcss@8.4.36)(ts-node@10.9.2)
       postcss-nested: 6.0.1(postcss@8.4.36)
-      postcss-selector-parser: 6.0.11
+      postcss-selector-parser: 6.0.16
       resolve: 1.22.4
       sucrase: 3.32.0
     transitivePeerDependencies:
@@ -9874,7 +9875,7 @@ packages:
       '@rollup/plugin-json': 6.0.0(rollup@3.29.4)
       '@rollup/plugin-node-resolve': 15.2.1(rollup@3.29.4)
       '@rollup/plugin-replace': 5.0.2(rollup@3.29.4)
-      '@rollup/pluginutils': 5.0.4(rollup@3.29.4)
+      '@rollup/pluginutils': 5.1.0(rollup@3.29.4)
       chalk: 5.3.0
       citty: 0.1.4
       consola: 3.2.3
@@ -9883,7 +9884,7 @@ packages:
       globby: 13.2.2
       hookable: 5.5.3
       jiti: 1.20.0
-      magic-string: 0.30.4
+      magic-string: 0.30.8
       mkdist: 1.3.0(typescript@5.2.2)
       mlly: 1.4.2
       pathe: 1.1.2
@@ -10075,7 +10076,7 @@ packages:
       - rollup
     dev: true
 
-  /vite-node@1.4.0:
+  /vite-node@1.4.0(@types/node@20.11.28):
     resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
@@ -10084,12 +10085,55 @@ packages:
       debug: 4.3.4
       pathe: 1.1.2
       picocolors: 1.0.0
-      vite: link:packages/vite
+      vite: 5.2.2(@types/node@20.11.28)
     transitivePeerDependencies:
+      - '@types/node'
+      - less
+      - lightningcss
+      - sass
+      - stylus
+      - sugarss
       - supports-color
+      - terser
     dev: true
 
-  /vitepress@1.0.0-rc.45(typescript@5.2.2):
+  /vite@5.2.2(@types/node@20.11.28):
+    resolution: {integrity: sha512-FWZbz0oSdLq5snUI0b6sULbz58iXFXdvkZfZWR/F0ZJuKTSPO7v72QPXt6KqYeMFb0yytNp6kZosxJ96Nr/wDQ==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^18.0.0 || >=20.0.0
+      less: '*'
+      lightningcss: ^1.21.0
+      sass: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.4.0
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+    dependencies:
+      '@types/node': 20.11.28
+      esbuild: 0.20.1
+      postcss: 8.4.36
+      rollup: 4.13.0
+    optionalDependencies:
+      fsevents: 2.3.3
+    dev: true
+
+  /vitepress@1.0.0-rc.45(@types/node@20.11.28)(typescript@5.2.2):
     resolution: {integrity: sha512-/OiYsu5UKpQKA2c0BAZkfyywjfauDjvXyv6Mo4Ra57m5n4Bxg1HgUGoth1CLH2vwUbR/BHvDA9zOM0RDvgeSVQ==}
     hasBin: true
     peerDependencies:
@@ -10106,7 +10150,7 @@ packages:
       '@shikijs/core': 1.1.5
       '@shikijs/transformers': 1.1.5
       '@types/markdown-it': 13.0.7
-      '@vitejs/plugin-vue': 5.0.4(vite@packages+vite)(vue@3.4.21)
+      '@vitejs/plugin-vue': 5.0.4(vite@5.2.2)(vue@3.4.21)
       '@vue/devtools-api': 7.0.14
       '@vueuse/core': 10.7.2(vue@3.4.21)
       '@vueuse/integrations': 10.7.2(focus-trap@7.5.4)(vue@3.4.21)
@@ -10114,10 +10158,11 @@ packages:
       mark.js: 8.11.1
       minisearch: 6.3.0
       shiki: 1.1.5
-      vite: link:packages/vite
+      vite: 5.2.2(@types/node@20.11.28)
       vue: 3.4.21(typescript@5.2.2)
     transitivePeerDependencies:
       - '@algolia/client-search'
+      - '@types/node'
       - '@types/react'
       - '@vue/composition-api'
       - async-validator
@@ -10127,12 +10172,18 @@ packages:
       - fuse.js
       - idb-keyval
       - jwt-decode
+      - less
+      - lightningcss
       - nprogress
       - qrcode
       - react
       - react-dom
+      - sass
       - search-insights
       - sortablejs
+      - stylus
+      - sugarss
+      - terser
       - typescript
       - universal-cookie
     dev: true
@@ -10180,12 +10231,18 @@ packages:
       strip-literal: 2.0.0
       tinybench: 2.5.1
       tinypool: 0.8.2
-      vite: link:packages/vite
-      vite-node: 1.4.0
+      vite: 5.2.2(@types/node@20.11.28)
+      vite-node: 1.4.0(@types/node@20.11.28)
       why-is-node-running: 2.2.2
     transitivePeerDependencies:
       - acorn
+      - less
+      - lightningcss
+      - sass
+      - stylus
+      - sugarss
       - supports-color
+      - terser
     dev: true
 
   /void-elements@3.1.0: