chore: port tests over

This port isn't the best. Vite builds are ran multiple times with different modules configuration. The ideal abstraction is to have unit tests instead and Vite should "trust" that it works, but this will do for now.

I also skipped lightningcss tests for now. Check css-modules-lightningcss.spec.ts for notes.
This commit is contained in:
bluwy 2024-03-21 16:35:55 +08:00
parent 0b13c294b6
commit ece9f720fd
48 changed files with 1221 additions and 46 deletions

View File

@ -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<string, string>
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')
})
})

View File

@ -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<no source>'],
sourcesContent: [
'<template>\n' +
'\t<p :class="$style[\'css-module\']">&lt;css&gt; module</p>\n' +
'</template>\n' +
'\n' +
'<style module>\n' +
'.css-module {\n' +
"\tcomposes: util-class from './utils.css';\n" +
'\tcolor: red;\n' +
'}\n' +
'</style>',
null,
],
file: expect.stringMatching(/\/comp\.vue$/),
},
{
version: 3,
mappings: 'AAAA;;;;;AAKA;;;;ACLA',
names: [],
sources: [expect.stringMatching(/\/utils\.css$/), '\u0000<no source>'],
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$/),
},
])
})
})

View File

@ -0,0 +1,2 @@
export * from './style.module.css'
export { default } from './style.module.css'

View File

@ -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;
}

View File

@ -0,0 +1,6 @@
@value primary: #fff;
@value simple-border: 1px solid black;
.util-class {
border: primary;
}

View File

@ -0,0 +1,5 @@
@value primary: #000;
.util-class {
border: primary;
}

View File

@ -0,0 +1,2 @@
export * from './style.module.css'
export { default } from './style.module.css'

View File

@ -0,0 +1,6 @@
.page {
padding: 20px;
}
:local(.title) {
color: green;
}

View File

@ -0,0 +1,2 @@
export * from './global.module.css'
export { default } from './global.module.css'

View File

@ -0,0 +1,67 @@
<h1>CSS Modules</h1>
<p>css-modules-value</p>
<pre class="css-modules-value"></pre>
<p>empty-css-module</p>
<pre class="empty-css-module"></pre>
<p>global-module</p>
<pre class="global-module"></pre>
<p>inline-query</p>
<pre class="inline-query"></pre>
<p>module-namespace</p>
<pre class="module-namespace"></pre>
<p>multi-css-modules</p>
<pre class="multi-css-modules"></pre>
<p>reserved-keywords</p>
<pre class="reserved-keywords"></pre>
<p>scss-modules</p>
<pre class="scss-modules"></pre>
<p>scss-modules-mixed</p>
<pre class="scss-modules-mixed"></pre>
<p>vue</p>
<pre class="vue"></pre>
<script type="module">
import * as cssModulesValue from './css-modules-value'
text('.css-modules-value', JSON.stringify(cssModulesValue, null, 2))
import * as emptyCssModule from './empty-css-module'
text('.empty-css-module', JSON.stringify(emptyCssModule, null, 2))
import * as globalModule from './global-module'
text('.global-module', JSON.stringify(globalModule, null, 2))
import * as inlineQuery from './inline-query'
text('.inline-query', JSON.stringify(inlineQuery, null, 2))
import * as moduleNamespace from './module-namespace'
text('.module-namespace', JSON.stringify(moduleNamespace, null, 2))
import * as multiCssModules from './multi-css-modules'
text('.multi-css-modules', JSON.stringify(multiCssModules, null, 2))
import * as reservedKeywords from './reserved-keywords'
text('.reserved-keywords', JSON.stringify(reservedKeywords, null, 2))
import * as scssModules from './scss-modules'
text('.scss-modules', JSON.stringify(scssModules, null, 2))
import * as scssModulesMixed from './scss-modules-mixed'
text('.scss-modules-mixed', JSON.stringify(scssModulesMixed, null, 2))
import * as vue from './vue'
text('.vue', JSON.stringify(vue, null, 2))
function text(el, text) {
document.querySelector(el).textContent = text
}
</script>

