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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ jobs:
tsconfig.tsbuildinfo
key: test-tools-${{ runner.arch }}-${{ runner.os }}-node-${{ matrix.node }}-stylelint-${{ matrix.stylelint-version }}-${{ github.sha }}
restore-keys: test-tools-${{ runner.arch }}-${{ runner.os }}-node-${{ matrix.node }}-stylelint-${{ matrix.stylelint-version }}
# The tsgo binary built by scripts/setup-tsgo.sh, used by the content-mapper e2e tests.
# The e2e test setup skips the build when the binary exists, so a stale binary must
# never be restored. Keying on the hash of setup-tsgo.sh (which contains the pinned
# commit) with no restore-keys guarantees that.
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .tmp/typescript-go/built
key: tsgo-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('scripts/setup-tsgo.sh') }}
- run: vp test
env:
STYLELINT_VERSION: ${{ matrix.stylelint-version }}
Expand Down
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/
24 changes: 24 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,30 @@
"TSS_DEBUG": "5859"
}
},
{
// Launches the TypeScript Native Preview extension built from the content mapper
// PR branch (microsoft/typescript-go#4712). The marketplace build cannot enable
// content mappers, so the extension must be run from the PR branch's source.
"name": "tsgo (7-content-mapper)",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/.tmp/typescript-go/_extension",
"--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-go/_extension/dist/**/*.js"],
"preLaunchTask": "prepare content-mapper example",
"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 example",
"dependsOn": ["vp: build - packages/content-mapper", "setup tsgo extension"],
"group": "build"
}
]
}
3 changes: 3 additions & 0 deletions examples/7-content-mapper/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"js/ts.experimental.useTsgo": 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"]
}
]
}
78 changes: 78 additions & 0 deletions packages/content-mapper/e2e-test/diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import dedent from 'dedent';
import { describe, expect, test } from 'vite-plus/test';
import { buildStylesImport, buildTSConfigJSON } from './test-util/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('reports an unknown property access on a styles binding', async () => {
const { iff, getRange } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'index.ts': dedent`
${buildStylesImport('./a.module.css', { namedExports })}
styles.unknown;
`,
'a.module.css': `.a_1 { color: red; }`,
});
await client.openFile(iff.paths['index.ts']);

const report = await client.sendDocumentDiagnostic(iff.paths['index.ts']);

expect(report.items).toStrictEqual([
expect.objectContaining({ code: 2339, range: getRange('index.ts', 'unknown') }),
]);
});

test('provides the mapper-generated type on the styles binding', async () => {
const { iff } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'index.ts': dedent`
${buildStylesImport('./a.module.css', { namedExports })}
type Expected = { a_1: string };
export const _t: Expected = styles;
`,
'a.module.css': `.a_1 { color: red; }`,
});
await client.openFile(iff.paths['index.ts']);

const report = await client.sendDocumentDiagnostic(iff.paths['index.ts']);

expect(report.items).toStrictEqual([]);
});

// NOTE: Unlike ts-plugin, which reports its own "Cannot import module" diagnostic on the bare
// path, the unresolvable import is reported by TypeScript itself (TS2307) on the quoted
// specifier.
test('reports a semantic diagnostic on a CSS module file', async () => {
const { iff, getRange } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'a.module.css': `@import './unresolvable.module.css';`,
});
await client.openFile(iff.paths['a.module.css']);

const report = await client.sendDocumentDiagnostic(iff.paths['a.module.css']);

expect(report.items).toStrictEqual([
expect.objectContaining({ code: 2307, range: getRange('a.module.css', `'./unresolvable.module.css'`) }),
]);
});

test('reports a syntactic diagnostic on a CSS module file', async () => {
const { iff, getRange } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'a.module.css': `@value;`,
});
await client.openFile(iff.paths['a.module.css']);

const report = await client.sendDocumentDiagnostic(iff.paths['a.module.css']);

expect(report.items).toStrictEqual([
expect.objectContaining({
message: '`@value` is a invalid syntax.',
range: getRange('a.module.css', '@value;'),
}),
]);
});
});
60 changes: 60 additions & 0 deletions packages/content-mapper/e2e-test/file-events.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import dedent from 'dedent';
import { describe, expect, test } from 'vite-plus/test';
import { buildStylesImport, buildTSConfigJSON } from './test-util/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 }) => {
describe('when adding a CSS module', () => {
test("updates the importer's diagnostic when a CSS module is added", async () => {
const { iff, getRange } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'index.ts': dedent`
${buildStylesImport('./a.module.css', { namedExports })}
styles.a_1;
`,
});
await client.openFile(iff.paths['index.ts']);

const before = await client.sendDocumentDiagnostic(iff.paths['index.ts']);
expect(before.items).toStrictEqual([
expect.objectContaining({ code: 2307, range: getRange('index.ts', `'./a.module.css'`) }),
]);

await iff.addFixtures({ 'a.module.css': '.a_1 { color: red; }' });
await client.openFile(iff.join('a.module.css'));

const after = await client.sendDocumentDiagnostic(iff.paths['index.ts']);
expect(after.items).toStrictEqual([]);
});
});

describe('when updating a CSS module', () => {
test("updates the importer's diagnostic when a CSS module is modified", async () => {
const { iff, getRange } = await setupFixture({
'tsconfig.json': buildTSConfigJSON({ mapperOptions: { namedExports } }),
'index.ts': dedent`
${buildStylesImport('./a.module.css', { namedExports })}
styles.a_1;
`,
'a.module.css': '',
});
await client.openFile(iff.paths['index.ts']);

const before = await client.sendDocumentDiagnostic(iff.paths['index.ts']);
expect(before.items).toStrictEqual([expect.objectContaining({ code: 2339, range: getRange('index.ts', 'a_1') })]);

await client.openFile(iff.paths['a.module.css']);
await client.changeFile(iff.paths['a.module.css'], `.a_1 {}`);

const after = await client.sendDocumentDiagnostic(iff.paths['index.ts']);
expect(after.items).toStrictEqual([]);
});
});

describe('when removing a CSS module', () => {
test.todo("updates the importer's diagnostic when a CSS module is removed");
});
});
Loading
Loading