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/move-token-name-validation-to-parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@css-modules-kit/core': patch
'@css-modules-kit/ts-plugin': patch
'@css-modules-kit/codegen': patch
---

refactor(core, ts-plugin, codegen): report token name violations in the parse phase instead of the check phase
2 changes: 1 addition & 1 deletion packages/codegen/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export function createProject(args: ProjectArgs): Project {
animation: config.animation,
dashedIdents: config.dashedIdents,
container: config.container,
namedExports: config.namedExports,
});
}

Expand Down Expand Up @@ -191,7 +192,6 @@ export function createProject(args: ProjectArgs): Project {
let diagnostics = semanticDiagnosticsMap.get(cssModule.fileName);
if (!diagnostics) {
diagnostics = checkCSSModule(cssModule, {
config,
getExportRecord: (m) => exportBuilder.build(m),
matchesPattern,
resolver,
Expand Down
124 changes: 0 additions & 124 deletions packages/core/src/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { createExportBuilder } from './export-builder.js';
import { createResolver } from './resolver.js';
import { readAndParseCSSModule } from './test/css-module.js';
import { formatDiagnostics } from './test/diagnostic.js';
import { fakeConfig } from './test/faker.js';
import { createIFF } from './test/fixture.js';
import type { CSSModule } from './type.js';

Expand All @@ -16,7 +15,6 @@ const matchesPattern = (path: string) => path.endsWith('.module.css');
type Checker = (cssModule: CSSModule) => ReturnType<typeof checkCSSModule>;

function prepareChecker(args?: Partial<CheckerArgs>): Checker {
const config = args?.config ?? fakeConfig();
const resolverFn = args?.resolver ?? resolver;
const matchesPatternFn = args?.matchesPattern ?? matchesPattern;
const exportBuilder = createExportBuilder({
Expand All @@ -26,7 +24,6 @@ function prepareChecker(args?: Partial<CheckerArgs>): Checker {
});
return (cssModule: CSSModule) => {
return checkCSSModule(cssModule, {
config,
getExportRecord: (m) => exportBuilder.build(m),
matchesPattern: matchesPatternFn,
resolver: resolverFn,
Expand All @@ -36,127 +33,6 @@ function prepareChecker(args?: Partial<CheckerArgs>): Checker {
}

describe('checkCSSModule', () => {
test('report diagnostics for "__proto__" name', async () => {
const iff = await createIFF({
'a.module.css': dedent`
.__proto__ { color: red; }
@value __proto__, valid as __proto__ from './b.module.css';
`,
'b.module.css': dedent`
@value __proto__: red;
@value valid: 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": 9,
"start": {
"column": 2,
"line": 1,
},
"text": "\`__proto__\` is not allowed as names.",
},
{
"category": "error",
"fileName": "<rootDir>/a.module.css",
"length": 9,
"start": {
"column": 8,
"line": 2,
},
"text": "\`__proto__\` is not allowed as names.",
},
{
"category": "error",
"fileName": "<rootDir>/a.module.css",
"length": 9,
"start": {
"column": 28,
"line": 2,
},
"text": "\`__proto__\` is not allowed as names.",
},
]
`);
});
test('report diagnostics for "default" name when namedExports is true', async () => {
const iff = await createIFF({
'a.module.css': dedent`
.default { color: red; }
@value default, valid as default from './b.module.css';
`,
'b.module.css': dedent`
@value default: red;
@value valid: red;
`,
});
const check = prepareChecker({ config: fakeConfig({ namedExports: true }) });
const diagnostics = check(readAndParseCSSModule(iff.paths['a.module.css'])!);
expect(formatDiagnostics(diagnostics, iff.rootDir)).toMatchInlineSnapshot(`
[
{
"category": "error",
"fileName": "<rootDir>/a.module.css",
"length": 7,
"start": {
"column": 2,
"line": 1,
},
"text": "\`default\` is not allowed as names when \`cmkOptions.namedExports\` is set to \`true\`.",
},
{
"category": "error",
"fileName": "<rootDir>/a.module.css",
"length": 7,
"start": {
"column": 8,
"line": 2,
},
"text": "\`default\` is not allowed as names when \`cmkOptions.namedExports\` is set to \`true\`.",
},
{
"category": "error",
"fileName": "<rootDir>/a.module.css",
"length": 7,
"start": {
"column": 26,
"line": 2,
},
"text": "\`default\` is not allowed as names when \`cmkOptions.namedExports\` is set to \`true\`.",
},
]
`);
});
test('report diagnostics for backslash in name', 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.",
},
]
`);
});
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
47 changes: 1 addition & 46 deletions packages/core/src/checker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { CMKConfig } from './config.js';
import type {
CSSModule,
Diagnostic,
Expand All @@ -8,28 +7,18 @@ import type {
MatchesPattern,
Resolver,
} from './type.js';
import { isURLSpecifier, type TokenNameViolation, validateTokenName } from './util.js';
import { isURLSpecifier } from './util.js';

export interface CheckerArgs {
config: CMKConfig;
getExportRecord: (cssModule: CSSModule) => ExportRecord;
matchesPattern: MatchesPattern;
resolver: Resolver;
getCSSModule: (path: string) => CSSModule | undefined;
}

export function checkCSSModule(cssModule: CSSModule, args: CheckerArgs): Diagnostic[] {
const { config } = args;
const diagnostics: Diagnostic[] = [];

for (const token of cssModule.localTokens) {
// Reject special names as they may break .d.ts files
const violation = validateTokenName(token.name, { namedExports: config.namedExports });
if (violation) {
diagnostics.push(createTokenNameDiagnostic(cssModule, token.loc, violation));
}
}

for (const tokenImporter of cssModule.tokenImporters) {
if (isURLSpecifier(tokenImporter.from)) continue;
const from = args.resolver(tokenImporter.from, { request: cssModule.fileName });
Expand All @@ -49,16 +38,6 @@ export function checkCSSModule(cssModule: CSSModule, args: CheckerArgs): Diagnos
createModuleHasNoExportedTokenDiagnostic(cssModule, tokenImporter.from, entry.name, entry.loc),
);
}
const nameViolation = validateTokenName(entry.name, { namedExports: config.namedExports });
if (nameViolation) {
diagnostics.push(createTokenNameDiagnostic(cssModule, entry.loc, nameViolation));
}
if (entry.localName) {
const localNameViolation = validateTokenName(entry.localName, { namedExports: config.namedExports });
if (localNameViolation) {
diagnostics.push(createTokenNameDiagnostic(cssModule, entry.localLoc!, localNameViolation));
}
}
}
}
}
Expand Down Expand Up @@ -90,30 +69,6 @@ export function checkCSSModule(cssModule: CSSModule, args: CheckerArgs): Diagnos
return diagnostics;
}

function createTokenNameDiagnostic(cssModule: CSSModule, loc: Location, violation: TokenNameViolation): Diagnostic {
let text: string;
switch (violation) {
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.`;
break;
default:
throw new Error('unreachable: unknown TokenNameViolation');
}
return {
text,
category: 'error',
file: { fileName: cssModule.fileName, text: cssModule.text },
start: { line: loc.start.line, column: loc.start.column },
length: loc.end.offset - loc.start.offset,
};
}

function createCannotImportModuleDiagnostic(cssModule: CSSModule, from: string, fromLoc: Location): Diagnostic {
return {
text: `Cannot import module '${from}'`,
Expand Down
Loading
Loading