Skip to content

Commit 60b7515

Browse files
authored
generate .d.ts files even if syntax or semantic errors are found (#269)
1 parent 86b15f2 commit 60b7515

File tree

7 files changed

+125
-48
lines changed

7 files changed

+125
-48
lines changed

.changeset/busy-plants-take.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@css-modules-kit/ts-plugin': minor
3+
'@css-modules-kit/codegen': minor
4+
'@css-modules-kit/core': minor
5+
---
6+
7+
feat: generate .d.ts files even if syntax or semantic errors are found

packages/codegen/src/runner.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,22 @@ describe('runCMK', () => {
204204
},
205205
]
206206
`);
207+
// Even if there is a syntax error, .d.ts files are generated.
208+
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
209+
"// @ts-nocheck
210+
declare const styles = {
211+
a1: '' as readonly string,
212+
};
213+
export default styles;
214+
"
215+
`);
216+
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
217+
"// @ts-nocheck
218+
declare const styles = {
219+
};
220+
export default styles;
221+
"
222+
`);
207223
});
208224
test('reports semantic diagnostics in *.module.css', async () => {
209225
const iff = await createIFF({
@@ -247,6 +263,25 @@ describe('runCMK', () => {
247263
},
248264
]
249265
`);
266+
// Even if there is a semantic error, .d.ts files are generated.
267+
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
268+
"// @ts-nocheck
269+
declare const styles = {
270+
b_1: (await import('./b.module.css')).default.b_1,
271+
b_2: (await import('./b.module.css')).default.b_2,
272+
};
273+
export default styles;
274+
"
275+
`);
276+
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
277+
"// @ts-nocheck
278+
declare const styles = {
279+
b_1: '' as readonly string,
280+
...(await import('./c.module.css')).default,
281+
};
282+
export default styles;
283+
"
284+
`);
250285
});
251286
});
252287

packages/codegen/src/runner.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async function parseCSSModuleByFileName(fileName: string, config: CMKConfig): Pr
3434
} catch (error) {
3535
throw new ReadCSSModuleFileError(fileName, error);
3636
}
37-
return parseCSSModule(text, { fileName, safe: false, keyframes: config.keyframes });
37+
return parseCSSModule(text, { fileName, includeSyntaxError: true, keyframes: config.keyframes });
3838
}
3939

4040
/**
@@ -101,6 +101,15 @@ export async function runCMK(args: ParsedArgs, logger: Logger): Promise<void> {
101101
syntacticDiagnostics.push(...parseResult.diagnostics);
102102
}
103103

104+
if (args.clean) {
105+
await rm(config.dtsOutDir, { recursive: true, force: true });
106+
}
107+
await Promise.all(
108+
parseResults.map(async (parseResult) =>
109+
writeDtsByCSSModule(parseResult.cssModule, config, resolver, matchesPattern),
110+
),
111+
);
112+
104113
if (syntacticDiagnostics.length > 0) {
105114
logger.logDiagnostics(syntacticDiagnostics);
106115
// eslint-disable-next-line n/no-process-exit
@@ -120,13 +129,4 @@ export async function runCMK(args: ParsedArgs, logger: Logger): Promise<void> {
120129
// eslint-disable-next-line n/no-process-exit
121130
process.exit(1);
122131
}
123-
124-
if (args.clean) {
125-
await rm(config.dtsOutDir, { recursive: true, force: true });
126-
}
127-
await Promise.all(
128-
parseResults.map(async (parseResult) =>
129-
writeDtsByCSSModule(parseResult.cssModule, config, resolver, matchesPattern),
130-
),
131-
);
132132
}

packages/core/src/parser/css-module-parser.test.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import dedent from 'dedent';
22
import { describe, expect, test } from 'vitest';
33
import { parseCSSModule, type ParseCSSModuleOptions } from './css-module-parser.js';
44

5-
const options: ParseCSSModuleOptions = { fileName: '/test.module.css', safe: false, keyframes: true };
5+
const options: ParseCSSModuleOptions = { fileName: '/test.module.css', includeSyntaxError: true, keyframes: true };
66

77
describe('parseCSSModule', () => {
88
test('collects local tokens', () => {
@@ -695,7 +695,35 @@ describe('parseCSSModule', () => {
695695
{
696696
"cssModule": {
697697
"fileName": "/test.module.css",
698-
"localTokens": [],
698+
"localTokens": [
699+
{
700+
"declarationLoc": {
701+
"end": {
702+
"column": 6,
703+
"line": 1,
704+
"offset": 5,
705+
},
706+
"start": {
707+
"column": 1,
708+
"line": 1,
709+
"offset": 0,
710+
},
711+
},
712+
"loc": {
713+
"end": {
714+
"column": 3,
715+
"line": 1,
716+
"offset": 2,
717+
},
718+
"start": {
719+
"column": 2,
720+
"line": 1,
721+
"offset": 1,
722+
},
723+
},
724+
"name": "a",
725+
},
726+
],
699727
"text": ".a {",
700728
"tokenImporters": [],
701729
},
@@ -742,14 +770,15 @@ describe('parseCSSModule', () => {
742770
}
743771
`);
744772
});
745-
test('parses CSS in a fault-tolerant manner if safe is true', () => {
746-
const parsed = parseCSSModule(
747-
dedent`
748-
.a {
749-
`,
750-
{ ...options, safe: true },
751-
);
752-
expect(parsed).toMatchInlineSnapshot(`
773+
test('does not include syntax error in diagnostics if includeSyntaxError is false', () => {
774+
expect(
775+
parseCSSModule(
776+
dedent`
777+
.a {
778+
`,
779+
{ ...options, includeSyntaxError: false },
780+
),
781+
).toMatchInlineSnapshot(`
753782
{
754783
"cssModule": {
755784
"fileName": "/test.module.css",

packages/core/src/parser/css-module-parser.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ function collectTokens(ast: Root, keyframes: boolean) {
7878

7979
export interface ParseCSSModuleOptions {
8080
fileName: string;
81-
safe: boolean;
81+
/** Whether to include syntax errors from diagnostics */
82+
includeSyntaxError: boolean;
8283
keyframes: boolean;
8384
}
8485

