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/great-ads-check.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
---

fix: `default` is not allowed as names when `namedExports` is `true`
7 changes: 7 additions & 0 deletions .changeset/honest-onions-walk.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
---

fix: `__proto__` is not allowed as names
2 changes: 1 addition & 1 deletion packages/codegen/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function runCMK(project: string, clean: boolean, logger: Logger): P
const exportBuilder = createExportBuilder({ getCSSModule, matchesPattern, resolver });
const semanticDiagnostics: Diagnostic[] = [];
for (const { cssModule } of parseResults) {
const diagnostics = checkCSSModule(cssModule, exportBuilder, matchesPattern, resolver, getCSSModule);
const diagnostics = checkCSSModule(cssModule, config, exportBuilder, matchesPattern, resolver, getCSSModule);
semanticDiagnostics.push(...diagnostics);
}

Expand Down
168 changes: 167 additions & 1 deletion packages/core/src/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createExportBuilder } from './export-builder.js';
import { resolve } from './path.js';
import { createResolver } from './resolver.js';
import { fakeCSSModule } from './test/css-module.js';
import { fakeConfig } from './test/faker.js';
import {
fakeAtImportTokenImporter,
fakeAtValueTokenImporter,
Expand All @@ -15,14 +16,15 @@ import type { CSSModule, Location } from './type.js';
const resolver = createResolver({}, undefined);

function prepareCheckerArgs<const T extends CSSModule[]>(cssModules: T) {
const config = fakeConfig();
const getCSSModule = (path: string) => cssModules.find((m) => resolve(m.fileName) === resolve(path));
const matchesPattern = (path: string) => path.endsWith('.module.css');
const exportBuilder = createExportBuilder({
getCSSModule,
matchesPattern,
resolver,
});
return { cssModules, exportBuilder, matchesPattern, resolver, getCSSModule };
return { cssModules, config, exportBuilder, matchesPattern, resolver, getCSSModule };
}

function fakeLoc({ column }: { column: number }): Location {
Expand Down Expand Up @@ -61,6 +63,7 @@ describe('checkCSSModule', () => {
]);
const diagnostics = checkCSSModule(
args.cssModules[0],
args.config,
args.exportBuilder,
args.matchesPattern,
args.resolver,
Expand Down Expand Up @@ -123,6 +126,165 @@ describe('checkCSSModule', () => {
]
`);
});
test('report diagnostics for "__proto__" name', () => {
const args = prepareCheckerArgs([
fakeCSSModule({
fileName: '/a.module.css',
localTokens: [fakeToken({ name: '__proto__', loc: fakeLoc({ column: 1 }) })],
tokenImporters: [
fakeAtValueTokenImporter({
from: './b.module.css',
fromLoc: fakeLoc({ column: 2 }),
values: [
fakeAtValueTokenImporterValue({ name: '__proto__', loc: fakeLoc({ column: 3 }) }),
fakeAtValueTokenImporterValue({
name: 'valid',
loc: fakeLoc({ column: 4 }),
localName: '__proto__',
localLoc: fakeLoc({ column: 5 }),
}),
],
}),
],
}),
fakeCSSModule({
fileName: '/b.module.css',
localTokens: [fakeToken({ name: '__proto__' }), fakeToken({ name: 'valid' })],
}),
]);

const diagnostics = checkCSSModule(
args.cssModules[0],
args.config,
args.exportBuilder,
args.matchesPattern,
args.resolver,
args.getCSSModule,
);
expect(diagnostics).toMatchInlineSnapshot(`
[
{
"category": "error",
"file": {
"fileName": "/a.module.css",
"text": "",
},
"length": 0,
"start": {
"column": 1,
"line": 1,
},
"text": "\`__proto__\` is not allowed as names.",
},
{
"category": "error",
"file": {
"fileName": "/a.module.css",
"text": "",
},
"length": 0,
"start": {
"column": 3,
"line": 1,
},
"text": "\`__proto__\` is not allowed as names.",
},
{
"category": "error",
"file": {
"fileName": "/a.module.css",
"text": "",
},
"length": 0,
"start": {
"column": 5,
"line": 1,
},
"text": "\`__proto__\` is not allowed as names.",
},
]
`);
});
test('report diagnostics for "default" name when namedExports is true', () => {
const args = prepareCheckerArgs([
fakeCSSModule({
fileName: '/a.module.css',
localTokens: [fakeToken({ name: 'default', loc: fakeLoc({ column: 1 }) })],
tokenImporters: [
fakeAtValueTokenImporter({
from: './b.module.css',
fromLoc: fakeLoc({ column: 2 }),
values: [
fakeAtValueTokenImporterValue({ name: 'default', loc: fakeLoc({ column: 3 }) }),
fakeAtValueTokenImporterValue({
name: 'valid',
loc: fakeLoc({ column: 4 }),
localName: 'default',
localLoc: fakeLoc({ column: 5 }),
}),
],
}),
],
}),
fakeCSSModule({
fileName: '/b.module.css',
localTokens: [fakeToken({ name: 'default' }), fakeToken({ name: 'valid' })],
}),
]);
args.config.namedExports = true;

const diagnostics = checkCSSModule(
args.cssModules[0],
args.config,
args.exportBuilder,
args.matchesPattern,
args.resolver,
args.getCSSModule,
);
expect(diagnostics).toMatchInlineSnapshot(`
[
{
"category": "error",
"file": {
"fileName": "/a.module.css",
"text": "",
},
"length": 0,
"start": {
"column": 1,
"line": 1,
},
"text": "\`default\` is not allowed as names when \`cmkOptions.namedExports\` is set to \`true\`.",
},
{
"category": "error",
"file": {
"fileName": "/a.module.css",
"text": "",
},
"length": 0,
"start": {
"column": 3,
"line": 1,
},
"text": "\`default\` is not allowed as names when \`cmkOptions.namedExports\` is set to \`true\`.",
},
{
"category": "error",
"file": {
"fileName": "/a.module.css",
"text": "",
},
"length": 0,
"start": {
"column": 5,
"line": 1,
},
"text": "\`default\` is not allowed as names when \`cmkOptions.namedExports\` is set to \`true\`.",
},
]
`);
});
test('report diagnostics for non-existing module', () => {
const args = prepareCheckerArgs([
fakeCSSModule({
Expand All @@ -139,6 +301,7 @@ describe('checkCSSModule', () => {
]);
const diagnostics = checkCSSModule(
args.cssModules[0],
args.config,
args.exportBuilder,
args.matchesPattern,
args.resolver,
Expand Down Expand Up @@ -197,6 +360,7 @@ describe('checkCSSModule', () => {
]);
const diagnostics = checkCSSModule(
args.cssModules[0],
args.config,
args.exportBuilder,
args.matchesPattern,
args.resolver,
Expand Down Expand Up @@ -229,6 +393,7 @@ describe('checkCSSModule', () => {
]);
const diagnostics = checkCSSModule(
args.cssModules[0],
args.config,
args.exportBuilder,
args.matchesPattern,
() => undefined, // Simulate unresolvable module
Expand All @@ -251,6 +416,7 @@ describe('checkCSSModule', () => {
]);
const diagnostics = checkCSSModule(
args.cssModules[0],
args.config,
args.exportBuilder,
(path: string) => path === '/a.module.css', // Only match the current module
args.resolver,
Expand Down
44 changes: 44 additions & 0 deletions packages/core/src/checker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CMKConfig } from './config.js';
import type {
AtValueTokenImporter,
AtValueTokenImporterValue,
Expand All @@ -11,8 +12,10 @@ import type {
} from './type.js';
import { isValidAsJSIdentifier } from './util.js';

// eslint-disable-next-line max-params, complexity
export function checkCSSModule(
cssModule: CSSModule,
config: CMKConfig,
exportBuilder: ExportBuilder,
matchesPattern: MatchesPattern,
resolver: Resolver,
Expand All @@ -21,9 +24,16 @@ export function checkCSSModule(
const diagnostics: Diagnostic[] = [];

for (const token of cssModule.localTokens) {
// Reject special names as they may break .d.ts files
if (!isValidAsJSIdentifier(token.name)) {
diagnostics.push(createInvalidNameAsJSIdentifiersDiagnostic(cssModule, token.loc));
}
if (token.name === '__proto__') {
diagnostics.push(createProtoIsNotAllowedDiagnostic(cssModule, token.loc));
}
if (config.namedExports && token.name === 'default') {
diagnostics.push(createDefaultIsNotAllowedDiagnostic(cssModule, token.loc));
}
}

for (const tokenImporter of cssModule.tokenImporters) {
Expand All @@ -47,6 +57,20 @@ export function checkCSSModule(
if (value.localName && !isValidAsJSIdentifier(value.localName)) {
diagnostics.push(createInvalidNameAsJSIdentifiersDiagnostic(cssModule, value.localLoc!));
}
if (value.name === '__proto__') {
diagnostics.push(createProtoIsNotAllowedDiagnostic(cssModule, value.loc));
}
if (value.localName === '__proto__') {
diagnostics.push(createProtoIsNotAllowedDiagnostic(cssModule, value.localLoc!));
}
if (config.namedExports) {
if (value.name === 'default') {
diagnostics.push(createDefaultIsNotAllowedDiagnostic(cssModule, value.loc));
}
if (value.localName === 'default') {
diagnostics.push(createDefaultIsNotAllowedDiagnostic(cssModule, value.localLoc!));
}
}
}
}
}
Expand Down Expand Up @@ -86,3 +110,23 @@ function createInvalidNameAsJSIdentifiersDiagnostic(cssModule: CSSModule, loc: L
length: loc.end.offset - loc.start.offset,
};
}

function createProtoIsNotAllowedDiagnostic(cssModule: CSSModule, loc: Location): Diagnostic {
return {
text: `\`__proto__\` is not allowed as names.`,
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 createDefaultIsNotAllowedDiagnostic(cssModule: CSSModule, loc: Location): Diagnostic {
return {
text: `\`default\` is not allowed as names when \`cmkOptions.namedExports\` is set to \`true\`.`,
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,
};
}
47 changes: 47 additions & 0 deletions packages/core/src/dts-creator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,53 @@ describe('createDts', () => {
"
`);
});
test('does not create types for `__proto__`', () => {
expect(
createDts(
fakeCSSModule({
localTokens: [fakeToken({ name: '__proto__', loc: fakeLoc(0) })],
}),
host,
options,
).text,
).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
};
export default styles;
"
`);
});
test('does not create types for `default` when `namedExports` is true', () => {
expect(
createDts(
fakeCSSModule({
localTokens: [fakeToken({ name: 'default', loc: fakeLoc(0) })],
}),
host,
{ ...options, namedExports: true },
).text,
).toMatchInlineSnapshot(`
"// @ts-nocheck
"
`);
expect(
createDts(
fakeCSSModule({
localTokens: [fakeToken({ name: 'default', loc: fakeLoc(0) })],
}),
host,
{ ...options, namedExports: false },
).text,
).toMatchInlineSnapshot(`
"// @ts-nocheck
declare const styles = {
default: '' as readonly string,
};
export default styles;
"
`);
});
test('creates d.ts file with named exports', () => {
expect(
createDts(
Expand Down
Loading