View File

@ -0,0 +1,2 @@
export * from './style.module.css?inline'
export { default } from './style.module.css?inline'

View File

@ -0,0 +1,4 @@
.class-name1 {
composes: util-class from './utils.css';
color: red;
}

View File

@ -0,0 +1,8 @@
.util-class {
--name: 'foo';
color: blue;
}
.unused-class {
color: yellow;
}

View File

@ -0,0 +1,2 @@
export { default as style1 } from './style1.module.css'
export { default as style2 } from './style2.module.css'

View File

@ -0,0 +1,3 @@
.button {
background: var(--accent-color from './vars.module.css');
}

View File

@ -0,0 +1,3 @@
.input {
color: var(--accent-color from './vars.module.css');
}

View File

@ -0,0 +1,3 @@
:root {
--accent-color: hotpink;
}

View File

@ -0,0 +1,2 @@
export * from './style.module.css'
export { default } from './style.module.css'

View File

@ -0,0 +1,5 @@
.button {
&.primary {
color: red;
}
}

View File

@ -0,0 +1,22 @@
<h1>CSS Modules lightningcss</h1>
<p>lightningcss-custom-properties-from</p>
<pre class="lightningcss-custom-properties-from"></pre>
<p>lightningcss-features</p>
<pre class="lightningcss-features"></pre>
<script type="module">
import * as lightningcssCustomPropertiesFrom from './lightningcss-custom-properties-from'
text(
'.lightningcss-custom-properties-from',
JSON.stringify(lightningcssCustomPropertiesFrom, null, 2),
)
import * as lightningcssFeatures from './lightningcss-features'
text('.lightningcss-features', JSON.stringify(lightningcssFeatures, null, 2))
function text(el, text) {
document.querySelector(el).textContent = text
}
</script>

View File

@ -0,0 +1,2 @@
export * from './style.module.css'
export { default } from './style.module.css'

View File

@ -0,0 +1,4 @@
.className1 {
composes: non-existent from './utils.css';
color: red;
}

View File

@ -0,0 +1 @@
import('./style.module.css')

View File

@ -0,0 +1,3 @@
.class-name {
color: red;
}

View File

@ -0,0 +1,2 @@
export * as style1 from './style1.module.css'
export * as style2 from './style2.module.css'

View File

@ -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';
}

View File

@ -0,0 +1,4 @@
.class-name2 {
composes: util-class from './utils1.css';
color: red;
}

View File

@ -0,0 +1,8 @@
.util-class {
--name: 'foo';
color: blue;
}
.unused-class {
color: yellow;
}

View File

@ -0,0 +1,4 @@
.util-class {
--name: 'bar';
color: green;
}

View File

@ -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"
}
}

View File

@ -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)
},
],
}

View File

@ -0,0 +1 @@
export * as style from './style.module.css'

View File

@ -0,0 +1,8 @@
.import {
composes: if from './utils.css';
color: red;
}
.export {
composes: with from './utils.css';
}

View File

@ -0,0 +1,8 @@
.if {
--name: 'foo';
color: blue;
}
.with {
color: yellow;
}

View File

@ -0,0 +1,3 @@
.text-primary {
composes: text-primary from './scss.module.scss';
}

View File

@ -0,0 +1,2 @@
export * from './css.module.css'
export { default } from './css.module.css'

View File

@ -0,0 +1,7 @@
$primary: #cc0000;
// comment
.text-primary {
color: $primary;
}

View File

@ -0,0 +1,2 @@
export * from './style.module.scss'
export { default } from './style.module.scss'

View File

@ -0,0 +1,7 @@
$primary: #cc0000;
// comment
.text-primary {
color: $primary;
}

View File

@ -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'),
},
},
},
})

View File

@ -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'),
},
},
},
})

View File

