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 e4ff755f..05e233ae 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 @@ -1,279 +1,294 @@ import dedent from 'dedent'; import { describe, expect, test } from 'vite-plus/test'; -import { createIFF } from '../test-util/fixture.js'; +import { buildStylesImport, buildTSConfigJSON } from '../../src/test/builder.js'; +import { setupFixture } from '../test-util/fixture.js'; import { formatPath, launchTsserver, normalizeRefItems } from '../test-util/tsserver.js'; -describe.each([ - { - namedExports: false, - importStatement: "import styles from './a.module.css';", - stylesOffset: 8, - specifierOffset: 20, - }, - { - namedExports: true, - importStatement: "import * as styles from './a.module.css';", - stylesOffset: 13, - specifierOffset: 25, - }, -])( - 'Find All References (namedExports: $namedExports)', - async ({ importStatement, namedExports, stylesOffset, specifierOffset }) => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - ${importStatement} - styles.a_1; - styles['a-2']; - styles.b_1; - styles.c_1; - styles.c_alias; - `, - 'a.module.css': dedent` - @import './b.module.css'; - @value c_1, c_2 as c_alias from './c.module.css'; - .a_1 { color: red; } - .a_1 { color: red; } - .a-2 { color: red; } - `, - 'b.module.css': dedent` - .b_1 { color: red; } - `, - 'c.module.css': dedent` - @value c_1: red; - @value c_2: red; - `, - 'tsconfig.json': dedent` - { - "compilerOptions": {}, - "cmkOptions": { - "enabled": true, - "dtsOutDir": "generated", - "namedExports": ${namedExports} - } - } - `, +const tsserver = launchTsserver(); + +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + describe('finds all references to the styles binding', () => { + test('from the styles binding in the import statement', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + styles.a_2; + `, + 'a.module.css': dedent` + .a_1 { color: red; } + .a_2 { color: red; } + `, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendReferences({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'styles', 0), + }); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'styles', 0) }, + { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'styles', 1) }, + { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'styles', 2) }, + ]), + ); + }); + }); + + describe('finds all references to a local token', () => { + test('from styles. access', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendReferences({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'a_1'), + }); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'a_1') }, + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1') }, + ]), + ); }); - const a_1_in_index_ts = { - file: formatPath(iff.paths['index.ts']), - start: { line: 2, offset: 8 }, - end: { line: 2, offset: 11 }, - }; - const a_1_1_in_a_module_css = { - file: formatPath(iff.paths['a.module.css']), - start: { line: 3, offset: 2 }, - end: { line: 3, offset: 5 }, - }; - const a_1_2_in_a_module_css = { - file: formatPath(iff.paths['a.module.css']), - start: { line: 4, offset: 2 }, - end: { line: 4, offset: 5 }, - }; - const a_2_in_index_ts = { - file: formatPath(iff.paths['index.ts']), - start: { line: 3, offset: 9 }, - end: { line: 3, offset: 12 }, - }; - const a_2_in_a_module_css = { - file: formatPath(iff.paths['a.module.css']), - start: { line: 5, offset: 2 }, - end: { line: 5, offset: 5 }, - }; - const b_1_in_index_ts = { - file: formatPath(iff.paths['index.ts']), - start: { line: 4, offset: 8 }, - end: { line: 4, offset: 11 }, - }; - const b_1_in_b_module_css = { - file: formatPath(iff.paths['b.module.css']), - start: { line: 1, offset: 2 }, - end: { line: 1, offset: 5 }, - }; - const c_1_in_index_ts = { - file: formatPath(iff.paths['index.ts']), - start: { line: 5, offset: 8 }, - end: { line: 5, offset: 11 }, - }; - const c_1_in_a_module_css = { - file: formatPath(iff.paths['a.module.css']), - start: { line: 2, offset: 8 }, - end: { line: 2, offset: 11 }, - }; - const c_1_in_c_module_css = { - file: formatPath(iff.paths['c.module.css']), - start: { line: 1, offset: 8 }, - end: { line: 1, offset: 11 }, - }; - const c_alias_in_index_ts = { - file: formatPath(iff.paths['index.ts']), - start: { line: 6, offset: 8 }, - end: { line: 6, offset: 15 }, - }; - const c_alias_in_a_module_css = { - file: formatPath(iff.paths['a.module.css']), - start: { line: 2, offset: 20 }, - end: { line: 2, offset: 27 }, - }; - const c_2_in_a_module_css = { - file: formatPath(iff.paths['a.module.css']), - start: { line: 2, offset: 13 }, - end: { line: 2, offset: 16 }, - }; - const c_2_in_c_module_css = { - file: formatPath(iff.paths['c.module.css']), - start: { line: 2, offset: 8 }, - end: { line: 2, offset: 11 }, - }; - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['index.ts'] }], + + test('from styles[] access', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles['a-1']; + `, + 'a.module.css': `.a-1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendReferences({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'a-1'), + }); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'a-1') }, + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a-1') }, + ]), + ); }); - test.each([ - { - name: 'styles in index.ts', + + test('when the same class is declared multiple times', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + 'a.module.css': dedent` + .a_1 { color: red; } + .a_1 { color: red; } + `, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendReferences({ 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", + ...getLoc('index.ts', 'a_1'), + }); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'a_1') }, + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1', 0) }, + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1', 1) }, + ]), + ); + }); + }); + + describe('transitively resolves a re-export to its source declaration', () => { + test('class re-exported via @import', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.b_1; + `, + 'a.module.css': `@import './b.module.css';`, + 'b.module.css': `.b_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendReferences({ 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", + ...getLoc('index.ts', 'b_1'), + }); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'b_1') }, + { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, + ]), + ); + }); + + test('value re-exported via @value ... from', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.b_1; + `, + 'a.module.css': `@value b_1 from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendReferences({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'b_1'), + }); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'b_1') }, + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, + { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, + ]), + ); + }); + + // NOTE: Ideally only `b_alias` should be returned, but `b_1` is also returned for implementation simplicity. + test('aliased value re-exported via @value ... from', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.b_alias; + `, + 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendReferences({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'b_alias'), + }); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'b_alias') }, + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_alias') }, + { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, + ]), + ); + }); + }); + + describe('finds all references from a CSS-side class declaration', () => { + test('from a . declaration', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendReferences({ 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", + ...getLoc('a.module.css', 'a_1'), + }); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['index.ts']), ...getRange('index.ts', 'a_1') }, + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'a_1') }, + ]), + ); + }); + }); + + describe('finds all references from a CSS-side @value ... from binding', () => { + test('from the import binding', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@value b_1 from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendReferences({ 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 }) => { + ...getLoc('a.module.css', 'b_1'), + }); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, + { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, + ]), + ); + }); + + // NOTE: Ideally only `b_alias` should be returned, but `b_1` is also returned for implementation simplicity. + test('from the alias name in `name as alias`', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + const res = await tsserver.sendReferences({ - file, - line, - offset, + file: iff.paths['a.module.css'], + ...getLoc('a.module.css', 'b_alias'), }); - expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual(normalizeRefItems(expected)); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_alias') }, + { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, + ]), + ); + }); + + // NOTE: Ideally only `b_1` should be returned, but `b_alias` is also returned for implementation simplicity. + test('from the source name in `name as alias`', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendReferences({ + file: iff.paths['a.module.css'], + ...getLoc('a.module.css', 'b_1'), + }); + + expect(normalizeRefItems(res.body?.refs ?? [])).toStrictEqual( + normalizeRefItems([ + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_1') }, + { file: formatPath(iff.paths['a.module.css']), ...getRange('a.module.css', 'b_alias') }, + { file: formatPath(iff.paths['b.module.css']), ...getRange('b.module.css', 'b_1') }, + ]), + ); }); - }, -); + }); +}); 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 875f8232..294413aa 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 @@ -1,6 +1,6 @@ import dedent from 'dedent'; import { describe, expect, test } from 'vite-plus/test'; -import { buildStylesImport, buildTSConfigJSON } from '../../test/builder.js'; +import { buildStylesImport, buildTSConfigJSON } from '../../src/test/builder.js'; import { setupFixture } from '../test-util/fixture.js'; import { formatPath, launchTsserver, normalizeDefinitions } from '../test-util/tsserver.js'; @@ -192,7 +192,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: ); }); - test('returns all declarations when the same class is declared multiple times', async () => { + test('when the same class is declared multiple times', async () => { const { iff, getLoc, getRange } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` @@ -298,16 +298,16 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` ${buildStylesImport('./a.module.css', { namedExports })} - styles.a_alias; + styles.b_alias; `, - 'a.module.css': `@value b_1 as a_alias from './b.module.css';`, + 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); const res = await tsserver.sendDefinitionAndBoundSpan({ file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a_alias'), + ...getLoc('index.ts', 'b_alias'), }); const { start: contextStart, end: contextEnd } = getRange('b.module.css', '@value b_1: red'); @@ -324,6 +324,37 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); }); + describe('jumps from a CSS-side class declaration', () => { + test('from a . declaration', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendDefinitionAndBoundSpan({ + file: iff.paths['a.module.css'], + ...getLoc('a.module.css', 'a_1'), + }); + + const { start: contextStart, end: contextEnd } = getRange('a.module.css', '.a_1 { color: red; }'); + expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( + normalizeDefinitions([ + { + file: formatPath(iff.paths['a.module.css']), + ...getRange('a.module.css', 'a_1'), + contextStart, + contextEnd, + }, + ]), + ); + }); + }); + describe('jumps from a CSS-side @value ... from binding to its source declaration', () => { test('from the import binding', async () => { const { iff, getLoc, getRange } = await setupFixture({ @@ -354,14 +385,14 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: test('from the alias name in `name as alias`', async () => { const { iff, getLoc, getRange } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), - 'a.module.css': `@value b_1 as a_alias from './b.module.css';`, + 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); const res = await tsserver.sendDefinitionAndBoundSpan({ file: iff.paths['a.module.css'], - ...getLoc('a.module.css', 'a_alias'), + ...getLoc('a.module.css', 'b_alias'), }); const { start: contextStart, end: contextEnd } = getRange('b.module.css', '@value b_1: red'); @@ -380,7 +411,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: test('from the source name in `name as alias`', async () => { const { iff, getLoc, getRange } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), - 'a.module.css': `@value b_1 as a_alias from './b.module.css';`, + 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, 'b.module.css': `@value b_1: red;`, }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); diff --git a/packages/ts-plugin/e2e-test/feature/rename-symbol.test.ts b/packages/ts-plugin/e2e-test/feature/rename-symbol.test.ts index 0a1e2fb1..e94b2e1e 100644 --- a/packages/ts-plugin/e2e-test/feature/rename-symbol.test.ts +++ b/packages/ts-plugin/e2e-test/feature/rename-symbol.test.ts @@ -1,431 +1,274 @@ import dedent from 'dedent'; import { describe, expect, test } from 'vite-plus/test'; -import { createIFF } from '../test-util/fixture.js'; +import { buildStylesImport, buildTSConfigJSON } from '../../src/test/builder.js'; +import { setupFixture } from '../test-util/fixture.js'; import { formatPath, launchTsserver, normalizeSpanGroups } from '../test-util/tsserver.js'; -describe('Rename Symbol', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - import styles from './a.module.css'; - styles.a_1; - styles.a_1; - styles.a_2; - styles['a-3']; - styles.b_1; - styles.c_1; - styles.c_alias; - styles.d_1; - `, - 'a.module.css': dedent` - @import './b.module.css'; - @value c_1, c_2 as c_alias, d_1 from './c.module.css'; - .a_1 { color: red; } - .a_1 { color: red; } - @value a_2: red; - .a-3 { color: red; } - `, - 'b.module.css': dedent` - .b_1 { color: red; } - `, - 'c.module.css': dedent` - @value c_1: red; - @value c_2: red; - @value d_1 from './d.module.css'; - `, - 'd.module.css': dedent` - @value d_1: red; - `, - 'tsconfig.json': dedent` - { - "compilerOptions": {}, - "cmkOptions": { - "enabled": true, - "dtsOutDir": "generated" - } - } - `, +const tsserver = launchTsserver(); + +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + describe('renames a local token', () => { + test('from styles. access', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendRename({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'a_1'), + }); + + expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( + normalizeSpanGroups([ + { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'a_1')] }, + { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'a_1')] }, + ]), + ); + }); + + test('from styles[] access', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles['a-1']; + `, + 'a.module.css': `.a-1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendRename({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'a-1'), + }); + + expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( + normalizeSpanGroups([ + { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'a-1')] }, + { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'a-1')] }, + ]), + ); + }); + + test('when the same class is declared multiple times', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + 'a.module.css': dedent` + .a_1 { color: red; } + .a_1 { color: red; } + `, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendRename({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'a_1'), + }); + + expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( + normalizeSpanGroups([ + { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'a_1')] }, + { + file: formatPath(iff.paths['a.module.css']), + locs: [getRange('a.module.css', 'a_1', 0), getRange('a.module.css', 'a_1', 1)], + }, + ]), + ); + }); + }); + + describe('transitively resolves a re-export to its source declaration', () => { + test('class re-exported via @import', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.b_1; + `, + 'a.module.css': `@import './b.module.css';`, + 'b.module.css': `.b_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendRename({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'b_1'), + }); + + expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( + normalizeSpanGroups([ + { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'b_1')] }, + { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, + ]), + ); + }); + + // NOTE: For simplicity of implementation, this is not the ideal behavior. + // The ideal behavior would attach `prefixText: 'b_1 as '` to the binding loc in `a.module.css` + // so that renaming changes only the alias side. Currently the binding loc is rewritten directly. + test('value re-exported via @value ... from', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.b_1; + `, + 'a.module.css': `@value b_1 from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendRename({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'b_1'), + }); + + expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( + normalizeSpanGroups([ + { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'b_1')] }, + { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'b_1')] }, + { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, + ]), + ); + }); + + // NOTE: Ideally only `b_alias` should be returned, but `b_1` is also returned for implementation simplicity. + test('aliased value re-exported via @value ... from', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.b_alias; + `, + 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendRename({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'b_alias'), + }); + + expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( + normalizeSpanGroups([ + { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'b_alias')] }, + { + file: formatPath(iff.paths['a.module.css']), + locs: [getRange('a.module.css', 'b_1'), getRange('a.module.css', 'b_alias')], + }, + { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, + ]), + ); + }); }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['index.ts'] }], + + describe('renames from a CSS-side class declaration', () => { + test('from a . declaration', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendRename({ + file: iff.paths['a.module.css'], + ...getLoc('a.module.css', 'a_1'), + }); + + expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( + normalizeSpanGroups([ + { file: formatPath(iff.paths['index.ts']), locs: [getRange('index.ts', 'a_1')] }, + { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'a_1')] }, + ]), + ); + }); }); - test.each([ - { - name: 'a_1 in index.ts', - file: iff.paths['index.ts'], - line: 2, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [ - { start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 } }, - { start: { line: 3, offset: 8 }, end: { line: 3, offset: 11 } }, - ], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [ - { start: { line: 3, offset: 2 }, end: { line: 3, offset: 5 } }, - { start: { line: 4, offset: 2 }, end: { line: 4, offset: 5 } }, - ], - }, - ], - }, - { - name: 'a_1 in a.module.css', - file: iff.paths['a.module.css'], - line: 3, - offset: 2, - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [ - { start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 } }, - { start: { line: 3, offset: 8 }, end: { line: 3, offset: 11 } }, - ], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [ - { start: { line: 3, offset: 2 }, end: { line: 3, offset: 5 } }, - { start: { line: 4, offset: 2 }, end: { line: 4, offset: 5 } }, - ], - }, - ], - }, - { - name: 'a_2 in index.ts', - file: iff.paths['index.ts'], - line: 4, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 4, offset: 8 }, end: { line: 4, offset: 11 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [{ start: { line: 5, offset: 8 }, end: { line: 5, offset: 11 } }], - }, - ], - }, - { - name: 'a_2 in a.module.css', - file: iff.paths['a.module.css'], - line: 5, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 4, offset: 8 }, end: { line: 4, offset: 11 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [{ start: { line: 5, offset: 8 }, end: { line: 5, offset: 11 } }], - }, - ], - }, - { - name: 'a-3 in index.ts', - file: iff.paths['index.ts'], - line: 5, - offset: 9, - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 5, offset: 9 }, end: { line: 5, offset: 12 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [{ start: { line: 6, offset: 2 }, end: { line: 6, offset: 5 } }], - }, - ], - }, - { - name: 'a-3 in a.module.css', - file: iff.paths['a.module.css'], - line: 6, - offset: 2, - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 5, offset: 9 }, end: { line: 5, offset: 12 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [{ start: { line: 6, offset: 2 }, end: { line: 6, offset: 5 } }], - }, - ], - }, - { - name: 'b_1 in index.ts', - file: iff.paths['index.ts'], - line: 6, - offset: 8, - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 6, offset: 8 }, end: { line: 6, offset: 11 } }], - }, - { - file: formatPath(iff.paths['b.module.css']), - locs: [{ start: { line: 1, offset: 2 }, end: { line: 1, offset: 5 } }], - }, - ], - }, - { - name: 'b_1 in b.module.css', - file: iff.paths['b.module.css'], - line: 1, - offset: 2, - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 6, offset: 8 }, end: { line: 6, offset: 11 } }], - }, - { - file: formatPath(iff.paths['b.module.css']), - locs: [{ start: { line: 1, offset: 2 }, end: { line: 1, offset: 5 } }], - }, - ], - }, - { - name: 'c_1 in index.ts', - file: iff.paths['index.ts'], - line: 7, - offset: 8, - // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: - // expected: [ - // { - // file: formatPath(iff.paths['index.ts']), - // locs: [{ start: { line: 7, offset: 8 }, end: { line: 7, offset: 11 } }], - // }, - // { - // file: formatPath(iff.paths['a.module.css']), - // locs: [{ start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 }, prefixText: 'c_1 as ' }], - // }, - // ], - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 7, offset: 8 }, end: { line: 7, offset: 11 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [{ start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 } }], - }, - { - file: formatPath(iff.paths['c.module.css']), - locs: [{ start: { line: 1, offset: 8 }, end: { line: 1, offset: 11 } }], - }, - ], - }, - { - name: 'c_1 in a.module.css', - file: iff.paths['a.module.css'], - line: 2, - offset: 8, - // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: - // expected: [ - // { - // file: formatPath(iff.paths['index.ts']), - // locs: [{ start: { line: 7, offset: 8 }, end: { line: 7, offset: 11 } }], - // }, - // { - // file: formatPath(iff.paths['a.module.css']), - // locs: [{ start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 }, prefixText: 'c_1 as ' }], - // }, - // ], - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 7, offset: 8 }, end: { line: 7, offset: 11 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [{ start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 } }], - }, - { - file: formatPath(iff.paths['c.module.css']), - locs: [{ start: { line: 1, offset: 8 }, end: { line: 1, offset: 11 } }], - }, - ], - }, - { - name: 'c_1 in c.module.css', - file: iff.paths['c.module.css'], - line: 1, - offset: 8, - // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: - // expected: [ - // { - // file: formatPath(iff.paths['a.module.css']), - // locs: [{ start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 }, suffixText: ' as c_1' }], - // }, - // { - // file: formatPath(iff.paths['c.module.css']), - // locs: [{ start: { line: 1, offset: 8 }, end: { line: 1, offset: 11 } }], - // }, - // ], - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 7, offset: 8 }, end: { line: 7, offset: 11 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [{ start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 } }], - }, - { - file: formatPath(iff.paths['c.module.css']), - locs: [{ start: { line: 1, offset: 8 }, end: { line: 1, offset: 11 } }], - }, - ], - }, - { - name: 'c_alias in index.ts', - file: iff.paths['index.ts'], - line: 8, - offset: 8, - // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: - // expected: [ - // { - // file: formatPath(iff.paths['index.ts']), - // locs: [{ start: { line: 8, offset: 8 }, end: { line: 8, offset: 15 } }], - // }, - // { - // file: formatPath(iff.paths['a.module.css']), - // locs: [{ start: { line: 2, offset: 20 }, end: { line: 2, offset: 27 } }], - // }, - // ], - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 8, offset: 8 }, end: { line: 8, offset: 15 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [ - { start: { line: 2, offset: 13 }, end: { line: 2, offset: 16 } }, - { start: { line: 2, offset: 20 }, end: { line: 2, offset: 27 } }, - ], - }, - { - file: formatPath(iff.paths['c.module.css']), - locs: [{ start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 } }], - }, - ], - }, - { - name: 'c_alias in a.module.css', - file: iff.paths['a.module.css'], - line: 2, - offset: 20, - // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: - // expected: [ - // { - // file: formatPath(iff.paths['index.ts']), - // locs: [{ start: { line: 7, offset: 8 }, end: { line: 7, offset: 15 } }], - // }, - // { - // file: formatPath(iff.paths['a.module.css']), - // locs: [{ start: { line: 2, offset: 20 }, end: { line: 2, offset: 27 } }], - // }, - // ], - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 8, offset: 8 }, end: { line: 8, offset: 15 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [ - { start: { line: 2, offset: 13 }, end: { line: 2, offset: 16 } }, - { start: { line: 2, offset: 20 }, end: { line: 2, offset: 27 } }, - ], - }, - { - file: formatPath(iff.paths['c.module.css']), - locs: [{ start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 } }], - }, - ], - }, - { - name: 'c_2 in c.module.css', - file: iff.paths['c.module.css'], - line: 2, - offset: 8, - // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: - // expected: [ - // { - // file: formatPath(iff.paths['a.module.css']), - // locs: [{ start: { line: 2, offset: 13 }, end: { line: 2, offset: 16 } }], - // }, - // { - // file: formatPath(iff.paths['c.module.css']), - // locs: [{ start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 } }], - // }, - // ], - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 8, offset: 8 }, end: { line: 8, offset: 15 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [ - { start: { line: 2, offset: 13 }, end: { line: 2, offset: 16 } }, - { start: { line: 2, offset: 20 }, end: { line: 2, offset: 27 } }, - ], - }, - { - file: formatPath(iff.paths['c.module.css']), - locs: [{ start: { line: 2, offset: 8 }, end: { line: 2, offset: 11 } }], - }, - ], - }, - { - name: 'd_1 in d.module.css', - file: iff.paths['d.module.css'], - line: 1, - offset: 8, - // NOTE: For simplicity of implementation, this is not the ideal behavior. The ideal behavior is as follows: - // expected: [ - // { - // file: formatPath(iff.paths['c.module.css']), - // locs: [{ start: { line: 3, offset: 8 }, end: { line: 3, offset: 11 }, suffixText: ' as d_1' }], - // }, - // { - // file: formatPath(iff.paths['d.module.css']), - // locs: [{ start: { line: 1, offset: 8 }, end: { line: 1, offset: 11 } }], - // }, - // ], - expected: [ - { - file: formatPath(iff.paths['index.ts']), - locs: [{ start: { line: 9, offset: 8 }, end: { line: 9, offset: 11 } }], - }, - { - file: formatPath(iff.paths['a.module.css']), - locs: [{ start: { line: 2, offset: 29 }, end: { line: 2, offset: 32 } }], - }, - { - file: formatPath(iff.paths['c.module.css']), - locs: [{ start: { line: 3, offset: 8 }, end: { line: 3, offset: 11 } }], - }, - { - file: formatPath(iff.paths['d.module.css']), - locs: [{ start: { line: 1, offset: 8 }, end: { line: 1, offset: 11 } }], - }, - ], - }, - ])('Rename Symbol for $name', async ({ file, line, offset, expected }) => { - const res = await tsserver.sendRename({ - file, - line, - offset, + + describe('renames from a CSS-side @value ... from binding', () => { + test('from the import binding', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@value b_1 from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendRename({ + file: iff.paths['a.module.css'], + ...getLoc('a.module.css', 'b_1'), + }); + + expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( + normalizeSpanGroups([ + { file: formatPath(iff.paths['a.module.css']), locs: [getRange('a.module.css', 'b_1')] }, + { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, + ]), + ); + }); + + // NOTE: Ideally only `b_alias` should be returned, but `b_1` is also returned for implementation simplicity. + test('from the alias name in `name as alias`', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendRename({ + file: iff.paths['a.module.css'], + ...getLoc('a.module.css', 'b_alias'), + }); + + expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( + normalizeSpanGroups([ + { + file: formatPath(iff.paths['a.module.css']), + locs: [getRange('a.module.css', 'b_1'), getRange('a.module.css', 'b_alias')], + }, + { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, + ]), + ); + }); + + // NOTE: Ideally only `b_1` should be returned, but `b_alias` is also returned for implementation simplicity. + test('from the source name in `name as alias`', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@value b_1 as b_alias from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendRename({ + file: iff.paths['a.module.css'], + ...getLoc('a.module.css', 'b_1'), + }); + + expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual( + normalizeSpanGroups([ + { + file: formatPath(iff.paths['a.module.css']), + locs: [getRange('a.module.css', 'b_1'), getRange('a.module.css', 'b_alias')], + }, + { file: formatPath(iff.paths['b.module.css']), locs: [getRange('b.module.css', 'b_1')] }, + ]), + ); }); - expect(normalizeSpanGroups(res.body?.locs ?? [])).toStrictEqual(normalizeSpanGroups(expected)); }); }); diff --git a/packages/ts-plugin/test/builder.ts b/packages/ts-plugin/src/test/builder.ts similarity index 100% rename from packages/ts-plugin/test/builder.ts rename to packages/ts-plugin/src/test/builder.ts