Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/busy-plants-take.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@css-modules-kit/ts-plugin': minor
'@css-modules-kit/codegen': minor
'@css-modules-kit/core': minor
---

feat: generate .d.ts files even if syntax or semantic errors are found
35 changes: 35 additions & 0 deletions packages/codegen/src/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,22 @@ describe('runCMK', () => {
},
]
`);
// Even if there is a syntax error, .d.ts files are generated.
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
a1: '' as readonly string,
};
export default styles;
"
`);
expect(await iff.readFile('generated/src/b.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
};
export default styles;
"
`);
});
test('reports semantic diagnostics in *.module.css', async () => {
const iff = await createIFF({
Expand Down Expand Up @@ -247,6 +263,25 @@ describe('runCMK', () => {
},
]
`);
// Even if there is a semantic error, .d.ts files are generated.
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
b_1: (await import('./b.module.css')).default.b_1,
b_2: (await import('./b.module.css')).default.b_2,
};
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,
...(await import('./c.module.css')).default,
};
export default styles;
"
`);
});
});

Expand Down
20 changes: 10 additions & 10 deletions packages/codegen/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async function parseCSSModuleByFileName(fileName: string, config: CMKConfig): Pr
} catch (error) {
throw new ReadCSSModuleFileError(fileName, error);
}
return parseCSSModule(text, { fileName, safe: false, keyframes: config.keyframes });
return parseCSSModule(text, { fileName, includeSyntaxError: true, keyframes: config.keyframes });
}

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

if (args.clean) {
await rm(config.dtsOutDir, { recursive: true, force: true });
}
await Promise.all(
parseResults.map(async (parseResult) =>
writeDtsByCSSModule(parseResult.cssModule, config, resolver, matchesPattern),
),
);

if (syntacticDiagnostics.length > 0) {
logger.logDiagnostics(syntacticDiagnostics);
// eslint-disable-next-line n/no-process-exit
Expand All @@ -120,13 +129,4 @@ export async function runCMK(args: ParsedArgs, logger: Logger): Promise<void> {
// eslint-disable-next-line n/no-process-exit
process.exit(1);
}

if (args.clean) {
await rm(config.dtsOutDir, { recursive: true, force: true });
}
await Promise.all(
parseResults.map(async (parseResult) =>
writeDtsByCSSModule(parseResult.cssModule, config, resolver, matchesPattern),
),
);
}
49 changes: 39 additions & 10 deletions packages/core/src/parser/css-module-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import dedent from 'dedent';
import { describe, expect, test } from 'vitest';
import { parseCSSModule, type ParseCSSModuleOptions } from './css-module-parser.js';

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

describe('parseCSSModule', () => {
test('collects local tokens', () => {
Expand Down Expand Up @@ -695,7 +695,35 @@ describe('parseCSSModule', () => {
{
"cssModule": {
"fileName": "/test.module.css",
"localTokens": [],
"localTokens": [
{
"declarationLoc": {
"end": {
"column": 6,
"line": 1,
"offset": 5,
},
"start": {
"column": 1,
"line": 1,
"offset": 0,
},
},
"loc": {
"end": {
"column": 3,
"line": 1,
"offset": 2,
},
"start": {
"column": 2,
"line": 1,
"offset": 1,
},
},
"name": "a",
},
],
"text": ".a {",
"tokenImporters": [],
},
Expand Down Expand Up @@ -742,14 +770,15 @@ describe('parseCSSModule', () => {
}
`);
});
test('parses CSS in a fault-tolerant manner if safe is true', () => {
const parsed = parseCSSModule(
dedent`
.a {
`,
{ ...options, safe: true },
);
expect(parsed).toMatchInlineSnapshot(`
test('does not include syntax error in diagnostics if includeSyntaxError is false', () => {
expect(
parseCSSModule(
dedent`
.a {
`,
{ ...options, includeSyntaxError: false },
),
).toMatchInlineSnapshot(`
{
"cssModule": {
"fileName": "/test.module.css",
Expand Down
52 changes: 30 additions & 22 deletions packages/core/src/parser/css-module-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ function collectTokens(ast: Root, keyframes: boolean) {

export interface ParseCSSModuleOptions {
fileName: string;
safe: boolean;
/** Whether to include syntax errors from diagnostics */
includeSyntaxError: boolean;
keyframes: boolean;
}

Expand All @@ -87,39 +88,46 @@ export interface ParseCSSModuleResult {
diagnostics: DiagnosticWithLocation[];
}

/**
* Parse CSS Module text.
* 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.
*/
export function parseCSSModule(
text: string,
{ fileName, safe, keyframes }: ParseCSSModuleOptions,
{ fileName, includeSyntaxError, keyframes }: ParseCSSModuleOptions,
): ParseCSSModuleResult {
let ast: Root;
const diagnosticSourceFile = { fileName, text };
try {
const parser = safe ? safeParser : parse;
ast = parser(text, { from: fileName });
} catch (e) {
if (e instanceof CssSyntaxError) {
const diagnosticFile = { fileName, text };
const allDiagnostics: DiagnosticWithLocation[] = [];
if (includeSyntaxError) {
try {
ast = parse(text, { from: fileName });
} catch (e) {
if (!(e instanceof CssSyntaxError)) throw e;
// If syntax error, try to parse with safe parser. While this incurs a cost
// due to parsing the file twice, it rarely becomes an issue since files
// with syntax errors are usually few in number.
ast = safeParser(text, { from: fileName });
const { line, column, endColumn } = e.input!;
return {
cssModule: { fileName, text, localTokens: [], tokenImporters: [] },
diagnostics: [
{
file: diagnosticSourceFile,
start: { line, column },
length: endColumn !== undefined ? endColumn - column : 1,
text: e.reason,
category: 'error',
},
],
};
allDiagnostics.push({
file: diagnosticFile,
start: { line, column },
length: endColumn !== undefined ? endColumn - column : 1,
text: e.reason,
category: 'error',
});
}
throw e;
} else {
ast = safeParser(text, { from: fileName });
}

const { localTokens, tokenImporters, diagnostics } = collectTokens(ast, keyframes);
const cssModule = {
fileName,
text,
localTokens,
tokenImporters,
};
return { cssModule, diagnostics: diagnostics.map((diagnostic) => ({ ...diagnostic, file: diagnosticSourceFile })) };
allDiagnostics.push(...diagnostics.map((diagnostic) => ({ ...diagnostic, file: diagnosticFile })));
return { cssModule, diagnostics: allDiagnostics };
}
3 changes: 0 additions & 3 deletions packages/ts-plugin/e2e-test/invalid-syntax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ describe('handle invalid syntax CSS without crashing', async () => {
expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual(normalizeDefinitions(expected));
});
test('does not report syntactic diagnostics', async () => {
// NOTE: The standard CSS Language Server reports invalid syntax errors.
// Therefore, if ts-plugin also reports it, the same error is reported twice.
// To avoid this, ts-plugin does not report invalid syntax errors.
const res = await tsserver.sendSyntacticDiagnosticsSync({
file: iff.paths['a.module.css'],
});
Expand Down
7 changes: 4 additions & 3 deletions packages/ts-plugin/src/language-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ export function createCSSLanguagePlugin(
const cssModuleCode = snapshot.getText(0, length);
const { cssModule, diagnostics } = parseCSSModule(cssModuleCode, {
fileName: scriptId,
// The CSS in the process of being written in an editor often contains invalid syntax.
// So, ts-plugin uses a fault-tolerant Parser to parse CSS.
safe: true,
// NOTE: The standard CSS Language Server reports invalid syntax errors.
// Therefore, if ts-plugin also reports it, the same error is reported twice.
// To avoid this, ts-plugin does not report invalid syntax errors.
includeSyntaxError: false,
keyframes: config.keyframes,
});
// eslint-disable-next-line prefer-const
Expand Down