Skip to content
Merged
6 changes: 6 additions & 0 deletions .changeset/easy-breads-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@css-modules-kit/ts-plugin': minor
'@css-modules-kit/core': minor
---

feat(core, ts-plugin): support non-JavaScript identifier token in default export
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,11 @@ Due to implementation constraints and technical reasons, css-modules-kit has var

- Sass and Less are not supported.
- If you want to use Sass and Less, please use [happy-css-modules](https://github.com/mizdra/happy-css-modules). Although it does not offer as rich language features as css-modules-kit, it provides basic features such as code completion and Go to Definition.
- The name of classes, `@value`, and `@keyframes` must be valid JavaScript identifiers.
- Case conversion for [token](docs/glossary.md#token) names is not supported.
- For example, if you have a CSS class `.foo-bar`, it will be exported as `styles['foo-bar']`, not `styles.fooBar` or `styles.foo_bar`.
- The [token](docs/glossary.md#token) names must be valid JavaScript identifiers when `cmkOptions.namedExports` is `true`.
- For example, `.fooBar` and `.foo_bar` are supported, but `.foo-bar` is not supported.
- See [#176](https://github.com/mizdra/css-modules-kit/issues/176) for more details.
- This restriction may be lifted in the future.
- The specifiers in `@import '<specifier>'` and `@value ... from '<specifier>'` are resolved according to TypeScript's module resolution method.
- This may differ from the resolution methods of bundlers like Turbopack or Vite.
- If you want to use import aliases, use [`compilerOptions.paths`](https://www.typescriptlang.org/tsconfig/#paths) or [`imports`](https://nodejs.org/api/packages.html#imports) in `package.json`.
Expand Down
14 changes: 7 additions & 7 deletions examples/1-basic/generated/src/a.module.css.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// @ts-nocheck
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
declare const styles = {
a_1: '' as readonly string,
a_2: '' as readonly string,
a_2: '' as readonly string,
a_3: '' as readonly string,
a_4: '' as readonly string,
'a_1': '' as readonly string,
'a_2': '' as readonly string,
'a_2': '' as readonly string,
'a_3': '' as readonly string,
'a_4': '' as readonly string,
...blockErrorType((await import('./b.module.css')).default),
c_1: (await import('./c.module.css')).default.c_1,
c_alias: (await import('./c.module.css')).default.c_2,
'c_1': (await import('./c.module.css')).default['c_1'],
'c_alias': (await import('./c.module.css')).default['c_2'],
};
export default styles;
4 changes: 2 additions & 2 deletions examples/1-basic/generated/src/b.module.css.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-nocheck
declare const styles = {
b_1: '' as readonly string,
b_2: '' as readonly string,
'b_1': '' as readonly string,
'b_2': '' as readonly string,
};
export default styles;
4 changes: 2 additions & 2 deletions examples/1-basic/generated/src/c.module.css.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-nocheck
declare const styles = {
c_1: '' as readonly string,
c_2: '' as readonly string,
'c_1': '' as readonly string,
'c_2': '' as readonly string,
};
export default styles;
2 changes: 1 addition & 1 deletion examples/3-import-alias/generated/src/b.module.css.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-nocheck
declare const styles = {
b_1: '' as readonly string,
'b_1': '' as readonly string,
};
export default styles;
12 changes: 6 additions & 6 deletions packages/codegen/e2e-test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ test('generates .d.ts', async () => {
"// @ts-nocheck
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
declare const styles = {
a1: '' as readonly string,
'a1': '' as readonly string,
...blockErrorType((await import('./b.module.css')).default),
...blockErrorType((await import('./unmatched.module.css')).default),
};
Expand All @@ -58,15 +58,15 @@ test('generates .d.ts', async () => {
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
b1: '' as readonly string,
'b1': '' as readonly string,
};
export default styles;
"
`);
expect(await iff.readFile('generated/src/c.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
c1: '' as readonly string,
'c1': '' as readonly string,
};
export default styles;
"
Expand Down Expand Up @@ -158,7 +158,7 @@ test('generates .d.ts with circular import', async () => {
"// @ts-nocheck
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
declare const styles = {
a1: '' as readonly string,
'a1': '' as readonly string,
...blockErrorType((await import('./b.module.css')).default),
};
export default styles;
Expand All @@ -168,7 +168,7 @@ test('generates .d.ts with circular import', async () => {
"// @ts-nocheck
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
declare const styles = {
b1: '' as readonly string,
'b1': '' as readonly string,
...blockErrorType((await import('./a.module.css')).default),
};
export default styles;
Expand All @@ -178,7 +178,7 @@ test('generates .d.ts with circular import', async () => {
"// @ts-nocheck
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
declare const styles = {
c1: '' as readonly string,
'c1': '' as readonly string,
...blockErrorType((await import('./c.module.css')).default),
};
export default styles;
Expand Down
8 changes: 4 additions & 4 deletions packages/codegen/src/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe('updateFile', () => {
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
a_1: '' as readonly string,
'a_1': '' as readonly string,
};
export default styles;
"
Expand Down Expand Up @@ -537,15 +537,15 @@ describe('emitDtsFiles', () => {
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
a1: '' as readonly string,
'a1': '' as readonly string,
};
export default styles;
"
`);
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
b1: '' as readonly string,
'b1': '' as readonly string,
};
export default styles;
"
Expand All @@ -562,7 +562,7 @@ describe('emitDtsFiles', () => {
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
a1: '' as readonly string,
'a1': '' as readonly string,
};
export default styles;
"
Expand Down
8 changes: 4 additions & 4 deletions packages/codegen/src/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ describe('runCMK', () => {
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
a_1: '' as readonly string,
'a_1': '' as readonly string,
};
export default styles;
"
`);
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
b_1: '' as readonly string,
'b_1': '' as readonly string,
};
export default styles;
"
Expand Down Expand Up @@ -133,15 +133,15 @@ describe('runCMKInWatchMode', () => {
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
a_1: '' as readonly string,
'a_1': '' as readonly string,
};
export default styles;
"
`);
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
b_1: '' as readonly string,
'b_1': '' as readonly string,
};
export default styles;
"
Expand Down
50 changes: 45 additions & 5 deletions packages/core/src/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function prepareChecker(args?: Partial<CheckerArgs>): Checker {
}

describe('checkCSSModule', () => {
test('report diagnostics for invalid name as js identifier', async () => {
test('do not report diagnostics for invalid name as js identifier when namedExports is false', async () => {
const iff = await createIFF({
'a.module.css': dedent`
.a-1 { color: red; }
Expand All @@ -49,6 +49,21 @@ describe('checkCSSModule', () => {
});
const check = prepareChecker();
const diagnostics = check(readAndParseCSSModule(iff.paths['a.module.css'])!);
expect(formatDiagnostics(diagnostics, iff.rootDir)).toMatchInlineSnapshot(`[]`);
});
test('report diagnostics for invalid name as js identifier when namedExports is true', async () => {
const iff = await createIFF({
'a.module.css': dedent`
.a-1 { color: red; }
@value b-1, b-2 as a-2 from './b.module.css';
`,
'b.module.css': dedent`
@value b-1: red;
@value b-2: red;
`,
});
const check = prepareChecker({ config: fakeConfig({ namedExports: true }) });
const diagnostics = check(readAndParseCSSModule(iff.paths['a.module.css'])!);
expect(formatDiagnostics(diagnostics, iff.rootDir)).toMatchInlineSnapshot(`
[
{
Expand All @@ -59,7 +74,7 @@ describe('checkCSSModule', () => {
"column": 2,
"line": 1,
},
"text": "css-modules-kit does not support invalid names as JavaScript identifiers.",
"text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.",
},
{
"category": "error",
Expand All @@ -69,7 +84,7 @@ describe('checkCSSModule', () => {
"column": 8,
"line": 2,
},
"text": "css-modules-kit does not support invalid names as JavaScript identifiers.",
"text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.",
},
{
"category": "error",
Expand All @@ -79,7 +94,7 @@ describe('checkCSSModule', () => {
"column": 13,
"line": 2,
},
"text": "css-modules-kit does not support invalid names as JavaScript identifiers.",
"text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.",
},
{
"category": "error",
Expand All @@ -89,7 +104,7 @@ describe('checkCSSModule', () => {
"column": 20,
"line": 2,
},
"text": "css-modules-kit does not support invalid names as JavaScript identifiers.",
"text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.",
},
]
`);
Expand Down Expand Up @@ -190,6 +205,31 @@ describe('checkCSSModule', () => {
]
`);
});
test('report diagnostics for backslash in name when namedExports is false', async () => {
// NOTE: The backslash is valid syntax in class selectors, but it is invalid syntax in `@value`.
// Therefore, it is sufficient for diagnostics to be reported only for class selectors.
const iff = await createIFF({
'a.module.css': dedent`
.a\\1 { color: red; }
`,
});
const check = prepareChecker();
const diagnostics = check(readAndParseCSSModule(iff.paths['a.module.css'])!);
expect(formatDiagnostics(diagnostics, iff.rootDir)).toMatchInlineSnapshot(`
[
{
"category": "error",
"fileName": "<rootDir>/a.module.css",
"length": 4,
"start": {
"column": 2,
"line": 1,
},
"text": "Backslash (\\) is not allowed in names when \`cmkOptions.namedExports\` is set to \`false\`.",
},
]
`);
});
test('report diagnostics for non-exported token', async () => {
const iff = await createIFF({
'a.module.css': `@value b_1, b_2 from './b.module.css';`,
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,17 @@ function createTokenNameDiagnostic(cssModule: CSSModule, loc: Location, violatio
let text: string;
switch (violation) {
case 'invalid-js-identifier':
text = `css-modules-kit does not support invalid names as JavaScript identifiers.`;
text = `Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.`;
break;
case 'proto-not-allowed':
text = `\`__proto__\` is not allowed as names.`;
break;
case 'default-not-allowed':
text = `\`default\` is not allowed as names when \`cmkOptions.namedExports\` is set to \`true\`.`;
break;
case 'backslash-not-allowed':
text = `Backslash (\\) is not allowed in names when \`cmkOptions.namedExports\` is set to \`false\`.`;
break;
default:
throw new Error('unreachable: unknown TokenNameViolation');
}
Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/dts-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ describe('generateDts', () => {
expect(generateDts(readAndParseCSSModule(iff.paths['test.module.css'])!, options).text).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
local1: '' as readonly string,
local2: '' as readonly string,
'local1': '' as readonly string,
'local2': '' as readonly string,
};
export default styles;
"
Expand All @@ -52,8 +52,8 @@ describe('generateDts', () => {
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
declare const styles = {
...blockErrorType((await import('./a.module.css')).default),
imported1: (await import('./b.module.css')).default.imported1,
aliasedImported2: (await import('./b.module.css')).default.imported2,
'imported1': (await import('./b.module.css')).default['imported1'],
'aliasedImported2': (await import('./b.module.css')).default['imported2'],
};
export default styles;
"
Expand All @@ -77,9 +77,9 @@ describe('generateDts', () => {
test('does not generate types for invalid name', async () => {
const iff = await createIFF({
'test.module.css': dedent`
.a-1 { color: red; }
@value b-1 from './b.module.css';
@value b_2 as a-2 from './b.module.css';
.__proto__ { color: red; }
@value __proto__ from './b.module.css';
@value b_1 as __proto__ from './b.module.css';
`,
});
expect(generateDts(readAndParseCSSModule(iff.paths['test.module.css'])!, options).text).toMatchInlineSnapshot(`
Expand Down
Loading