diff --git a/.changeset/allow-non-js-ident-named-exports.md b/.changeset/allow-non-js-ident-named-exports.md new file mode 100644 index 00000000..6adcca7a --- /dev/null +++ b/.changeset/allow-non-js-ident-named-exports.md @@ -0,0 +1,6 @@ +--- +'@css-modules-kit/core': minor +'@css-modules-kit/ts-plugin': minor +--- + +feat(core, ts-plugin): support non-JavaScript identifier token in named export diff --git a/examples/2-named-exports/generated/src/a.module.css.d.ts b/examples/2-named-exports/generated/src/a.module.css.d.ts index fba7f2bc..5c07a1d6 100644 --- a/examples/2-named-exports/generated/src/a.module.css.d.ts +++ b/examples/2-named-exports/generated/src/a.module.css.d.ts @@ -1,10 +1,13 @@ // @ts-nocheck -export var a_1: string; -export var a_2: string; -export var a_2: string; -export var a_3: string; +var _token_0: string; +export { _token_0 as 'a_1' }; +var _token_1: string; +var _token_1: string; +export { _token_1 as 'a_2' }; +var _token_2: string; +export { _token_2 as 'a_3' }; export * from './b.module.css'; export { - c_1, - c_2 as c_alias, + 'c_1' as 'c_1', + 'c_2' as 'c_alias', } from './c.module.css'; diff --git a/examples/2-named-exports/generated/src/b.module.css.d.ts b/examples/2-named-exports/generated/src/b.module.css.d.ts index bf28ed09..f692fdba 100644 --- a/examples/2-named-exports/generated/src/b.module.css.d.ts +++ b/examples/2-named-exports/generated/src/b.module.css.d.ts @@ -1,2 +1,3 @@ // @ts-nocheck -export var b_1: string; +var _token_0: string; +export { _token_0 as 'b_1' }; diff --git a/examples/2-named-exports/generated/src/c.module.css.d.ts b/examples/2-named-exports/generated/src/c.module.css.d.ts index 07e44be9..09acb553 100644 --- a/examples/2-named-exports/generated/src/c.module.css.d.ts +++ b/examples/2-named-exports/generated/src/c.module.css.d.ts @@ -1,3 +1,5 @@ // @ts-nocheck -export var c_1: string; -export var c_2: string; +var _token_0: string; +export { _token_0 as 'c_1' }; +var _token_1: string; +export { _token_1 as 'c_2' }; diff --git a/packages/core/src/checker.test.ts b/packages/core/src/checker.test.ts index 02bed516..a63f0f7b 100644 --- a/packages/core/src/checker.test.ts +++ b/packages/core/src/checker.test.ts @@ -36,79 +36,6 @@ function prepareChecker(args?: Partial): Checker { } describe('checkCSSModule', () => { - test('do not report diagnostics for invalid name as js identifier when namedExports is false', async () => { - const iff = await createIFF({ - 'a.module.css': dedent` - .a-1 { color: red; } - @value b-1, b-2 as a-2 from './b.module.css'; - `, - 'b.module.css': dedent` - @value b-1: red; - @value b-2: red; - `, - }); - const check = prepareChecker(); - const diagnostics = check(readAndParseCSSModule(iff.paths['a.module.css'])!); - expect(formatDiagnostics(diagnostics, iff.rootDir)).toMatchInlineSnapshot(`[]`); - }); - test('report diagnostics for invalid name as js identifier when namedExports is true', async () => { - const iff = await createIFF({ - 'a.module.css': dedent` - .a-1 { color: red; } - @value b-1, b-2 as a-2 from './b.module.css'; - `, - 'b.module.css': dedent` - @value b-1: red; - @value b-2: 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": "/a.module.css", - "length": 3, - "start": { - "column": 2, - "line": 1, - }, - "text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.", - }, - { - "category": "error", - "fileName": "/a.module.css", - "length": 3, - "start": { - "column": 8, - "line": 2, - }, - "text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.", - }, - { - "category": "error", - "fileName": "/a.module.css", - "length": 3, - "start": { - "column": 13, - "line": 2, - }, - "text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.", - }, - { - "category": "error", - "fileName": "/a.module.css", - "length": 3, - "start": { - "column": 20, - "line": 2, - }, - "text": "Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.", - }, - ] - `); - }); test('report diagnostics for "__proto__" name', async () => { const iff = await createIFF({ 'a.module.css': dedent` @@ -205,7 +132,7 @@ describe('checkCSSModule', () => { ] `); }); - test('report diagnostics for backslash in name when namedExports is false', async () => { + 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({ @@ -225,7 +152,7 @@ describe('checkCSSModule', () => { "column": 2, "line": 1, }, - "text": "Backslash (\\) is not allowed in names when \`cmkOptions.namedExports\` is set to \`false\`.", + "text": "Backslash (\\) is not allowed in names.", }, ] `); diff --git a/packages/core/src/checker.ts b/packages/core/src/checker.ts index c81d9f47..3a545497 100644 --- a/packages/core/src/checker.ts +++ b/packages/core/src/checker.ts @@ -68,9 +68,6 @@ export function checkCSSModule(cssModule: CSSModule, args: CheckerArgs): Diagnos function createTokenNameDiagnostic(cssModule: CSSModule, loc: Location, violation: TokenNameViolation): Diagnostic { let text: string; switch (violation) { - case 'invalid-js-identifier': - text = `Token names must be valid JavaScript identifiers when \`cmkOptions.namedExports\` is set to \`true\`.`; - break; case 'proto-not-allowed': text = `\`__proto__\` is not allowed as names.`; break; @@ -78,7 +75,7 @@ function createTokenNameDiagnostic(cssModule: CSSModule, loc: Location, violatio 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 when \`cmkOptions.namedExports\` is set to \`false\`.`; + text = `Backslash (\\) is not allowed in names.`; break; default: throw new Error('unreachable: unknown TokenNameViolation'); diff --git a/packages/core/src/dts-generator.test.ts b/packages/core/src/dts-generator.test.ts index b1d6f9c3..3d794973 100644 --- a/packages/core/src/dts-generator.test.ts +++ b/packages/core/src/dts-generator.test.ts @@ -95,22 +95,26 @@ describe('generateDts', () => { 'test.module.css': dedent` .local1 { color: red; } .local2 { color: red; } + .local2 { color: red; } @import './a.module.css'; @value imported1, imported2 as aliasedImported2 from './b.module.css'; `, }); expect(generateDts(readAndParseCSSModule(iff.paths['test.module.css'])!, { ...options, namedExports: true }).text) .toMatchInlineSnapshot(` - "// @ts-nocheck - export var local1: string; - export var local2: string; - export * from './a.module.css'; - export { - imported1, - imported2 as aliasedImported2, - } from './b.module.css'; - " - `); + "// @ts-nocheck + var _token_0: string; + export { _token_0 as 'local1' }; + var _token_1: string; + var _token_1: string; + export { _token_1 as 'local2' }; + export * from './a.module.css'; + export { + 'imported1' as 'imported1', + 'imported2' as 'aliasedImported2', + } from './b.module.css'; + " + `); }); test('exports styles as default when `namedExports` and `forTsPlugin` are true, but `prioritizeNamedImports` is false', async () => { const iff = await createIFF({ @@ -125,7 +129,8 @@ describe('generateDts', () => { }).text, ).toMatchInlineSnapshot(` "// @ts-nocheck - export var local1: string; + var _token_0: string; + export { _token_0 as 'local1' }; declare const styles: {}; export default styles; " diff --git a/packages/core/src/dts-generator.ts b/packages/core/src/dts-generator.ts index c8c6870e..d0f29403 100644 --- a/packages/core/src/dts-generator.ts +++ b/packages/core/src/dts-generator.ts @@ -115,7 +115,12 @@ function generateNamedExportsDts( tokenImporters: TokenImporter[], options: GenerateDtsOptions, ): { text: string; mapping: CodeMapping; linkedCodeMapping: LinkedCodeMapping } { - const mapping: CodeMapping = { sourceOffsets: [], lengths: [], generatedOffsets: [] }; + const mapping: Required = { + sourceOffsets: [], + lengths: [], + generatedOffsets: [], + generatedLengths: [], + }; const linkedCodeMapping: LinkedCodeMapping = { sourceOffsets: [], lengths: [], @@ -130,31 +135,54 @@ function generateNamedExportsDts( // Therefore, `@ts-nocheck` is added to the generated type definition file. let text = `// @ts-nocheck\n`; - for (const token of localTokens) { - /** - * The mapping is created as follows: - * a.module.css: - * 1 | .a_1 { color: red; } - * | ^ mapping.sourceOffsets[0] - * | - * 2 | .a_2 { color: blue; } - * | ^ mapping.sourceOffsets[1] - * | - * - * a.module.css.d.ts: - * 1 | // @ts-nocheck - * 2 | export var a_1: string; - * | ^ mapping.generatedOffsets[0] - * | - * 3 | export var a_2: string; - * | ^ mapping.generatedOffsets[1] - */ + /** + * The mapping is created as follows: + * a.module.css: + * 1 | .a_1 { color: red; } + * | ^ mapping.sourceOffsets[0], mapping.sourceOffsets[2] + * | + * 2 | .a_1 { color: blue; } + * | ^ mapping.sourceOffsets[1] + * | + * + * a.module.css.d.ts: + * 1 | // @ts-nocheck + * 2 | var _token_0: string; + * | ^ mapping.generatedOffsets[0] + * | + * 3 | var _token_0: string; + * | ^ mapping.generatedOffsets[1] + * | + * 4 | export { _token_0 as 'a_1' }; + * | ^ mapping.generatedOffsets[2] + * | ^ ^ linkedCodeMapping.generatedOffsets[0] + * | ^ linkedCodeMapping.sourceOffsets[0] + */ - text += `export var `; - mapping.sourceOffsets.push(token.loc.start.offset); + const groupedLocalTokens = Object.groupBy(localTokens, (token) => token.name); + for (const [index, [name, tokens]] of Object.entries(groupedLocalTokens).entries()) { + if (tokens === undefined || tokens.length === 0) continue; + const internalName = `_token_${index}`; + for (const token of tokens) { + text += `var `; + mapping.sourceOffsets.push(token.loc.start.offset); + mapping.lengths.push(name.length); + mapping.generatedOffsets.push(text.length); + mapping.generatedLengths.push(internalName.length); + text += `${internalName}: string;\n`; + } + text += `export { `; + linkedCodeMapping.sourceOffsets.push(text.length); + linkedCodeMapping.lengths.push(internalName.length); + text += `${internalName} as `; + linkedCodeMapping.generatedOffsets.push(text.length); + linkedCodeMapping.generatedLengths.push(name.length + 2); + text += `'`; + mapping.sourceOffsets.push(tokens[0]!.loc.start.offset); + mapping.lengths.push(name.length); mapping.generatedOffsets.push(text.length); - mapping.lengths.push(token.name.length); - text += `${token.name}: string;\n`; + mapping.generatedLengths.push(name.length); + text += `${name}' };\n`; } for (const tokenImporter of tokenImporters) { if (tokenImporter.type === 'import') { @@ -183,76 +211,68 @@ function generateNamedExportsDts( mapping.sourceOffsets.push(tokenImporter.fromLoc.start.offset - 1); mapping.lengths.push(tokenImporter.from.length + 2); mapping.generatedOffsets.push(text.length); + mapping.generatedLengths.push(tokenImporter.from.length + 2); text += `'${tokenImporter.from}';\n`; } else { /** * The mapping is created as follows: * a.module.css: - * 1 | @value b_1, b_2 from './b.module.css'; - * | ^ ^ ^ mapping.sourceOffsets[2] + * 1 | @value b_1, b_2 as aliased_b_2 from './b.module.css'; + * | ^ ^ ^ ^ mapping.sourceOffsets[3] + * | ^ ^ ^ mapping.sourceOffsets[2] * | ^ ^ mapping.sourceOffsets[1] * | ^ mapping.sourceOffsets[0] * | - * 2 | @value c_1 as aliased_c_1 from './c.module.css'; - * | ^ ^ ^ mapping.sourceOffsets[5] - * | ^ ^ mapping.sourceOffsets[4] - * | ^ mapping.sourceOffsets[3] - * | * * a.module.css.d.ts: * 1 | // @ts-nocheck * 2 | export { - * 3 | b_1, - * | ^ mapping.generatedOffsets[0] + * 3 | 'b_1' as 'b_1', + * | ^ ^^ mapping.generatedOffsets[0] + * | ^ ^ linkedCodeMapping.generatedOffsets[0] + * | ^ linkedCodeMapping.sourceOffsets[0] * | - * 4 | b_2, - * | ^ mapping.generatedOffsets[1] + * 4 | 'b_2' as 'aliased_b_2', + * | ^^ ^^ mapping.generatedOffsets[2] + * | ^^ ^ linkedCodeMapping.generatedOffsets[1] + * | ^^ mapping.generatedOffsets[1] + * | ^ linkedCodeMapping.sourceOffsets[1] * | * 5 | } from './b.module.css'; - * | ^ mapping.generatedOffsets[2] - * | - * 6 | export { - * 7 | c_1 as aliased_c_1, - * | ^ ^ mapping.generatedOffsets[4], linkedCodeMapping.sourceOffsets[0] - * | ^ mapping.generatedOffsets[3], linkedCodeMapping.generatedOffsets[0] - * | - * 8 | } from './c.module.css'; - * | ^ mapping.generatedOffsets[5] + * | ^ mapping.generatedOffsets[3] * * NOTE: Not only the specifier but also the surrounding quotes are included in the mapping. - * NOTE: linkedCodeMapping is only generated for tokens that have a `localName` (i.e., aliased tokens). */ text += `export {\n`; - // oxlint-disable-next-line no-loop-func - tokenImporter.values.forEach((value) => { + for (const value of tokenImporter.values) { const localName = value.localName ?? value.name; const localLoc = value.localLoc ?? value.loc; text += ` `; + linkedCodeMapping.sourceOffsets.push(text.length); + linkedCodeMapping.lengths.push(value.name.length + 2); + text += `'`; if ('localName' in value) { mapping.sourceOffsets.push(value.loc.start.offset); mapping.lengths.push(value.name.length); mapping.generatedOffsets.push(text.length); - linkedCodeMapping.generatedOffsets.push(text.length); - linkedCodeMapping.generatedLengths.push(value.name.length); - text += `${value.name} as `; - mapping.sourceOffsets.push(localLoc.start.offset); - mapping.lengths.push(localName.length); - mapping.generatedOffsets.push(text.length); - linkedCodeMapping.sourceOffsets.push(text.length); - linkedCodeMapping.lengths.push(localName.length); - text += `${localName},\n`; - } else { - mapping.sourceOffsets.push(value.loc.start.offset); - mapping.lengths.push(value.name.length); - mapping.generatedOffsets.push(text.length); - text += `${value.name},\n`; + mapping.generatedLengths.push(value.name.length); } - }); + text += `${value.name}' as `; + linkedCodeMapping.generatedOffsets.push(text.length); + linkedCodeMapping.generatedLengths.push(localName.length + 2); + text += `'`; + mapping.sourceOffsets.push(localLoc.start.offset); + mapping.lengths.push(localName.length); + mapping.generatedOffsets.push(text.length); + mapping.generatedLengths.push(localName.length); + text += `${localName}',\n`; + } text += `} from `; mapping.sourceOffsets.push(tokenImporter.fromLoc.start.offset - 1); mapping.lengths.push(tokenImporter.from.length + 2); mapping.generatedOffsets.push(text.length); + mapping.generatedLengths.push(tokenImporter.from.length + 2); text += `'${tokenImporter.from}';\n`; } } diff --git a/packages/core/src/util.test.ts b/packages/core/src/util.test.ts index c4f2b3ea..fe48c72c 100644 --- a/packages/core/src/util.test.ts +++ b/packages/core/src/util.test.ts @@ -17,17 +17,9 @@ describe('validateTokenName', () => { test('returns undefined for default when namedExports is false', () => { expect(validateTokenName('default', { namedExports: false })).toBe(undefined); }); - test('returns "invalid-js-identifier" for invalid JS identifier when namedExports is true', () => { - expect(validateTokenName('a-1', { namedExports: true })).toBe('invalid-js-identifier'); - }); - test('returns undefined for invalid JS identifier when namedExports is false', () => { - expect(validateTokenName('a-1', { namedExports: false })).toBe(undefined); - }); - test('returns "backslash-not-allowed" for backslash when namedExports is false', () => { + test('returns "backslash-not-allowed" for backslash', () => { expect(validateTokenName('a\\b', { namedExports: false })).toBe('backslash-not-allowed'); - }); - test('returns "invalid-js-identifier" for backslash when namedExports is true', () => { - expect(validateTokenName('a\\b', { namedExports: true })).toBe('invalid-js-identifier'); + expect(validateTokenName('a\\b', { namedExports: true })).toBe('backslash-not-allowed'); }); }); diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts index 27a6d035..f05a838a 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/util.ts @@ -2,11 +2,8 @@ export function isPosixRelativePath(path: string): boolean { return path.startsWith(`./`) || path.startsWith(`../`); } -const JS_IDENTIFIER_PATTERN = /^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u; - /** The type of token name violation. */ export type TokenNameViolation = - | 'invalid-js-identifier' // Invalid as a JavaScript identifier | 'proto-not-allowed' // `__proto__` is not allowed | 'default-not-allowed' // `default` is not allowed when namedExports is true | 'backslash-not-allowed'; // Backslash (`\`) is not allowed @@ -23,12 +20,8 @@ export interface ValidateTokenNameOptions { */ export function validateTokenName(name: string, options: ValidateTokenNameOptions): TokenNameViolation | undefined { if (name === '__proto__') return 'proto-not-allowed'; - if (options.namedExports) { - if (name === 'default') return 'default-not-allowed'; - if (!JS_IDENTIFIER_PATTERN.test(name)) return 'invalid-js-identifier'; - } else { - if (name.includes('\\')) return 'backslash-not-allowed'; - } + if (options.namedExports && name === 'default') return 'default-not-allowed'; + if (name.includes('\\')) return 'backslash-not-allowed'; return undefined; } diff --git a/packages/ts-plugin/e2e-test/feature/find-all-references.test.ts b/packages/ts-plugin/e2e-test/feature/find-all-references.test.ts index 543e08df..e4ff755f 100644 --- a/packages/ts-plugin/e2e-test/feature/find-all-references.test.ts +++ b/packages/ts-plugin/e2e-test/feature/find-all-references.test.ts @@ -127,149 +127,147 @@ describe.each([ await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }], }); - test.each( - [ - { - name: 'styles in index.ts', - file: iff.paths['index.ts'], - line: 1, - offset: stylesOffset, - expected: [ - { - file: formatPath(iff.paths['index.ts']), - start: { line: 1, offset: stylesOffset }, - end: { line: 1, offset: stylesOffset + 6 }, - }, - { file: formatPath(iff.paths['index.ts']), start: { line: 2, offset: 1 }, end: { line: 2, offset: 7 } }, - { file: formatPath(iff.paths['index.ts']), start: { line: 3, offset: 1 }, end: { line: 3, offset: 7 } }, - { file: formatPath(iff.paths['index.ts']), start: { line: 4, offset: 1 }, end: { line: 4, offset: 7 } }, - { file: formatPath(iff.paths['index.ts']), start: { line: 5, offset: 1 }, end: { line: 5, offset: 7 } }, - { file: formatPath(iff.paths['index.ts']), start: { line: 6, offset: 1 }, end: { line: 6, offset: 7 } }, - ], - }, - { - name: "'./a.module.css' in index.ts", - file: iff.paths['index.ts'], - line: 1, - offset: specifierOffset, - expected: [ - { - file: formatPath(iff.paths['index.ts']), - start: { line: 1, offset: specifierOffset + 1 }, - end: { line: 1, offset: specifierOffset + 15 }, - }, - ], - }, - { - name: "'./b.module.css' in a.module.css", - file: iff.paths['a.module.css'], - line: 1, - offset: 9, - expected: [ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 1, offset: 10 }, - end: { line: 1, offset: 24 }, - }, - ], - }, - { - name: "'./c.module.css' in a.module.css", - file: iff.paths['a.module.css'], - line: 2, - offset: 33, - expected: [ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 2, offset: 34 }, - end: { line: 2, offset: 48 }, - }, - ], - }, - { - name: 'a_1 in index.ts', - file: a_1_in_index_ts.file, - ...a_1_in_index_ts.start, - expected: [a_1_in_index_ts, a_1_1_in_a_module_css, a_1_2_in_a_module_css], - }, - { - name: 'a_1 in a.module.css', - file: a_1_1_in_a_module_css.file, - ...a_1_1_in_a_module_css.start, - expected: [a_1_in_index_ts, a_1_1_in_a_module_css, a_1_2_in_a_module_css], - }, - !namedExports && { - name: 'a-2 in index.ts', - file: a_2_in_index_ts.file, - ...a_2_in_index_ts.start, - expected: [a_2_in_index_ts, a_2_in_a_module_css], - }, - !namedExports && { - name: 'a-2 in a.module.css', - file: a_2_in_a_module_css.file, - ...a_2_in_a_module_css.start, - expected: [a_2_in_index_ts, a_2_in_a_module_css], - }, - { - name: 'b_1 in index.ts', - file: b_1_in_index_ts.file, - ...b_1_in_index_ts.start, - expected: [b_1_in_index_ts, b_1_in_b_module_css], - }, - { - name: 'b_1 in b.module.css', - file: b_1_in_b_module_css.file, - ...b_1_in_b_module_css.start, - expected: [b_1_in_index_ts, b_1_in_b_module_css], - }, - { - name: 'c_1 in index.ts', - file: c_1_in_index_ts.file, - ...c_1_in_index_ts.start, - expected: [c_1_in_index_ts, c_1_in_a_module_css, c_1_in_c_module_css], - }, - { - name: 'c_1 in a.module.css', - file: c_1_in_a_module_css.file, - ...c_1_in_a_module_css.start, - expected: [c_1_in_index_ts, c_1_in_a_module_css, c_1_in_c_module_css], - }, - { - name: 'c_1 in c.module.css', - file: c_1_in_c_module_css.file, - ...c_1_in_c_module_css.start, - expected: [c_1_in_index_ts, c_1_in_a_module_css, c_1_in_c_module_css], - }, - { - name: 'c_alias in index.ts', - file: c_alias_in_index_ts.file, - ...c_alias_in_index_ts.start, - // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: - // expected: [c_alias_in_index_ts, c_alias_in_a_module_css], - expected: [c_alias_in_index_ts, c_alias_in_a_module_css, c_2_in_a_module_css, c_2_in_c_module_css], - }, - { - name: 'c_alias in a.module.css', - file: c_alias_in_a_module_css.file, - ...c_alias_in_a_module_css.start, - // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: - // expected: [c_alias_in_index_ts, c_alias_in_a_module_css], - expected: [c_alias_in_index_ts, c_alias_in_a_module_css, c_2_in_a_module_css, c_2_in_c_module_css], - }, - { - name: 'c_2 in a.module.css', - file: c_2_in_a_module_css.file, - ...c_2_in_a_module_css.start, - expected: [c_alias_in_index_ts, c_alias_in_a_module_css, c_2_in_a_module_css, c_2_in_c_module_css], - }, - { - name: 'c_2 in c.module.css', - file: c_2_in_c_module_css.file, - ...c_2_in_c_module_css.start, - expected: [c_alias_in_index_ts, c_alias_in_a_module_css, c_2_in_a_module_css, c_2_in_c_module_css], - }, - ].filter((c) => c !== false), - )('Find All References for $name', async ({ file, line, offset, expected }) => { + test.each([ + { + name: 'styles in index.ts', + file: iff.paths['index.ts'], + line: 1, + offset: stylesOffset, + expected: [ + { + file: formatPath(iff.paths['index.ts']), + start: { line: 1, offset: stylesOffset }, + end: { line: 1, offset: stylesOffset + 6 }, + }, + { file: formatPath(iff.paths['index.ts']), start: { line: 2, offset: 1 }, end: { line: 2, offset: 7 } }, + { file: formatPath(iff.paths['index.ts']), start: { line: 3, offset: 1 }, end: { line: 3, offset: 7 } }, + { file: formatPath(iff.paths['index.ts']), start: { line: 4, offset: 1 }, end: { line: 4, offset: 7 } }, + { file: formatPath(iff.paths['index.ts']), start: { line: 5, offset: 1 }, end: { line: 5, offset: 7 } }, + { file: formatPath(iff.paths['index.ts']), start: { line: 6, offset: 1 }, end: { line: 6, offset: 7 } }, + ], + }, + { + name: "'./a.module.css' in index.ts", + file: iff.paths['index.ts'], + line: 1, + offset: specifierOffset, + expected: [ + { + file: formatPath(iff.paths['index.ts']), + start: { line: 1, offset: specifierOffset + 1 }, + end: { line: 1, offset: specifierOffset + 15 }, + }, + ], + }, + { + name: "'./b.module.css' in a.module.css", + file: iff.paths['a.module.css'], + line: 1, + offset: 9, + expected: [ + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 1, offset: 10 }, + end: { line: 1, offset: 24 }, + }, + ], + }, + { + name: "'./c.module.css' in a.module.css", + file: iff.paths['a.module.css'], + line: 2, + offset: 33, + expected: [ + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 2, offset: 34 }, + end: { line: 2, offset: 48 }, + }, + ], + }, + { + name: 'a_1 in index.ts', + file: a_1_in_index_ts.file, + ...a_1_in_index_ts.start, + expected: [a_1_in_index_ts, a_1_1_in_a_module_css, a_1_2_in_a_module_css], + }, + { + name: 'a_1 in a.module.css', + file: a_1_1_in_a_module_css.file, + ...a_1_1_in_a_module_css.start, + expected: [a_1_in_index_ts, a_1_1_in_a_module_css, a_1_2_in_a_module_css], + }, + { + name: 'a-2 in index.ts', + file: a_2_in_index_ts.file, + ...a_2_in_index_ts.start, + expected: [a_2_in_index_ts, a_2_in_a_module_css], + }, + { + name: 'a-2 in a.module.css', + file: a_2_in_a_module_css.file, + ...a_2_in_a_module_css.start, + expected: [a_2_in_index_ts, a_2_in_a_module_css], + }, + { + name: 'b_1 in index.ts', + file: b_1_in_index_ts.file, + ...b_1_in_index_ts.start, + expected: [b_1_in_index_ts, b_1_in_b_module_css], + }, + { + name: 'b_1 in b.module.css', + file: b_1_in_b_module_css.file, + ...b_1_in_b_module_css.start, + expected: [b_1_in_index_ts, b_1_in_b_module_css], + }, + { + name: 'c_1 in index.ts', + file: c_1_in_index_ts.file, + ...c_1_in_index_ts.start, + expected: [c_1_in_index_ts, c_1_in_a_module_css, c_1_in_c_module_css], + }, + { + name: 'c_1 in a.module.css', + file: c_1_in_a_module_css.file, + ...c_1_in_a_module_css.start, + expected: [c_1_in_index_ts, c_1_in_a_module_css, c_1_in_c_module_css], + }, + { + name: 'c_1 in c.module.css', + file: c_1_in_c_module_css.file, + ...c_1_in_c_module_css.start, + expected: [c_1_in_index_ts, c_1_in_a_module_css, c_1_in_c_module_css], + }, + { + name: 'c_alias in index.ts', + file: c_alias_in_index_ts.file, + ...c_alias_in_index_ts.start, + // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: + // expected: [c_alias_in_index_ts, c_alias_in_a_module_css], + expected: [c_alias_in_index_ts, c_alias_in_a_module_css, c_2_in_a_module_css, c_2_in_c_module_css], + }, + { + name: 'c_alias in a.module.css', + file: c_alias_in_a_module_css.file, + ...c_alias_in_a_module_css.start, + // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: + // expected: [c_alias_in_index_ts, c_alias_in_a_module_css], + expected: [c_alias_in_index_ts, c_alias_in_a_module_css, c_2_in_a_module_css, c_2_in_c_module_css], + }, + { + name: 'c_2 in a.module.css', + file: c_2_in_a_module_css.file, + ...c_2_in_a_module_css.start, + expected: [c_alias_in_index_ts, c_alias_in_a_module_css, c_2_in_a_module_css, c_2_in_c_module_css], + }, + { + name: 'c_2 in c.module.css', + file: c_2_in_c_module_css.file, + ...c_2_in_c_module_css.start, + expected: [c_alias_in_index_ts, c_alias_in_a_module_css, c_2_in_a_module_css, c_2_in_c_module_css], + }, + ])('Find All References for $name', async ({ file, line, offset, expected }) => { const res = await tsserver.sendReferences({ file, line, diff --git a/packages/ts-plugin/e2e-test/feature/go-to-definition.test.ts b/packages/ts-plugin/e2e-test/feature/go-to-definition.test.ts index 2d32f6bb..b3cfb050 100644 --- a/packages/ts-plugin/e2e-test/feature/go-to-definition.test.ts +++ b/packages/ts-plugin/e2e-test/feature/go-to-definition.test.ts @@ -64,250 +64,248 @@ describe.each([ await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }], }); - test.each( - [ - { - name: 'styles in index.ts', - file: iff.paths['index.ts'], - line: 1, - offset: stylesOffset, - expected: [ - { file: formatPath(iff.paths['a.module.css']), start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } }, - ], - }, - { - name: "'./a.module.css' in index.ts", - file: iff.paths['index.ts'], - line: 1, - offset: specifierOffset, - expected: [ - { file: formatPath(iff.paths['a.module.css']), start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } }, - ], - }, - { - name: "'./b.module.css' in a.module.css", - file: iff.paths['a.module.css'], - line: 1, - offset: 9, - expected: [ - { file: formatPath(iff.paths['b.module.css']), start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } }, - ], - }, - { - name: "'./c.module.css' in a.module.css", - file: iff.paths['a.module.css'], - line: 2, - offset: 33, - expected: [ - { file: formatPath(iff.paths['c.module.css']), start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } }, - ], - }, - { - name: 'a_1 in index.ts', - file: iff.paths['index.ts'], - line: 2, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 3, offset: 2 }, - end: { line: 3, offset: 5 }, - contextStart: { line: 3, offset: 1 }, - contextEnd: { line: 3, offset: 21 }, - }, - ], - }, - { - name: 'a_2 in index.ts', - file: iff.paths['index.ts'], - line: 3, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 4, offset: 2 }, - end: { line: 4, offset: 5 }, - contextStart: { line: 4, offset: 1 }, - contextEnd: { line: 4, offset: 21 }, - }, - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 5, offset: 2 }, - end: { line: 5, offset: 5 }, - contextStart: { line: 5, offset: 1 }, - contextEnd: { line: 5, offset: 21 }, - }, - ], - }, - { - name: 'a_2 in a.module.ts', - file: iff.paths['a.module.css'], - line: 4, - offset: 2, - expected: [ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 4, offset: 2 }, - end: { line: 4, offset: 5 }, - contextStart: { line: 4, offset: 1 }, - contextEnd: { line: 4, offset: 21 }, - }, - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 5, offset: 2 }, - end: { line: 5, offset: 5 }, - contextStart: { line: 5, offset: 1 }, - contextEnd: { line: 5, offset: 21 }, - }, - ], - }, - { - name: 'a_3 in index.ts', - file: iff.paths['index.ts'], - line: 4, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 6, offset: 8 }, - end: { line: 6, offset: 11 }, - contextStart: { line: 6, offset: 1 }, - contextEnd: { line: 6, offset: 16 }, - }, - ], - }, - !namedExports && { - name: 'a-4 in index.ts', - file: iff.paths['index.ts'], - line: 5, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 7, offset: 2 }, - end: { line: 7, offset: 5 }, - contextStart: { line: 7, offset: 1 }, - contextEnd: { line: 7, offset: 21 }, - }, - ], - }, - !namedExports && { - name: 'a-4 in a.module.css', - file: iff.paths['a.module.css'], - line: 7, - offset: 2, - expected: [ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 7, offset: 2 }, - end: { line: 7, offset: 5 }, - contextStart: { line: 7, offset: 1 }, - contextEnd: { line: 7, offset: 21 }, - }, - ], - }, - { - name: 'b_1 in index.ts', - file: iff.paths['index.ts'], - line: 6, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['b.module.css']), - start: { line: 1, offset: 2 }, - end: { line: 1, offset: 5 }, - contextStart: { line: 1, offset: 1 }, - contextEnd: { line: 1, offset: 21 }, - }, - ], - }, - { - name: 'c_1 in index.ts', - file: iff.paths['index.ts'], - line: 7, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['c.module.css']), - start: { line: 1, offset: 8 }, - end: { line: 1, offset: 11 }, - contextStart: { line: 1, offset: 1 }, - contextEnd: { line: 1, offset: 16 }, - }, - ], - }, - { - name: 'c_1 in a.module.ts', - file: iff.paths['a.module.css'], - line: 2, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['c.module.css']), - start: { line: 1, offset: 8 }, - end: { line: 1, offset: 11 }, - contextStart: { line: 1, offset: 1 }, - contextEnd: { line: 1, offset: 16 }, - }, - ], - }, - { - name: 'c_alias in index.ts', - file: iff.paths['index.ts'], - line: 8, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['c.module.css']), - start: { line: 2, offset: 8 }, - end: { line: 2, offset: 11 }, - contextStart: { line: 2, offset: 1 }, - contextEnd: { line: 2, offset: 16 }, - }, - ], - }, - { - name: 'c_alias in a.module.css', - file: iff.paths['a.module.css'], - line: 2, - offset: 20, - expected: [ - { - file: formatPath(iff.paths['c.module.css']), - start: { line: 2, offset: 8 }, - end: { line: 2, offset: 11 }, - contextStart: { line: 2, offset: 1 }, - contextEnd: { line: 2, offset: 16 }, - }, - ], - }, - { - name: 'c_2 in a.module.css', - file: iff.paths['a.module.css'], - line: 2, - offset: 13, - expected: [ - { - file: formatPath(iff.paths['c.module.css']), - start: { line: 2, offset: 8 }, - end: { line: 2, offset: 11 }, - contextStart: { line: 2, offset: 1 }, - contextEnd: { line: 2, offset: 16 }, - }, - ], - }, - { - // NOTE: It is strange that `(` has a definition, but we allow it to keep the implementation simple. - name: '(./b.module.css) in a.module.css', - file: iff.paths['a.module.css'], - line: 8, - offset: 12, - expected: [ - { file: formatPath(iff.paths['b.module.css']), start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } }, - ], - }, - ].filter((c) => c !== false), - )('Go to Definition for $name', async ({ file, line, offset, expected }) => { + test.each([ + { + name: 'styles in index.ts', + file: iff.paths['index.ts'], + line: 1, + offset: stylesOffset, + expected: [ + { file: formatPath(iff.paths['a.module.css']), start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } }, + ], + }, + { + name: "'./a.module.css' in index.ts", + file: iff.paths['index.ts'], + line: 1, + offset: specifierOffset, + expected: [ + { file: formatPath(iff.paths['a.module.css']), start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } }, + ], + }, + { + name: "'./b.module.css' in a.module.css", + file: iff.paths['a.module.css'], + line: 1, + offset: 9, + expected: [ + { file: formatPath(iff.paths['b.module.css']), start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } }, + ], + }, + { + name: "'./c.module.css' in a.module.css", + file: iff.paths['a.module.css'], + line: 2, + offset: 33, + expected: [ + { file: formatPath(iff.paths['c.module.css']), start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } }, + ], + }, + { + name: 'a_1 in index.ts', + file: iff.paths['index.ts'], + line: 2, + offset: 8, + expected: [ + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 3, offset: 2 }, + end: { line: 3, offset: 5 }, + contextStart: { line: 3, offset: 1 }, + contextEnd: { line: 3, offset: 21 }, + }, + ], + }, + { + name: 'a_2 in index.ts', + file: iff.paths['index.ts'], + line: 3, + offset: 8, + expected: [ + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 4, offset: 2 }, + end: { line: 4, offset: 5 }, + contextStart: { line: 4, offset: 1 }, + contextEnd: { line: 4, offset: 21 }, + }, + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 5, offset: 2 }, + end: { line: 5, offset: 5 }, + contextStart: { line: 5, offset: 1 }, + contextEnd: { line: 5, offset: 21 }, + }, + ], + }, + { + name: 'a_2 in a.module.ts', + file: iff.paths['a.module.css'], + line: 4, + offset: 2, + expected: [ + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 4, offset: 2 }, + end: { line: 4, offset: 5 }, + contextStart: { line: 4, offset: 1 }, + contextEnd: { line: 4, offset: 21 }, + }, + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 5, offset: 2 }, + end: { line: 5, offset: 5 }, + contextStart: { line: 5, offset: 1 }, + contextEnd: { line: 5, offset: 21 }, + }, + ], + }, + { + name: 'a_3 in index.ts', + file: iff.paths['index.ts'], + line: 4, + offset: 8, + expected: [ + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 6, offset: 8 }, + end: { line: 6, offset: 11 }, + contextStart: { line: 6, offset: 1 }, + contextEnd: { line: 6, offset: 16 }, + }, + ], + }, + { + name: 'a-4 in index.ts', + file: iff.paths['index.ts'], + line: 5, + offset: 8, + expected: [ + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 7, offset: 2 }, + end: { line: 7, offset: 5 }, + contextStart: { line: 7, offset: 1 }, + contextEnd: { line: 7, offset: 21 }, + }, + ], + }, + { + name: 'a-4 in a.module.css', + file: iff.paths['a.module.css'], + line: 7, + offset: 2, + expected: [ + { + file: formatPath(iff.paths['a.module.css']), + start: { line: 7, offset: 2 }, + end: { line: 7, offset: 5 }, + contextStart: { line: 7, offset: 1 }, + contextEnd: { line: 7, offset: 21 }, + }, + ], + }, + { + name: 'b_1 in index.ts', + file: iff.paths['index.ts'], + line: 6, + offset: 8, + expected: [ + { + file: formatPath(iff.paths['b.module.css']), + start: { line: 1, offset: 2 }, + end: { line: 1, offset: 5 }, + contextStart: { line: 1, offset: 1 }, + contextEnd: { line: 1, offset: 21 }, + }, + ], + }, + { + name: 'c_1 in index.ts', + file: iff.paths['index.ts'], + line: 7, + offset: 8, + expected: [ + { + file: formatPath(iff.paths['c.module.css']), + start: { line: 1, offset: 8 }, + end: { line: 1, offset: 11 }, + contextStart: { line: 1, offset: 1 }, + contextEnd: { line: 1, offset: 16 }, + }, + ], + }, + { + name: 'c_1 in a.module.ts', + file: iff.paths['a.module.css'], + line: 2, + offset: 8, + expected: [ + { + file: formatPath(iff.paths['c.module.css']), + start: { line: 1, offset: 8 }, + end: { line: 1, offset: 11 }, + contextStart: { line: 1, offset: 1 }, + contextEnd: { line: 1, offset: 16 }, + }, + ], + }, + { + name: 'c_alias in index.ts', + file: iff.paths['index.ts'], + line: 8, + offset: 8, + expected: [ + { + file: formatPath(iff.paths['c.module.css']), + start: { line: 2, offset: 8 }, + end: { line: 2, offset: 11 }, + contextStart: { line: 2, offset: 1 }, + contextEnd: { line: 2, offset: 16 }, + }, + ], + }, + { + name: 'c_alias in a.module.css', + file: iff.paths['a.module.css'], + line: 2, + offset: 20, + expected: [ + { + file: formatPath(iff.paths['c.module.css']), + start: { line: 2, offset: 8 }, + end: { line: 2, offset: 11 }, + contextStart: { line: 2, offset: 1 }, + contextEnd: { line: 2, offset: 16 }, + }, + ], + }, + { + name: 'c_2 in a.module.css', + file: iff.paths['a.module.css'], + line: 2, + offset: 13, + expected: [ + { + file: formatPath(iff.paths['c.module.css']), + start: { line: 2, offset: 8 }, + end: { line: 2, offset: 11 }, + contextStart: { line: 2, offset: 1 }, + contextEnd: { line: 2, offset: 16 }, + }, + ], + }, + { + // NOTE: It is strange that `(` has a definition, but we allow it to keep the implementation simple. + name: '(./b.module.css) in a.module.css', + file: iff.paths['a.module.css'], + line: 8, + offset: 12, + expected: [ + { file: formatPath(iff.paths['b.module.css']), start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } }, + ], + }, + ])('Go to Definition for $name', async ({ file, line, offset, expected }) => { const res = await tsserver.sendDefinitionAndBoundSpan({ file, line, diff --git a/packages/ts-plugin/src/language-service/feature/definition-and-bound-span.ts b/packages/ts-plugin/src/language-service/feature/definition-and-bound-span.ts index a12341ec..aa3ea0e9 100644 --- a/packages/ts-plugin/src/language-service/feature/definition-and-bound-span.ts +++ b/packages/ts-plugin/src/language-service/feature/definition-and-bound-span.ts @@ -26,14 +26,11 @@ export function getDefinitionAndBoundSpan( continue; } const cssModule = script.generated.root[CMK_DATA_KEY]; - const defName = unquote(def.name); - // Keep only definitions that map to a token declared in this module's `localTokens`. - // Re-exports from `@value ... from '...'` aren't declarations here, so they're excluded — - // their real declaration lives in the target file. - const localToken = cssModule.localTokens.find( - (t) => t.name === defName && t.loc.start.offset === def.textSpan.start, - ); + const localToken = cssModule.localTokens.find((t) => t.loc.start.offset === def.textSpan.start); + + // Due to the structure of .d.ts files, tokens imported via `@value ... from ...` may be included in `result.definitions`. + // Since these are tokens imported from other modules, they should not be returned as definitions. if (!localToken) continue; // Set `contextSpan` for local tokens. `contextSpan` is used for Definition Preview in editors. @@ -50,15 +47,3 @@ export function getDefinitionAndBoundSpan( return result; }; } - -/** - * Removes surrounding single quotes from a string if present. - * When `namedExport` is false, `def.name` is `"'tokenName'"` (with quotes), - * but `token.name` is `"tokenName"` (without quotes). - */ -function unquote(name: string): string { - if (name.length >= 2 && name.startsWith("'") && name.endsWith("'")) { - return name.slice(1, -1); - } - return name; -}