Skip to content

Commit 12daec6

Browse files
feat: unified proxy cli generation. (#57)
1 parent 99eaebc commit 12daec6

15 files changed

Lines changed: 505 additions & 82 deletions

File tree

.github/instructions/pr-review.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
name: PR review instructions
3+
description: Guidance for Copilot when reviewing pull requests.
4+
applyTo: '**/*'
5+
---
6+
7+
When reviewing PRs in this repository:
8+
9+
- Focus on correctness, edge cases, and TypeScript type safety.
10+
- Call out potential bugs, duplicated logic, and opportunities to simplify.
11+
- Verify generated artifacts (dist, coverage, test-results, .knighted-css) are not modified.
12+
- Prefer minimal, localized changes and preserve existing public APIs.
13+
- Check for new or missing tests when behavior changes.
14+
- Flag NodeNext/ESM import correctness, especially extension handling.
15+
- Call out performance regressions in graph walking or CSS extraction.
16+
- Note any security or path traversal risks in resolver or loader changes.
17+
- Ensure docs/readme updates match behavior changes when user-facing.
18+
- Summarize risks and required follow-ups clearly.

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ jobs:
3838
run: npm run check:cycles
3939
- name: Test
4040
run: npm test
41+
- name: Generate Playwright Types
42+
run: npm run types -w @knighted/css-playwright-fixture
4143
- name: Check Types
4244
run: npm run check-types
4345
- name: Report Coverage

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ test-results/
1313
blob-report/
1414
.knighted-css/
1515
.knighted-css-auto/
16+
packages/playwright/src/**/*.knighted-css.ts

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Workspace-scoped (preferred when changing @knighted/css only):
5151
- ESM only (`type: module`).
5252
- Prettier: single quotes, no semicolons, `printWidth: 90`, `arrowParens: avoid`.
5353
- Keep functions small and side-effect aware; prefer pure helpers where possible.
54+
- Prefer multiline comment style (`/* ... */`) when a comment spans more than one line.
5455

5556
### Example style (good)
5657

docs/combined-queries.md

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Knighted CSS Combined Loader Reference
22

3-
This document summarizes how `?knighted-css&combined` behaves for different module export shapes and how to structure your imports accordingly. Use it as guidance when filing documentation feedback for `@knighted/css`.
3+
This document summarizes how `?knighted-css&combined` behaves for different module export shapes and how to structure your imports accordingly, plus how the double-extension proxy import replaces most combined-query use cases.
44

55
> [!NOTE]
6-
> TypeScript reads literal selector tokens from the generated `.knighted-css.ts` modules (emitted by `knighted-css-generate-types`). Append `&types` to combined imports only when you also need `stableSelectors` at runtime—the loader still exports the map, while the double-extension modules keep your editors in sync.
6+
> TypeScript reads literal selector tokens from the generated `.knighted-css.ts` modules (emitted by `knighted-css-generate-types`). The double-extension modules also re-export the original module exports and `knightedCss`, so a single import can provide component exports, typed selectors, and the compiled stylesheet string without helper casts.
77
88
> [!TIP]
9-
> Prefer importing `asKnightedCssCombinedModule` from `@knighted/css/loader-helpers` when you want a runtime helper—the file has zero Node dependencies, so both browser and Node builds stay green.
9+
> Prefer the double-extension import when you run `knighted-css-generate-types`—it removes the need for `asKnightedCssCombinedModule`. Use the helper only for `?knighted-css&combined` queries or when you cannot rely on generated proxy modules.
1010
1111
## Decision Matrix
1212

@@ -43,6 +43,12 @@ const { Component, knightedCss } =
4343
> [!NOTE]
4444
> Namespace imports (`import * as combined …`) are the most reliable pattern for `&named-only` queries because you intentionally drop the default export. Keep using the helper type to narrow the namespace.
4545
46+
If you run `knighted-css-generate-types`, use the unified proxy import instead:
47+
48+
```ts
49+
import { Component, knightedCss, stableSelectors } from './module.knighted-css.js'
50+
```
51+
4652
## Default export only
4753

4854
_Use when your component only exposes a default export and you want `knightedCss` beside it._
@@ -55,6 +61,12 @@ const { default: Component, knightedCss } =
5561
asKnightedCssCombinedModule<typeof import('./module.js')>(combinedModule)
5662
```
5763

64+
With generated proxies, use a single import instead:
65+
66+
```ts
67+
import Component, { knightedCss, stableSelectors } from './module.knighted-css.js'
68+
```
69+
5870
## Default and named exports
5971

6072
_Use when you consume both the default export and its named helpers from the same module._
@@ -70,12 +82,20 @@ const {
7082
} = asKnightedCssCombinedModule<typeof import('./module.js')>(combinedModule)
7183
```
7284

85+
With generated proxies, a single import covers everything:
86+
87+
```ts
88+
import Component, { helper, knightedCss, stableSelectors } from './module.knighted-css.js'
89+
```
90+
7391
Prefer `?knighted-css&combined&named-only` plus the [named exports only](#named-exports-only) snippet when you intentionally avoid default exports but still need the named members and `knightedCss`.
7492

7593
## Adding stable selectors (`&types`)
7694

7795
Append `&types` whenever you need the runtime `stableSelectors` map in addition to `knightedCss`. Configure the loader’s `stableNamespace` option (or pass `--stable-namespace` to the CLI) so runtime exports and generated `.knighted-css.ts` modules stay aligned.
7896

97+
If you run `knighted-css-generate-types`, the double-extension proxy already exports `stableSelectors` and keeps the literal types in sync—no `&types` query or helper required.
98+
7999
### Named exports with stable selectors
80100

81101
_Use when you only consume named exports (no default) but still want typed `stableSelectors`._
@@ -93,6 +113,12 @@ const { Component, knightedCss, stableSelectors } = asKnightedCssCombinedModule<
93113
stableSelectors.card // "knighted-card"
94114
```
95115

116+
With generated proxies, use a single import instead:
117+
118+
```ts
119+
import { Component, knightedCss, stableSelectors } from './module.knighted-css.js'
120+
```
121+
96122
> [!TIP]
97123
> Add `&named-only` before `&types` to drop the synthetic default export while still receiving `stableSelectors`.
98124
@@ -117,6 +143,12 @@ const {
117143
stableSelectors.badge // "knighted-badge"
118144
```
119145

146+
With generated proxies, use a single import instead:
147+
148+
```ts
149+
import Component, { knightedCss, stableSelectors } from './module.knighted-css.js'
150+
```
151+
120152
### Default and named exports with stable selectors
121153

122154
_Use when you consume every export from the module and still want the selector map._
@@ -139,16 +171,22 @@ const {
139171
stableSelectors.card // "knighted-card" (or your configured namespace)
140172
```
141173

174+
With generated proxies, a single import covers everything:
175+
176+
```ts
177+
import Component, { helper, knightedCss, stableSelectors } from './module.knighted-css.js'
178+
```
179+
142180
## Key Takeaways
143181

144182
- The loader always injects `knightedCss` alongside the module’s exports.
145-
- To avoid synthetic defaults (and TypeScript warnings) for modules that only expose named exports, add `&named-only` and use a namespace import.
146-
- Namespace imports plus `KnightedCssCombinedModule<typeof import('./module')>` work universally; default imports are optional conveniences when the source module exposes a default you actually consume.
147-
- Add `&types` when you also need the `stableSelectors` map. Configure the namespace globally (loader option or CLI flag) so runtime + generated types stay consistent.
183+
- When you run `knighted-css-generate-types`, prefer the double-extension proxy import to get component exports, `knightedCss`, and `stableSelectors` in one place.
184+
- Use `?knighted-css&combined` (plus `asKnightedCssCombinedModule`) when you need runtime combined imports without generated proxy modules.
185+
- Add `&types` only when you need a runtime selector map and cannot rely on the generated proxy files.
148186

149187
## When to use `KnightedCssCombinedModule`
150188

151-
The helper type still earns its keep when you need to narrow combined results without `asKnightedCssCombinedModule` (for example, in test doubles or custom wrappers):
189+
The helper type still earns its keep when you need to narrow combined results without `asKnightedCssCombinedModule` (for example, in test doubles, custom wrappers, or when you skip generated proxies):
152190

153191
```ts
154192
import type { KnightedCssCombinedModule } from '@knighted/css/loader'

docs/type-generation.md

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Type generation (`*.knighted-css*`)
22

3-
Use the `knighted-css-generate-types` CLI to create selector manifests that TypeScript can import. The CLI scans for specifiers ending in `.knighted-css` (for example `./button.module.scss.knighted-css.ts`), compiles the stylesheet once, and writes a sibling module that exports the literal selector tokens.
3+
Use the `knighted-css-generate-types` CLI to generate modules for `.knighted-css` double-extension imports. For stylesheets, it emits a sibling module with literal selector tokens. For JavaScript/TypeScript module specifiers, the generated file acts as a unified proxy that re-exports the module’s exports plus `knightedCss`.
44

55
## Running the CLI
66

@@ -20,6 +20,21 @@ Typical script entry:
2020

2121
Wire it into `postinstall` or your build so new selectors land automatically.
2222

23+
### Options
24+
25+
- `--root` / `-r` – project root (defaults to `process.cwd()`).
26+
- `--include` / `-i` – additional directories or files to scan (repeatable).
27+
- `--out-dir` – directory for the selector module manifest cache (defaults to `<root>/.knighted-css`).
28+
- `--stable-namespace` – namespace prefix shared by the generated selector maps and loader runtime.
29+
- `--auto-stable` – enable auto-stable selector generation during extraction (mirrors the loader’s auto-stable behavior).
30+
31+
### Relationship to the loader
32+
33+
- `.knighted-css*` imports include the generated selector map and, for module specifiers, re-exports plus `knightedCss`.
34+
- `?knighted-css` imports are purely runtime (see [docs/loader.md](./loader.md)). Append `&types` only when you also need the selector map at runtime; the compiler still reads the literal tokens from the generated modules.
35+
36+
For CSS Modules or Sass files that need stable selectors, import the generated `.knighted-css` module for types. For JS/TS component modules, the generated proxy already provides `knightedCss` alongside the exports, so you can rely on the proxy in place of separate `?knighted-css` runtime imports when appropriate.
37+
2338
## Minimal usage
2439

2540
```ts
@@ -28,18 +43,50 @@ import selectors from './button.module.scss.knighted-css.js'
2843
selectors.card // "knighted-card"
2944
```
3045

46+
## Unified proxy usage (module exports + CSS + selectors)
47+
48+
```ts
49+
import Button, { knightedCss, stableSelectors } from './button.knighted-css.js'
50+
51+
stableSelectors.card // "knighted-card"
52+
knightedCss // compiled CSS string
53+
```
54+
3155
Because the generated module lives next to the source stylesheet, TypeScript’s normal resolution logic applies—no custom `paths` entries required. Use the manifest in conjunction with runtime helpers such as `mergeStableClass` or `stableClassName` to keep hashed class names in sync.
3256

33-
### Options
57+
## Rspack watch hook
3458

35-
- `--root` / `-r` – project root (defaults to `process.cwd()`).
36-
- `--include` / `-i` – additional directories or files to scan (repeatable).
37-
- `--out-dir` – directory for the selector module manifest cache (defaults to `<root>/.knighted-css`).
38-
- `--stable-namespace` – namespace prefix shared by the generated selector maps and loader runtime.
59+
If you want the CLI to rerun during dev, hook it into Rspack’s watch pipeline. This keeps the generated `.knighted-css` proxy modules in sync whenever source files change. You can also scope the `--include` list using `compiler.modifiedFiles` to avoid rescanning the entire project on every rebuild.
3960

40-
### Relationship to the loader
61+
```js
62+
// rspack.config.js
63+
import { exec } from 'node:child_process'
4164

42-
- `.knighted-css*` imports are purely for types; they never include the compiled CSS string.
43-
- `?knighted-css` imports are purely runtime (see [docs/loader.md](./loader.md)). Append `&types` only when you also need the selector map at runtime; the compiler still reads the literal tokens from the generated modules.
65+
export default {
66+
// ... your existing config
67+
plugins: [
68+
{
69+
apply(compiler) {
70+
compiler.hooks.watchRun.tapPromise('knighted-css-generate-types', () => {
71+
const modified = Array.from(compiler.modifiedFiles ?? [])
72+
const includes = modified.length > 0 ? modified : ['src']
73+
const includeArgs = includes.flatMap(entry => ['--include', entry])
74+
const command = ['knighted-css-generate-types', '--root', '.', ...includeArgs]
75+
76+
return new Promise((resolve, reject) => {
77+
exec(command.join(' '), error => {
78+
if (error) {
79+
reject(error)
80+
return
81+
}
82+
resolve()
83+
})
84+
})
85+
})
86+
},
87+
},
88+
],
89+
}
90+
```
4491
45-
Keep both hooks in mind when authoring CSS Modules or Sass files that need stable selectors: import the generated module for types, and import the loader query when you need the runtime stylesheet.
92+
Scope the `--include` paths to the folders that actually import `.knighted-css` to keep the watch step fast. When `modifiedFiles` is empty (for example on the first run), fall back to a stable include root like `src`.

packages/css/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ Run `knighted-css-generate-types` so every specifier that ends with `.knighted-c
113113
import stableSelectors from './button.module.scss.knighted-css.js'
114114
```
115115

116+
When the `.knighted-css` import targets a JavaScript/TypeScript module, the generated proxy also re-exports the modules exports and `knightedCss`, so a single import can provide component exports, typed selectors, and the compiled stylesheet string:
117+
118+
```ts
119+
import Button, { knightedCss, stableSelectors } from './button.knighted-css.js'
120+
```
121+
116122
Refer to [docs/type-generation.md](../../docs/type-generation.md) for CLI options and workflow tips.
117123

118124
### Combined + runtime selectors
@@ -136,6 +142,9 @@ const {
136142
stableSelectors.shell
137143
```
138144

145+
> [!TIP]
146+
> If you run `knighted-css-generate-types`, prefer the double-extension proxy import shown above instead of `?knighted-css&combined` and `asKnightedCssCombinedModule`.
147+
139148
> [!NOTE]
140149
> `stableSelectors` here is for runtime use; TypeScript still reads literal tokens from the generated `.knighted-css.*` modules. For a full decision matrix, see [docs/combined-queries.md](../../docs/combined-queries.md).
141150
> Prefer importing `asKnightedCssCombinedModule` from `@knighted/css/loader-helpers` instead of grabbing it from `@knighted/css/loader`the helper lives in a Node-free chunk so both browser and server bundles stay happy.

0 commit comments

Comments
 (0)