Skip to content

Commit 1e9fc0d

Browse files
feat: type generation mode declaration. (#72)
1 parent dcfde6e commit 1e9fc0d

37 files changed

Lines changed: 4282 additions & 80 deletions

.github/workflows/playwright.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,28 @@ jobs:
3535
packages/playwright/playwright-report
3636
if-no-files-found: ignore
3737
retention-days: 7
38+
e2e-windows:
39+
runs-on: windows-latest
40+
steps:
41+
- name: Checkout
42+
uses: actions/checkout@v4.2.2
43+
- name: Setup Node
44+
uses: actions/setup-node@v4.3.0
45+
with:
46+
node-version: '24.11.1'
47+
- name: Install Dependencies
48+
run: npm ci
49+
- name: Install Playwright Browsers
50+
run: npx playwright install --with-deps chromium webkit
51+
- name: Run Playwright
52+
run: npm run test:e2e
53+
- name: Upload Playwright artifacts
54+
if: always()
55+
uses: actions/upload-artifact@v4.4.3
56+
with:
57+
name: playwright-artifacts-windows
58+
path: |
59+
packages/playwright/test-results
60+
packages/playwright/playwright-report
61+
if-no-files-found: ignore
62+
retention-days: 7

.gitignore

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
*.tgz
22
node_modules/
3-
dist/
4-
dist-webpack/
5-
dist-auto-stable/
6-
dist-hashed/
7-
dist-bridge/
8-
dist-bridge-webpack/
3+
dist*/
94
coverage/
105
.c8/
116
.duel-cache/
@@ -14,7 +9,6 @@ coverage/
149
playwright-report/
1510
test-results/
1611
blob-report/
17-
.knighted-css/
18-
.knighted-css-auto/
19-
.knighted-css-hashed/
12+
.knighted-css*/
2013
packages/playwright/src/**/*.knighted-css.ts
14+
packages/playwright/src/mode/**/*.d.ts

docs/loader.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Loader hook (`?knighted-css`)
1+
# Loader hook
22

33
`@knighted/css/loader` lets bundlers attach compiled CSS strings to any module by appending the `?knighted-css` query when importing. The loader mirrors the module graph, compiles every CSS dialect it discovers (CSS, Sass, Less, vanilla-extract, etc.), and exposes the concatenated result as `knightedCss`.
44

@@ -34,6 +34,66 @@ export default {
3434
}
3535
```
3636

37+
### Choosing a type generation mode
38+
39+
`knighted-css-generate-types` supports two modes. Both are fully supported and tested; the right choice depends on how explicit you want the imports to be versus how much resolver automation you want to lean on:
40+
41+
- `--mode module` (double-extension imports): use `.knighted-css` sidecar modules such as
42+
`import stableSelectors from './button.css.knighted-css.js'`. This keeps resolution explicit and tends to be the most stable under large, complex builds.
43+
- `--mode declaration` (idiomatic imports): emit `.d.ts` sidecars next to the original JS/TS module
44+
and keep clean imports like `import { knightedCss } from './button.js'`. This is cleaner at call sites, but it adds resolver work at build time and depends on the resolver plugin to stay in sync.
45+
46+
If you want the simplest, most transparent build behavior, start with `--mode module`.
47+
If you want cleaner imports and are comfortable with resolver automation, choose `--mode declaration` and enable strict sidecars + a manifest for safety.
48+
49+
### Resolver plugin (declaration mode)
50+
51+
When you use `knighted-css-generate-types --mode declaration`, TypeScript expects the
52+
augmented exports to be present on the original JS/TS module. Use the resolver plugin
53+
to automatically append `?knighted-css` for any module import that has a generated
54+
sidecar `.d.ts` file.
55+
56+
```js
57+
// rspack.config.js
58+
import { knightedCssResolverPlugin } from '@knighted/css/plugin'
59+
60+
export default {
61+
resolve: {
62+
plugins: [knightedCssResolverPlugin()],
63+
},
64+
}
65+
```
66+
67+
```js
68+
// webpack.config.js
69+
const { knightedCssResolverPlugin } = require('@knighted/css/plugin')
70+
71+
module.exports = {
72+
resolve: {
73+
plugins: [knightedCssResolverPlugin()],
74+
},
75+
}
76+
```
77+
78+
If you use declaration mode, consider enabling strict sidecar detection with a manifest. This
79+
ensures only `.d.ts` files generated by `knighted-css-generate-types` trigger rewrites:
80+
81+
```js
82+
import path from 'node:path'
83+
import { knightedCssResolverPlugin } from '@knighted/css/plugin'
84+
85+
export default {
86+
resolve: {
87+
plugins: [
88+
knightedCssResolverPlugin({
89+
strictSidecar: true,
90+
manifestPath: path.resolve('.knighted-css/knighted-manifest.json'),
91+
}),
92+
],
93+
},
94+
}
95+
```
96+
3797
> [!NOTE]
3898
> The loader shares the same auto-configured `oxc-resolver` as the standalone `css()` API, so hash-prefixed specifiers declared under `package.json#imports` (for example, `#ui/button`) resolve without additional options.
3999

docs/type-generation.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ Wire it into `postinstall` or your build so new selectors land automatically.
2929
- `--auto-stable` – enable auto-stable selector generation during extraction (mirrors the loader’s auto-stable behavior).
3030
- `--hashed` – emit proxy modules that export `selectors` backed by loader-bridge hashed class names (mutually exclusive with `--auto-stable`).
3131
- `--resolver` – path or package name exporting a `CssResolver` (default export or named `resolver`).
32+
- `--mode``module` (default) emits `.knighted-css.ts` proxy modules. `declaration` emits `.d.ts` module augmentations next to the referenced JS/TS modules, so you can keep standard imports like `import { knightedCss } from './button.js'` while the generator still discovers them via `.knighted-css` specifiers.
33+
- `--manifest` – optional path to write a sidecar manifest for declaration mode (recommended when you want strict resolver behavior).
34+
35+
### Mode quick reference
36+
37+
| Mode | Import style | Generated files | Bundler resolver plugin | Best for |
38+
| ------------------ | ---------------------------------- | ------------------------------------- | --------------------------------- | -------------------------------------------------- |
39+
| `module` (default) | Double-extension (`.knighted-css`) | `.knighted-css.*` proxy modules | Not required | Maximum transparency and stability in large builds |
40+
| `declaration` | Plain JS/TS imports | `.d.ts` augmentations next to modules | Required (append `?knighted-css`) | Cleaner imports when you accept resolver overhead |
41+
42+
If you use declaration mode, prefer enabling strict sidecars + a manifest so the resolver only rewrites imports that the CLI generated.
3243

3344
### Relationship to the loader
3445

@@ -54,6 +65,53 @@ stableSelectors.card // "knighted-card"
5465
knightedCss // compiled CSS string
5566
```
5667

68+
## Declaration mode (augment existing modules)
69+
70+
Declaration mode emits `.d.ts` files instead of `.knighted-css.ts` proxies, so you can import directly from the module:
71+
72+
```sh
73+
knighted-css-generate-types --root . --include src --mode declaration
74+
```
75+
76+
```ts
77+
import Button, { knightedCss, stableSelectors } from './button.js'
78+
```
79+
80+
> [!IMPORTANT]
81+
> Declaration mode requires a resolver plugin to append `?knighted-css` (and `&combined` when applicable)
82+
> at build time so runtime exports match the generated types.
83+
84+
### Sidecar manifests + strict resolver mode
85+
86+
Declaration mode emits `.d.ts` files with a `// @knighted-css` marker. If you want the resolver plugin
87+
to only opt into those explicit sidecars (and avoid accidentally matching unrelated `.d.ts` files),
88+
enable strict mode and pass a manifest created by the CLI:
89+
90+
```sh
91+
knighted-css-generate-types --root . --include src --mode declaration \
92+
--manifest .knighted-css/knighted-manifest.json
93+
```
94+
95+
```js
96+
import path from 'node:path'
97+
import { knightedCssResolverPlugin } from '@knighted/css/plugin'
98+
99+
export default {
100+
resolve: {
101+
plugins: [
102+
knightedCssResolverPlugin({
103+
strictSidecar: true,
104+
manifestPath: path.resolve('.knighted-css/knighted-manifest.json'),
105+
}),
106+
],
107+
},
108+
}
109+
```
110+
111+
The manifest maps each source module to its generated `.d.ts` path. When `strictSidecar` is enabled,
112+
the plugin only rewrites imports if the sidecar exists **and** includes the marker. That keeps
113+
resolution deterministic even when other tooling generates `.d.ts` files alongside your modules.
114+
57115
## Hashed selector proxies
58116

59117
Use `--hashed` when you want `.knighted-css` proxy modules to export `selectors` backed by

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/css/README.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ I needed a single source of truth for UI components that could drop into both li
3030
- Deterministic selector duplication via `autoStable`: duplicate matching class selectors with a stable namespace (default `knighted-`) in both plain CSS and CSS Modules exports.
3131
- Pluggable resolver/filter hooks for custom module resolution (e.g., Rspack/Vite/webpack aliases) or selective inclusion.
3232
- First-class loader (`@knighted/css/loader`) so bundlers can import compiled CSS alongside their modules via `?knighted-css`.
33-
- Built-in type generation CLI (`knighted-css-generate-types`) that emits `.knighted-css.*` selector manifests so TypeScript gets literal tokens in lockstep with the loader exports.
33+
- Built-in type generation CLI (`knighted-css-generate-types`) that emits `.knighted-css.*` selector manifests (module mode) or declaration augmentations (declaration mode) so TypeScript stays in lockstep with loader exports.
3434

3535
## Requirements
3636

@@ -107,7 +107,7 @@ See [docs/loader.md](../../docs/loader.md) for the full configuration, combined
107107

108108
### Type generation hook (`*.knighted-css*`)
109109

110-
Run `knighted-css-generate-types` so every specifier that ends with `.knighted-css` produces a sibling manifest containing literal selector tokens:
110+
Run `knighted-css-generate-types` so every specifier that ends with `.knighted-css` produces a sibling manifest containing literal selector tokens (module mode, the default):
111111

112112
```ts
113113
import stableSelectors from './button.module.scss.knighted-css.js'
@@ -142,6 +142,53 @@ selectors.card // hashed CSS Modules class name
142142
> include class names that are not exported by the module (e.g. sprinkles output), while the
143143
> runtime `selectors` map only includes exported locals from the loader bridge.
144144
145+
Prefer module-level imports without the double extension? Use declaration mode to emit `.d.ts` augmentations next to JS/TS modules that import styles:
146+
147+
```sh
148+
knighted-css-generate-types --root . --include src --mode declaration
149+
```
150+
151+
```ts
152+
import Button, { knightedCss, stableSelectors } from './button.js'
153+
```
154+
155+
See [docs/type-generation.md](../../docs/type-generation.md#mode-quick-reference) for a quick comparison of module vs declaration mode tradeoffs.
156+
157+
> [!IMPORTANT]
158+
> Declaration mode requires a resolver plugin to append `?knighted-css` (and `&combined` when applicable)
159+
> at build time so runtime exports match the generated types.
160+
161+
Install the resolver plugin via `@knighted/css/plugin` and wire it into your bundler resolver:
162+
163+
```js
164+
// rspack.config.js
165+
import { knightedCssResolverPlugin } from '@knighted/css/plugin'
166+
167+
export default {
168+
resolve: {
169+
plugins: [knightedCssResolverPlugin()],
170+
},
171+
}
172+
```
173+
174+
If you want the resolver to only match sidecars generated by the CLI, enable strict mode and provide a manifest (written by `knighted-css-generate-types --manifest`):
175+
176+
```js
177+
import path from 'node:path'
178+
import { knightedCssResolverPlugin } from '@knighted/css/plugin'
179+
180+
export default {
181+
resolve: {
182+
plugins: [
183+
knightedCssResolverPlugin({
184+
strictSidecar: true,
185+
manifestPath: path.resolve('.knighted-css/knighted-manifest.json'),
186+
}),
187+
],
188+
},
189+
}
190+
```
191+
145192
Refer to [docs/type-generation.md](../../docs/type-generation.md) for CLI options and workflow tips.
146193

147194
### Combined + runtime selectors

packages/css/package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@knighted/css",
3-
"version": "1.1.1",
3+
"version": "1.2.0-rc.0",
44
"description": "A build-time utility that traverses JavaScript/TypeScript module dependency graphs to extract, compile, and optimize all imported CSS into a single, in-memory string.",
55
"type": "module",
66
"main": "./dist/css.js",
@@ -22,6 +22,9 @@
2222
"generate-types": [
2323
"./dist/generateTypes.d.ts"
2424
],
25+
"plugin": [
26+
"./dist/plugin.d.ts"
27+
],
2528
"*": [
2629
"./types.d.ts"
2730
]
@@ -57,6 +60,11 @@
5760
"import": "./dist/generateTypes.js",
5861
"default": "./dist/generateTypes.js"
5962
},
63+
"./plugin": {
64+
"types": "./dist/plugin.d.ts",
65+
"import": "./dist/plugin.js",
66+
"require": "./dist/cjs/plugin.cjs"
67+
},
6068
"./stableSelectors": {
6169
"types": "./dist/stableSelectors.d.ts",
6270
"import": "./dist/stableSelectors.js",
@@ -87,7 +95,6 @@
8795
},
8896
"scripts": {
8997
"build": "duel && node ./scripts/copy-types-stub.js",
90-
"pretest": "npm run build",
9198
"check-types": "tsc --noEmit --project tsconfig.json && tsc --noEmit --project tsconfig.tests.json",
9299
"test": "c8 --reporter=text --reporter=text-summary --reporter=lcov --include \"src/**/*.ts\" tsx --test test/**/*.test.ts",
93100
"prepack": "npm run build"

0 commit comments

Comments
 (0)