Skip to content

Commit d89f583

Browse files
mizdraapple-yagi
andauthored
Support non-JavaScript identifier token in default export (#330)
* docs: fix dts-generator documentation * wip: allow tokens that are not valid as JavaScript identifiers when `namedExport` is false Co-Authored-By: Ryuya Yanagi <57742720+apple-yagi@users.noreply.github.com> * wip: disallow tokens that contains backslash * wip: support non-JavaScript identifier token in default export Co-Authored-By: Ryuya Yanagi <57742720+apple-yagi@users.noreply.github.com> * chore: npm run update-generated-in-examples * test: add test cases for non-js identifier token * docs: update limitation section * chore: add changelog * wip: ensure unquote function handles short strings correctly Co-Authored-By: Ryuya Yanagi <57742720+apple-yagi@users.noreply.github.com> --------- Co-authored-by: Ryuya Yanagi <57742720+apple-yagi@users.noreply.github.com>
1 parent 1d3ff96 commit d89f583

23 files changed

Lines changed: 372 additions & 129 deletions

.changeset/easy-breads-chew.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@css-modules-kit/ts-plugin': minor
3+
'@css-modules-kit/core': minor
4+
---
5+
6+
feat(core, ts-plugin): support non-JavaScript identifier token in default export

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,11 @@ Due to implementation constraints and technical reasons, css-modules-kit has var
231231

232232
- Sass and Less are not supported.
233233
- 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.
234-
- The name of classes, `@value`, and `@keyframes` must be valid JavaScript identifiers.
234+
- Case conversion for [token](docs/glossary.md#token) names is not supported.
235+
- 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`.
236+
- The [token](docs/glossary.md#token) names must be valid JavaScript identifiers when `cmkOptions.namedExports` is `true`.
235237
- For example, `.fooBar` and `.foo_bar` are supported, but `.foo-bar` is not supported.
236-
- See [#176](https://github.com/mizdra/css-modules-kit/issues/176) for more details.
238+
- This restriction may be lifted in the future.
237239
- The specifiers in `@import '<specifier>'` and `@value ... from '<specifier>'` are resolved according to TypeScript's module resolution method.
238240
- This may differ from the resolution methods of bundlers like Turbopack or Vite.
239241
- 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`.
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// @ts-nocheck
22
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
33
declare const styles = {
4-
a_1: '' as readonly string,
5-
a_2: '' as readonly string,
6-
a_2: '' as readonly string,
7-
a_3: '' as readonly string,
8-
a_4: '' as readonly string,
4+
'a_1': '' as readonly string,
5+
'a_2': '' as readonly string,
6+
'a_2': '' as readonly string,
7+
'a_3': '' as readonly string,
8+
'a_4': '' as readonly string,
99
...blockErrorType((await import('./b.module.css')).default),
10-
c_1: (await import('./c.module.css')).default.c_1,
11-
c_alias: (await import('./c.module.css')).default.c_2,
10+
'c_1': (await import('./c.module.css')).default['c_1'],
11+
'c_alias': (await import('./c.module.css')).default['c_2'],
1212
};
1313
export default styles;
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @ts-nocheck
22
declare const styles = {
3-
b_1: '' as readonly string,
4-
b_2: '' as readonly string,
3+
'b_1': '' as readonly string,
4+
'b_2': '' as readonly string,
55
};
66
export default styles;
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @ts-nocheck
22
declare const styles = {
3-
c_1: '' as readonly string,
4-
c_2: '' as readonly string,
3+
'c_1': '' as readonly string,
4+
'c_2': '' as readonly string,
55
};
66
export default styles;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @ts-nocheck
22
declare const styles = {
3-
b_1: '' as readonly string,
3+
'b_1': '' as readonly string,
44
};
55
export default styles;

packages/codegen/e2e-test/index.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ test('generates .d.ts', async () => {
4848
"// @ts-nocheck
4949
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
5050
declare const styles = {
51-
a1: '' as readonly string,
51+
'a1': '' as readonly string,
5252
...blockErrorType((await import('./b.module.css')).default),
5353
...blockErrorType((await import('./unmatched.module.css')).default),
5454
};
@@ -58,15 +58,15 @@ test('generates .d.ts', async () => {
5858
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
5959
"// @ts-nocheck
6060
declare const styles = {
61-
b1: '' as readonly string,
61+
'b1': '' as readonly string,
6262
};
6363
export default styles;
6464
"
6565
`);
6666
expect(await iff.readFile('generated/src/c.module.css.d.ts')).toMatchInlineSnapshot(`
6767
"// @ts-nocheck
6868
declare const styles = {
69-
c1: '' as readonly string,
69+
'c1': '' as readonly string,
7070
};
7171
export default styles;
7272
"
@@ -158,7 +158,7 @@ test('generates .d.ts with circular import', async () => {
158158
"// @ts-nocheck
159159
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
160160
declare const styles = {
161-
a1: '' as readonly string,
161+
'a1': '' as readonly string,
162162
...blockErrorType((await import('./b.module.css')).default),
163163
};
164164
export default styles;
@@ -168,7 +168,7 @@ test('generates .d.ts with circular import', async () => {
168168
"// @ts-nocheck
169169
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
170170
declare const styles = {
171-
b1: '' as readonly string,
171+
'b1': '' as readonly string,
172172
...blockErrorType((await import('./a.module.css')).default),
173173
};
174174
export default styles;
@@ -178,7 +178,7 @@ test('generates .d.ts with circular import', async () => {
178178
"// @ts-nocheck
179179
function blockErrorType<T>(val: T): [0] extends [(1 & T)] ? {} : T;
180180
declare const styles = {
181-
c1: '' as readonly string,
181+
'c1': '' as readonly string,
182182
...blockErrorType((await import('./c.module.css')).default),
183183
};
184184
export default styles;

packages/codegen/src/project.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ describe('updateFile', () => {
197197
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
198198
"// @ts-nocheck
199199
declare const styles = {
200-
a_1: '' as readonly string,
200+
'a_1': '' as readonly string,
201201
};
202202
export default styles;
203203
"
@@ -537,15 +537,15 @@ describe('emitDtsFiles', () => {
537537
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
538538
"// @ts-nocheck
539539
declare const styles = {
540-
a1: '' as readonly string,
540+
'a1': '' as readonly string,
541541
};
542542
export default styles;
543543
"
544544
`);
545545
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
546546
"// @ts-nocheck
547547
declare const styles = {
548-
b1: '' as readonly string,
548+
'b1': '' as readonly string,
549549
};
550550
export default styles;
551551
"
@@ -562,7 +562,7 @@ describe('emitDtsFiles', () => {
562562
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
563563
"// @ts-nocheck
564564
declare const styles = {
565-
a1: '' as readonly string,
565+
'a1': '' as readonly string,
566566
};
567567
export default styles;
568568
"

packages/codegen/src/runner.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ describe('runCMK', () => {
3030
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
3131
"// @ts-nocheck
3232
declare const styles = {
33-
a_1: '' as readonly string,
33+
'a_1': '' as readonly string,
3434
};
3535
export default styles;
3636
"
3737
`);
3838
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
3939
"// @ts-nocheck
4040
declare const styles = {
41-
b_1: '' as readonly string,
41+
'b_1': '' as readonly string,
4242
};
4343
export default styles;
4444
"
@@ -133,15 +133,15 @@ describe('runCMKInWatchMode', () => {
133133
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
134134
"// @ts-nocheck
135135
declare const styles = {
136-
a_1: '' as readonly string,
136+
'a_1': '' as readonly string,
137137
};
138138
export default styles;
139139
"
140140
`);
141141
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
142142
"// @ts-nocheck
143143
declare const styles = {
144-
b_1: '' as readonly string,
144+
'b_1': '' as readonly string,
145145
};
146146
export default styles;
147147
"

packages/core/src/checker.test.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function prepareChecker(args?: Partial<CheckerArgs>): Checker {
3636
}
3737

3838
describe('checkCSSModule', () => {
39-
test('report diagnostics for invalid name as js identifier', async () => {
39+
test('do not report diagnostics for invalid name as js identifier when namedExports is false', async () => {
4040
const iff = await createIFF({
4141
'a.module.css': dedent`
4242
.a-1 { color: red; }
@@ -49,6 +49,21 @@ describe('checkCSSModule', () => {
4949
});
5050
const check = prepareChecker();
5151
const diagnostics = check(readAndParseCSSModule(iff.paths['a.module.css'])!);
52+
expect(formatDiagnostics(diagnostics, iff.rootDir)).toMatchInlineSnapshot(`[]`);
53+
});
54+
test('report diagnostics for invalid name as js identifier when namedExports is true', async () => {
55+
const iff = await createIFF({
56+
'a.module.css': dedent`
57+
.a-1 { color: red; }
58+
@value b-1, b-2 as a-2 from './b.module.css';
59+
`,
60+
'b.module.css': dedent`
61+
@value b-1: red;
62+
@value b-2: red;
63+
`,
64+
});
65+
const check = prepareChecker({ config: fakeConfig({ namedExports: true }) });
66+
const diagnostics = check(readAndParseCSSModule(iff.paths['a.module.css'])!);
5267
expect(formatDiagnostics(diagnostics, iff.rootDir)).toMatchInlineSnapshot(`
5368
[
5469
{
@@ -59,7 +74,7 @@ describe('checkCSSModule', () => {
5974
"column": 2,
6075
"line": 1,
6176
},
62-
"text": "css-modules-kit does not support invalid names as JavaScript identifiers.",
77+
"text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.",
6378
},
6479
{
6580
"category": "error",
@@ -69,7 +84,7 @@ describe('checkCSSModule', () => {
6984
"column": 8,
7085
"line": 2,
7186
},
72-
"text": "css-modules-kit does not support invalid names as JavaScript identifiers.",
87+
"text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.",
7388
},
7489
{
7590
"category": "error",
@@ -79,7 +94,7 @@ describe('checkCSSModule', () => {
7994
"column": 13,
8095
"line": 2,
8196
},
82-
"text": "css-modules-kit does not support invalid names as JavaScript identifiers.",
97+
"text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.",
8398
},
8499
{
85100
"category": "error",
@@ -89,7 +104,7 @@ describe('checkCSSModule', () => {
89104
"column": 20,
90105
"line": 2,
91106
},
92-
"text": "css-modules-kit does not support invalid names as JavaScript identifiers.",
107+
"text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.",
93108
},
94109
]
95110
`);
@@ -190,6 +205,31 @@ describe('checkCSSModule', () => {
190205
]
191206
`);
192207
});
208+
test('report diagnostics for backslash in name when namedExports is false', async () => {
209+
// NOTE: The backslash is valid syntax in class selectors, but it is invalid syntax in `@value`.
210+
// Therefore, it is sufficient for diagnostics to be reported only for class selectors.
211+
const iff = await createIFF({
212+
'a.module.css': dedent`
213+
.a\\1 { color: red; }
214+
`,
215+
});
216+
const check = prepareChecker();
217+
const diagnostics = check(readAndParseCSSModule(iff.paths['a.module.css'])!);
218+
expect(formatDiagnostics(diagnostics, iff.rootDir)).toMatchInlineSnapshot(`
219+
[
220+
{
221+
"category": "error",
222+
"fileName": "<rootDir>/a.module.css",
223+
"length": 4,
224+
"start": {
225+
"column": 2,
226+
"line": 1,
227+
},
228+
"text": "Backslash (\\) is not allowed in names when \`cmkOptions.namedExports\` is set to \`false\`.",
229+
},
230+
]
231+
`);
232+
});
193233
test('report diagnostics for non-exported token', async () => {
194234
const iff = await createIFF({
195235
'a.module.css': `@value b_1, b_2 from './b.module.css';`,

0 commit comments

Comments
 (0)