@@ -87,39 +88,46 @@ export interface ParseCSSModuleResult {
8788
diagnostics: DiagnosticWithLocation[];
8889
}
8990

91+
/**
92+
* Parse CSS Module text.
93+
* If a syntax error is detected in the text, it is re-parsed using `postcss-safe-parser`, and `localTokens` are collected as much as possible.
94+
*/
9095
export function parseCSSModule(
9196
text: string,
92-
{ fileName, safe, keyframes }: ParseCSSModuleOptions,
97+
{ fileName, includeSyntaxError, keyframes }: ParseCSSModuleOptions,
9398
): ParseCSSModuleResult {
9499
let ast: Root;
95-
const diagnosticSourceFile = { fileName, text };
96-
try {
97-
const parser = safe ? safeParser : parse;
98-
ast = parser(text, { from: fileName });
99-
} catch (e) {
100-
if (e instanceof CssSyntaxError) {
100+
const diagnosticFile = { fileName, text };
101+
const allDiagnostics: DiagnosticWithLocation[] = [];
102+
if (includeSyntaxError) {
103+
try {
104+
ast = parse(text, { from: fileName });
105+
} catch (e) {
106+
if (!(e instanceof CssSyntaxError)) throw e;
107+
// If syntax error, try to parse with safe parser. While this incurs a cost
108+
// due to parsing the file twice, it rarely becomes an issue since files
109+
// with syntax errors are usually few in number.
110+
ast = safeParser(text, { from: fileName });
101111
const { line, column, endColumn } = e.input!;
102-
return {
103-
cssModule: { fileName, text, localTokens: [], tokenImporters: [] },
104-
diagnostics: [
105-
{
106-
file: diagnosticSourceFile,
107-
start: { line, column },
108-
length: endColumn !== undefined ? endColumn - column : 1,
109-
text: e.reason,
110-
category: 'error',
111-
},
112-
],
113-
};
112+
allDiagnostics.push({
113+
file: diagnosticFile,
114+
start: { line, column },
115+
length: endColumn !== undefined ? endColumn - column : 1,
116+
text: e.reason,
117+
category: 'error',
118+
});
114119
}
115-
throw e;
120+
} else {
121+
ast = safeParser(text, { from: fileName });
116122
}
123+
117124
const { localTokens, tokenImporters, diagnostics } = collectTokens(ast, keyframes);
118125
const cssModule = {
119126
fileName,
120127
text,
121128
localTokens,
122129
tokenImporters,
123130
};
124-
return { cssModule, diagnostics: diagnostics.map((diagnostic) => ({ ...diagnostic, file: diagnosticSourceFile })) };
131+
allDiagnostics.push(...diagnostics.map((diagnostic) => ({ ...diagnostic, file: diagnosticFile })));
132+
return { cssModule, diagnostics: allDiagnostics };
125133
}

packages/ts-plugin/e2e-test/invalid-syntax.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@ describe('handle invalid syntax CSS without crashing', async () => {
4444
expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual(normalizeDefinitions(expected));
4545
});
4646
test('does not report syntactic diagnostics', async () => {
47-
// NOTE: The standard CSS Language Server reports invalid syntax errors.
48-
// Therefore, if ts-plugin also reports it, the same error is reported twice.
49-
// To avoid this, ts-plugin does not report invalid syntax errors.
5047
const res = await tsserver.sendSyntacticDiagnosticsSync({
5148
file: iff.paths['a.module.css'],
5249
});

packages/ts-plugin/src/language-plugin.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ export function createCSSLanguagePlugin(
4949
const cssModuleCode = snapshot.getText(0, length);
5050
const { cssModule, diagnostics } = parseCSSModule(cssModuleCode, {
5151
fileName: scriptId,
52-
// The CSS in the process of being written in an editor often contains invalid syntax.
53-
// So, ts-plugin uses a fault-tolerant Parser to parse CSS.
54-
safe: true,
52+
// NOTE: The standard CSS Language Server reports invalid syntax errors.
53+
// Therefore, if ts-plugin also reports it, the same error is reported twice.
54+
// To avoid this, ts-plugin does not report invalid syntax errors.
55+
includeSyntaxError: false,
5556
keyframes: config.keyframes,
5657
});
5758
// eslint-disable-next-line prefer-const

0 commit comments

Comments
 (0)