@ -0,0 +1,10 @@
<template>
<p :class="$style['css-module']">&lt;css&gt; module</p>
</template>
<style module>
.css-module {
composes: util-class from './utils.css';
color: red;
}
</style>

View File

@ -0,0 +1 @@
export { default as Comp } from './Comp.vue'

View File

@ -0,0 +1,8 @@
.util-class {
--name: 'foo';
color: blue;
}
.unused-class {
color: yellow;
}

View File

@ -9,7 +9,7 @@ import type {
ElementHandle, ElementHandle,
Locator, Locator,
} from 'playwright-chromium' } from 'playwright-chromium'
import type { DepOptimizationMetadata, Manifest } from 'vite' import type { DepOptimizationMetadata, Manifest, Rollup } from 'vite'
import { normalizePath } from 'vite' import { normalizePath } from 'vite'
import { fromComment } from 'convert-source-map' import { fromComment } from 'convert-source-map'
import type { Assertion } from 'vitest' import type { Assertion } from 'vitest'
@ -408,3 +408,25 @@ export function promiseWithResolvers<T>(): PromiseWithResolvers<T> {
}) })
return { promise, resolve, reject } 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
}

View File

@ -173,7 +173,7 @@ importers:
version: 4.17.21 version: 4.17.21
vitepress: vitepress:
specifier: 1.0.0-rc.45 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: vue:
specifier: ^3.4.21 specifier: ^3.4.21
version: 3.4.21(typescript@5.2.2) version: 3.4.21(typescript@5.2.2)
@ -613,6 +613,24 @@ importers:
specifier: ^1.24.1 specifier: ^1.24.1
version: 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: playground/css-sourcemap:
devDependencies: devDependencies:
less: less:
@ -3941,21 +3959,6 @@ packages:
typescript: 5.2.2 typescript: 5.2.2
dev: true 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): /@rollup/pluginutils@5.1.0(rollup@3.29.4):
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -4318,12 +4321,6 @@ packages:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: true 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: /@types/node@20.11.28:
resolution: {integrity: sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==} resolution: {integrity: sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==}
dependencies: dependencies:
@ -4344,7 +4341,7 @@ packages:
/@types/prompts@2.4.9: /@types/prompts@2.4.9:
resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==}
dependencies: dependencies:
'@types/node': 20.10.0 '@types/node': 20.11.28
kleur: 3.0.3 kleur: 3.0.3
dev: true dev: true
@ -4549,6 +4546,17 @@ packages:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true 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): /@vitejs/plugin-vue@5.0.4(vite@packages+vite)(vue@3.4.21):
resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==} resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
@ -7500,13 +7508,6 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
dev: true 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: /magic-string@0.30.8:
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -8581,7 +8582,7 @@ packages:
dependencies: dependencies:
icss-utils: 5.1.0(postcss@8.4.36) icss-utils: 5.1.0(postcss@8.4.36)
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 postcss-value-parser: 4.2.0
dev: true dev: true
@ -8595,7 +8596,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
postcss: 8.4.36 postcss: 8.4.36
postcss-selector-parser: 6.0.11 postcss-selector-parser: 6.0.16
dev: true dev: true
/postcss-modules-values@4.0.0(postcss@8.4.36): /postcss-modules-values@4.0.0(postcss@8.4.36):
@ -8621,10 +8622,10 @@ packages:
optional: true optional: true
dependencies: dependencies:
postcss: 8.4.36 postcss: 8.4.36
postcss-selector-parser: 6.0.11 postcss-selector-parser: 6.0.16
/postcss-selector-parser@6.0.11: /postcss-selector-parser@6.0.16:
resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==} resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
cssesc: 3.0.0 cssesc: 3.0.0
@ -9589,7 +9590,7 @@ packages:
postcss-js: 4.0.1(postcss@8.4.36) 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-load-config: 4.0.2(postcss@8.4.36)(ts-node@10.9.2)
postcss-nested: 6.0.1(postcss@8.4.36) 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 resolve: 1.22.4
sucrase: 3.32.0 sucrase: 3.32.0
transitivePeerDependencies: transitivePeerDependencies:
@ -9874,7 +9875,7 @@ packages:
'@rollup/plugin-json': 6.0.0(rollup@3.29.4) '@rollup/plugin-json': 6.0.0(rollup@3.29.4)
'@rollup/plugin-node-resolve': 15.2.1(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/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 chalk: 5.3.0
citty: 0.1.4 citty: 0.1.4
consola: 3.2.3 consola: 3.2.3
@ -9883,7 +9884,7 @@ packages:
globby: 13.2.2 globby: 13.2.2
hookable: 5.5.3 hookable: 5.5.3
jiti: 1.20.0 jiti: 1.20.0
magic-string: 0.30.4 magic-string: 0.30.8
mkdist: 1.3.0(typescript@5.2.2) mkdist: 1.3.0(typescript@5.2.2)
mlly: 1.4.2 mlly: 1.4.2
pathe: 1.1.2 pathe: 1.1.2
@ -10075,7 +10076,7 @@ packages:
- rollup - rollup
dev: true dev: true
/vite-node@1.4.0: /vite-node@1.4.0(@types/node@20.11.28):
resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true hasBin: true
@ -10084,12 +10085,55 @@ packages:
debug: 4.3.4 debug: 4.3.4
pathe: 1.1.2 pathe: 1.1.2
picocolors: 1.0.0 picocolors: 1.0.0
vite: link:packages/vite vite: 5.2.2(@types/node@20.11.28)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color - supports-color
- terser
dev: true 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==} resolution: {integrity: sha512-/OiYsu5UKpQKA2c0BAZkfyywjfauDjvXyv6Mo4Ra57m5n4Bxg1HgUGoth1CLH2vwUbR/BHvDA9zOM0RDvgeSVQ==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -10106,7 +10150,7 @@ packages:
'@shikijs/core': 1.1.5 '@shikijs/core': 1.1.5
'@shikijs/transformers': 1.1.5 '@shikijs/transformers': 1.1.5
'@types/markdown-it': 13.0.7 '@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 '@vue/devtools-api': 7.0.14
'@vueuse/core': 10.7.2(vue@3.4.21) '@vueuse/core': 10.7.2(vue@3.4.21)
'@vueuse/integrations': 10.7.2(focus-trap@7.5.4)(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 mark.js: 8.11.1
minisearch: 6.3.0 minisearch: 6.3.0
shiki: 1.1.5 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) vue: 3.4.21(typescript@5.2.2)
transitivePeerDependencies: transitivePeerDependencies:
- '@algolia/client-search' - '@algolia/client-search'
- '@types/node'
- '@types/react' - '@types/react'
- '@vue/composition-api' - '@vue/composition-api'
- async-validator - async-validator
@ -10127,12 +10172,18 @@ packages:
- fuse.js - fuse.js
- idb-keyval - idb-keyval
- jwt-decode - jwt-decode
- less
- lightningcss
- nprogress - nprogress
- qrcode - qrcode
- react - react
- react-dom - react-dom
- sass
- search-insights - search-insights
- sortablejs - sortablejs
- stylus
- sugarss
- terser
- typescript - typescript
- universal-cookie - universal-cookie
dev: true dev: true
@ -10180,12 +10231,18 @@ packages:
strip-literal: 2.0.0 strip-literal: 2.0.0
tinybench: 2.5.1 tinybench: 2.5.1
tinypool: 0.8.2 tinypool: 0.8.2
vite: link:packages/vite vite: 5.2.2(@types/node@20.11.28)
vite-node: 1.4.0 vite-node: 1.4.0(@types/node@20.11.28)
why-is-node-running: 2.2.2 why-is-node-running: 2.2.2
transitivePeerDependencies: transitivePeerDependencies:
- acorn - acorn
- less
- lightningcss
- sass
- stylus
- sugarss
- supports-color - supports-color
- terser
dev: true dev: true
/void-elements@3.1.0: /void-elements@3.1.0: