Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [],
"ignore": ["@css-modules-kit/content-mapper"],
"privatePackages": {
"version": true,
"tag": true
Expand Down
5 changes: 5 additions & 0 deletions .changeset/export-token-utilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@css-modules-kit/core': minor
---

feat(core): export `validateTokenName`, `isURLSpecifier`, and token reference types
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@ Cargo.lock

### User
/crates/zed/extension.wasm
/.tmp/
44 changes: 44 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,50 @@
"TSS_DEBUG": "5859"
}
},
{
// Launches the TypeScript Native Preview extension and the tsgo binary built from
// the pinned microsoft/TypeScript commit (see scripts/setup-tsgo-extension.sh).
// The marketplace build predates the content mapper support, so the extension
// must be run from source.
"name": "tsgo (7-content-mapper)",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/.tmp/typescript/packages/vscode-typescript",
"--profile-temp",
"--skip-welcome",
// The extension enables content mappers only in a trusted workspace. Disabling
// workspace trust makes VS Code treat every workspace as trusted, which also
// skips the trust dialog on launch.
"--disable-workspace-trust",
"--folder-uri=${workspaceFolder}/examples/7-content-mapper",
"${workspaceFolder}/examples/7-content-mapper/src/index.ts"
],
"outFiles": ["${workspaceFolder}/.tmp/typescript/packages/vscode-typescript/dist/**/*.js"],
"preLaunchTask": "prepare content-mapper examples",
"presentation": {
"group": "tsgo"
}
},
{
// See the comments of "tsgo (7-content-mapper)".
"name": "tsgo (8-content-mapper-named-exports)",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/.tmp/typescript/packages/vscode-typescript",
"--profile-temp",
"--skip-welcome",
"--disable-workspace-trust",
"--folder-uri=${workspaceFolder}/examples/8-content-mapper-named-exports",
"${workspaceFolder}/examples/8-content-mapper-named-exports/src/index.ts"
],
"outFiles": ["${workspaceFolder}/.tmp/typescript/packages/vscode-typescript/dist/**/*.js"],
"preLaunchTask": "prepare content-mapper examples",
"presentation": {
"group": "tsgo"
}
},
{
"name": "vscode-test",
"type": "extensionHost",
Expand Down
23 changes: 23 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@
"cwd": "${workspaceFolder}/packages/vscode"
},
"group": "build"
},
{
"label": "vp: build - packages/content-mapper",
"type": "shell",
"command": "vp run build",
"options": {
"cwd": "${workspaceFolder}/packages/content-mapper"
},
"group": "build"
},
{
"label": "setup tsgo extension",
"type": "shell",
"command": "./scripts/setup-tsgo-extension.sh",
"options": {
"cwd": "${workspaceFolder}"
},
"group": "build"
},
{
"label": "prepare content-mapper examples",
"dependsOn": ["vp: build - packages/content-mapper", "setup tsgo extension"],
"group": "build"
}
]
}
5 changes: 5 additions & 0 deletions examples/7-content-mapper/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"js/ts.experimental.useTsgo": true,
// Enables the content mapper inspector ("TypeScript: Show Content Mapper Virtual Documents").
"js/ts.showDebugInfo": true
}
14 changes: 14 additions & 0 deletions examples/7-content-mapper/src/a.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@import './b.module.css';
@value primary: #2864f0;

.a_1 {
color: primary;
composes: b_1 from './b.module.css';
animation-name: fade-in;
}

@keyframes fade-in {
from {
opacity: 0;
}
}
3 changes: 3 additions & 0 deletions examples/7-content-mapper/src/b.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.b_1 {
color: blue;
}
3 changes: 3 additions & 0 deletions examples/7-content-mapper/src/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* {
margin: 0;
}
8 changes: 8 additions & 0 deletions examples/7-content-mapper/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import './global.css';
import styles from './a.module.css';

styles.a_1;
styles.b_1;
styles.primary;
styles['fade-in'];
styles.unknown; // Expected TS2339 error
19 changes: 19 additions & 0 deletions examples/7-content-mapper/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "es2015",
"lib": ["ES2015"],
"module": "Preserve",
"moduleResolution": "bundler",

"noEmit": true,
"incremental": false,
"types": [] // Simplify tsserver.log
},
"contentMappers": [
{
"package": "@css-modules-kit/content-mapper",
"extensions": [".css"]
}
]
}
5 changes: 5 additions & 0 deletions examples/8-content-mapper-named-exports/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"js/ts.experimental.useTsgo": true,
// Enables the content mapper inspector ("TypeScript: Show Content Mapper Virtual Documents").
"js/ts.showDebugInfo": true
}
14 changes: 14 additions & 0 deletions examples/8-content-mapper-named-exports/src/a.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@import './b.module.css';
@value primary: #2864f0;

.a_1 {
color: primary;
composes: b_1 from './b.module.css';
animation-name: fade-in;
}

@keyframes fade-in {
from {
opacity: 0;
}
}
3 changes: 3 additions & 0 deletions examples/8-content-mapper-named-exports/src/b.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.b_1 {
color: blue;
}
3 changes: 3 additions & 0 deletions examples/8-content-mapper-named-exports/src/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* {
margin: 0;
}
8 changes: 8 additions & 0 deletions examples/8-content-mapper-named-exports/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import './global.css';
import * as styles from './a.module.css';

styles.a_1;
styles.b_1;
styles.primary;
styles['fade-in'];
styles.unknown; // Expected TS2339 error
22 changes: 22 additions & 0 deletions examples/8-content-mapper-named-exports/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "es2015",
"lib": ["ES2015"],
"module": "Preserve",
"moduleResolution": "bundler",

"noEmit": true,
"incremental": false,
"types": [] // Simplify tsserver.log
},
"contentMappers": [
{
"package": "@css-modules-kit/content-mapper",
"extensions": [".css"],
"options": {
"namedExports": true
}
}
]
}
135 changes: 135 additions & 0 deletions packages/content-mapper/e2e-test/broken-css-module.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import dedent from 'dedent';
import { describe, expect, test } from 'vite-plus/test';
import { buildStylesImport, buildTSConfigJSON } from '../src/test/builder.js';
import { fixtureDir, setupFixture } from './test-util/fixture.js';
import { launchLSPClient } from './test-util/lsp-client.js';

const client = launchLSPClient(fixtureDir);

describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => {
test('returns the token definition even when the file contains invalid syntax elsewhere', async () => {
const { iff, getFileLocation, getFileSpan } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'index.ts': dedent`
${buildStylesImport('./a.module.css', { namedExports })}
styles.a_1;
`,
'a.module.css': dedent`
.a_1 { color: red; }
.a_2 {
`,
});
await client.openFiles([iff.paths['index.ts']]);

const definitions = await client.sendDefinition(getFileLocation('index.ts', 'a_1'));

expect(definitions).toStrictEqual([getFileSpan('a.module.css', 'a_1')]);

Check failure on line 26 in packages/content-mapper/e2e-test/broken-css-module.test.ts

View workflow job for this annotation

GitHub Actions / test (24.15.0, windows-11-arm, 17)

[e2e] packages/content-mapper/e2e-test/broken-css-module.test.ts > namedExports: true > returns the token definition even when the file contains invalid syntax elsewhere

AssertionError: expected [ { …(2) } ] to strictly equal [ { …(2) } ] - Expected + Received @@ -1,8 +1,8 @@ [ { - "file": "C:/Users/RUNNER~1/AppData/Local/Temp/@css-modules-kit/content-mapper/2/b003b032-9a2e-4d2c-bf68-d9c9f8464bf1/a.module.css", + "file": "c:/Users/RUNNER~1/AppData/Local/Temp/@css-modules-kit/content-mapper/2/b003b032-9a2e-4d2c-bf68-d9c9f8464bf1/a.module.css", "range": { "end": { "character": 4, "line": 0, }, ❯ packages/content-mapper/e2e-test/broken-css-module.test.ts:26:25

Check failure on line 26 in packages/content-mapper/e2e-test/broken-css-module.test.ts

View workflow job for this annotation

GitHub Actions / test (24.15.0, windows-11-arm, 17)

[e2e] packages/content-mapper/e2e-test/broken-css-module.test.ts > namedExports: false > returns the token definition even when the file contains invalid syntax elsewhere

AssertionError: expected [ { …(2) } ] to strictly equal [ { …(2) } ] - Expected + Received @@ -1,8 +1,8 @@ [ { - "file": "C:/Users/RUNNER~1/AppData/Local/Temp/@css-modules-kit/content-mapper/2/37e4ded1-c43b-41f6-93d4-224d98893121/a.module.css", + "file": "c:/Users/RUNNER~1/AppData/Local/Temp/@css-modules-kit/content-mapper/2/37e4ded1-c43b-41f6-93d4-224d98893121/a.module.css", "range": { "end": { "character": 4, "line": 0, }, ❯ packages/content-mapper/e2e-test/broken-css-module.test.ts:26:25
});

test('returns success responses even when all token importers form a cycle', async () => {
const { iff, getFileLocation } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'index.ts': dedent`
${buildStylesImport('./a.module.css', { namedExports })}
styles.b_1;
`,
'a.module.css': `@import './b.module.css';`,
'b.module.css': dedent`
@import './a.module.css';
.b_1 { color: red; }
`,
});
await client.openFiles([iff.paths['index.ts'], iff.paths['a.module.css'], iff.paths['b.module.css']]);

const cases = [
['from the TS-side styles.<token>', getFileLocation('index.ts', 'b_1')],
['from the token definition', getFileLocation('b.module.css', 'b_1')],
['from the specifier of the first all token importer', getFileLocation('a.module.css', "'./b.module.css'")],
['from the specifier of the second all token importer', getFileLocation('b.module.css', "'./a.module.css'")],
] as const;
for (const [name, origin] of cases) {
await expect.soft(client.sendDefinition(origin), name).resolves.toBeDefined();
await expect.soft(client.sendReferences(origin), name).resolves.toBeDefined();
await expect.soft(client.sendRename(origin), name).resolves.toBeDefined();
}
});

test('returns success responses even when named token importers form a cycle', async () => {
const { iff, getFileLocation } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'index.ts': dedent`
${buildStylesImport('./a.module.css', { namedExports })}
styles.unknown;
`,
'a.module.css': `@value unknown from './b.module.css';`,
'b.module.css': `@value unknown from './a.module.css';`,
});
await client.openFiles([iff.paths['index.ts'], iff.paths['a.module.css'], iff.paths['b.module.css']]);

const cases = [
['from the TS-side styles.<name>', getFileLocation('index.ts', 'unknown')],
['from the <name> of the first named token importer', getFileLocation('a.module.css', 'unknown')],
['from the <name> of the second named token importer', getFileLocation('b.module.css', 'unknown')],
] as const;
for (const [name, origin] of cases) {
await expect.soft(client.sendDefinition(origin), name).resolves.toBeDefined();
await expect.soft(client.sendReferences(origin), name).resolves.toBeDefined();
await expect.soft(client.sendRename(origin), name).resolves.toBeDefined();
}
});

test('returns success responses even when a specifier cannot be resolved', async () => {
const { iff, getFileLocation } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'a.module.css': dedent`
@import './unresolvable-1.module.css';
@value unknown_1 from './unresolvable-2.module.css';
.a_1 { composes: unknown_2 from './unresolvable-3.module.css'; }
`,
});
await client.openFiles([iff.paths['a.module.css']]);

const cases = [
['from the <name>', getFileLocation('a.module.css', 'unknown_1')],
['from the external token reference', getFileLocation('a.module.css', 'unknown_2')],
['from the specifier of an all token importer', getFileLocation('a.module.css', "'./unresolvable-1.module.css'")],
[
'from the specifier of a named token importer',
getFileLocation('a.module.css', "'./unresolvable-2.module.css'"),
],
[
'from the specifier of an external token reference',
getFileLocation('a.module.css', "'./unresolvable-3.module.css'"),
],
] as const;
for (const [name, origin] of cases) {
await expect.soft(client.sendDefinition(origin), name).resolves.toBeDefined();
await expect.soft(client.sendReferences(origin), name).resolves.toBeDefined();
await expect.soft(client.sendRename(origin), name).resolves.toBeDefined();
}
});

test('returns success responses even when a token does not exist', async () => {
const { iff, getFileLocation } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'a.module.css': dedent`
@value b_2 from './b.module.css';
.a_1 { composes: a_2; }
.a_3 { composes: b_3 from './b.module.css'; }
`,
'b.module.css': `.b_1 { color: red; }`,
});
await client.openFiles([iff.paths['a.module.css'], iff.paths['b.module.css']]);

const cases = [
['from the <name>', getFileLocation('a.module.css', 'b_2')],
['from the local token reference', getFileLocation('a.module.css', 'a_2')],
['from the external token reference', getFileLocation('a.module.css', 'b_3')],
] as const;
for (const [name, origin] of cases) {
await expect.soft(client.sendDefinition(origin), name).resolves.toBeDefined();
await expect.soft(client.sendReferences(origin), name).resolves.toBeDefined();
await expect.soft(client.sendRename(origin), name).resolves.toBeDefined();
}
});
});
Loading
Loading