Skip to content

Commit 7ae85d2

Browse files
feat: generate types. (#25)
1 parent d866ece commit 7ae85d2

30 files changed

+2125
-546
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
fail-fast: false
1717
matrix:
1818
node-version:
19-
- '22.15.0'
19+
- '22.17.0'
2020
- '24.11.1'
2121
steps:
2222
- name: Checkout

README.md

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@ CSS Modules hash class names after the loader extracts selectors, so the stylesh
198198
<div className={`${styles['css-modules-badge']} css-modules-badge`}>
199199
```
200200
201+
### Stable selector type generation
202+
203+
Run `npx knighted-css-generate-types --root .` to scan your project for `?knighted-css&types` imports. The CLI:
204+
205+
- extracts selectors via the loader, then writes literal module declarations into `node_modules/@knighted/css/node_modules/.knighted-css`
206+
- updates the packaged stub at `node_modules/@knighted/css/types-stub/index.d.ts`
207+
- exposes the declarations automatically because `types.d.ts` references the stub, so no `tsconfig` wiring is required
208+
209+
Re-run the command whenever imports change (add it to a `types:css` npm script or your build). If you need a different destination, pass `--out-dir` and/or `--types-root` to override the defaults.
210+
201211
Sass/Less projects can import the shared mixins directly:
202212
203213
```scss
@@ -238,15 +248,84 @@ Need a zero-JS approach? Import the optional layer helper and co-locate your fal
238248
239249
Override the namespace via `:root { --knighted-stable-namespace: 'acme'; }` if you want a different prefix in pure CSS.
240250
251+
#### Type-safe selector maps (`?knighted-css&types`)
252+
253+
Append `&types` to any loader import to receive a literal map of the discovered stable selectors alongside the raw CSS:
254+
255+
```ts
256+
import { knightedCss, stableSelectors } from './styles.css?knighted-css&types'
257+
258+
stableSelectors.demo // "knighted-demo"
259+
type StableSelectors = typeof stableSelectors
260+
```
261+
262+
The map ships as `as const`, so every key/value pair is type-safe without additional tooling. Need the combined import? Add the flag there too and destructure everything from one place:
263+
264+
```ts
265+
import type { KnightedCssCombinedModule } from '@knighted/css/loader'
266+
import combined, { stableSelectors } from './button.js?knighted-css&combined&types'
267+
268+
const { knightedCss } = combined as KnightedCssCombinedModule<
269+
typeof import('./button.js')
270+
>
271+
272+
stableSelectors.demo // "knighted-demo"
273+
```
274+
275+
Namespaces default to `knighted`, but you can configure a global fallback via the loader’s `stableNamespace` option:
276+
277+
```js
278+
{
279+
loader: '@knighted/css/loader',
280+
options: {
281+
stableNamespace: 'storybook',
282+
},
283+
}
284+
```
285+
286+
All imports share the namespace resolved by the loader (or the `knighted-css-generate-types` CLI). Use the loader option or CLI flag to align runtime + type generation, and the loader still emits highlighted warnings when the namespace trims to an empty value or when no selectors match. For best editor support, keep `&types` at the end of the query (`?knighted-css&combined&types`, `?knighted-css&combined&named-only&types`, etc.).
287+
241288
#### TypeScript support for loader queries
242289
243290
Loader query types ship directly with `@knighted/css`. Reference them once in your project—either by adding `"types": ["@knighted/css/loader-queries"]` to `tsconfig.json` or dropping `/// <reference types="@knighted/css/loader-queries" />` into a global `.d.ts`—and the following ambient modules become available everywhere:
244291

245292
- `*?knighted-css` imports expose a `knightedCss: string` export.
246-
- `*?knighted-css&combined` (and any query that includes both flags) expose `knightedCss` and return the original module exports, which you can narrow with `KnightedCssCombinedModule` before destructuring named members.
293+
- `*?knighted-css&types` exposes both `knightedCss` and `stableSelectors`, the readonly selector map.
294+
- `*?knighted-css&combined` (plus `&named-only` / `&no-default`) mirror the source module exports while adding `knightedCss`, which you can narrow with `KnightedCssCombinedModule` before destructuring named members.
295+
- `*?knighted-css&combined&types` variants add the same `stableSelectors` map on top of the combined behavior so a single import can surface everything.
247296

248297
No vendor copies are necessarythe declarations live inside `@knighted/css`, you just need to point your TypeScript config at the shipped `loader-queries` subpath once.
249298

299+
#### Generate literal selector types
300+
301+
The runtime `stableSelectors` export is always a literal `as const` map, but TypeScript can only see those exact tokens if your project emits matching `.d.ts` files. Run the bundled CLI whenever you change a module that imports `?knighted-css&types` (or any `&combined&types` variants):
302+
303+
```bash
304+
npx knighted-css-generate-types --root .
305+
```
306+
307+
or wire it into `package.json` for local workflows:
308+
309+
```json
310+
{
311+
"scripts": {
312+
"knighted:types": "knighted-css-generate-types --root . --include src"
313+
}
314+
}
315+
```
316+
317+
The CLI scans every file you include (by default the project root, skipping `node_modules`, `dist`, etc.), finds imports containing `?knighted-css&types`, reuses the loader to extract CSS, and writes deterministic `.d.ts` files into `node_modules/.knighted-css/knt-*.d.ts`. It also maintains `node_modules/@knighted/css/types-stub/index.d.ts`, so TypeScript picks up the generated declarations automaticallyno extra `typeRoots` configuration is required.
318+
319+
Key flags:
320+
321+
- `--root` / `-r`project root (defaults to `process.cwd()`).
322+
- `--include` / `-i`additional directories or files to scan (repeatable).
323+
- `--out-dir`custom output folder for the generated `knt-*` declarations.
324+
- `--types-root`override the `@types` directory used for the aggregator.
325+
- `--stable-namespace`namespace prefix for the generated selector map.
326+
327+
Re-run the CLI (or add it to a pre-build hook) whenever selectors change so new tokens land in the literal declaration files.
328+
250329
#### Combined module + CSS import
251330

252331
If you prefer a single import that returns both your module exports and the compiled stylesheet, append `&combined` to the query. Then narrow the import once so TypeScript understands the shape:

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ coverage:
88
- packages/css/src
99
patch:
1010
default:
11+
target: 80.0
1112
threshold: 5.0
1213
paths:
1314
- packages/css/src

docs/combined-queries.md

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ This document summarizes how `?knighted-css&combined` behaves for different modu
44

55
## Decision Matrix
66

7-
| Source module exports | Recommended query | TypeScript import pattern | Notes |
8-
| --------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
9-
| **Named exports only** | `?knighted-css&combined&named-only` | [Snippet](#named-exports-only) | `&named-only` disables the synthetic default export so you only destructure the original named members plus `knightedCss`. |
10-
| **Default export only** | `?knighted-css&combined` | [Snippet](#default-export-only) | Loader mirrors the default export and adds `knightedCss`, so default-import code keeps working. |
11-
| **Default + named exports** | `?knighted-css&combined` (append `&named-only` when you never consume the default) | [Snippet](#default-and-named-exports) | Without the flag you get both default + named exports; adding it drops the synthetic default for stricter codebases. |
7+
| Source module exports | Recommended query | TypeScript import pattern | Notes |
8+
| ---------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
9+
| **Named exports only** | `?knighted-css&combined&named-only` | [Snippet](#named-exports-only) | `&named-only` disables the synthetic default export so you only destructure the original named members plus `knightedCss`. |
10+
| **Default export only** | `?knighted-css&combined` | [Snippet](#default-export-only) | Loader mirrors the default export and adds `knightedCss`, so default-import code keeps working. |
11+
| **Default + named exports** | `?knighted-css&combined` (append `&named-only` when you never consume the default) | [Snippet](#default-and-named-exports) | Without the flag you get both default + named exports; adding it drops the synthetic default for stricter codebases. |
12+
| **Named exports + stable selector map** | `?knighted-css&combined&named-only&types` | [Snippet](#named-exports-with-stable-selectors) | Adds a `stableSelectors` named export; configure namespaces via the loader option or CLI flag. |
13+
| **Default export only + stable selector map** | `?knighted-css&combined&types` | [Snippet](#default-export-with-stable-selectors) | Keep your default-import flow and add `stableSelectors`; namespaces come from loader/CLI configuration. |
14+
| **Default + named exports + stable selectors** | `?knighted-css&combined&types` (append `&named-only` if you skip the default) | [Snippet](#default-and-named-exports-with-stable-selectors) | Best of both worlds—`stableSelectors` is exported alongside `knightedCss`; add `&named-only` if you don’t use the default. |
1215

1316
## Named exports only
1417

@@ -50,8 +53,56 @@ const {
5053

5154
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`.
5255

56+
## Named exports with stable selectors
57+
58+
```ts
59+
import type { KnightedCssCombinedModule } from '@knighted/css/loader'
60+
import combined, {
61+
stableSelectors,
62+
} from './module.js?knighted-css&combined&named-only&types'
63+
64+
const { Component, knightedCss } = combined as KnightedCssCombinedModule<
65+
typeof import('./module.js')
66+
>
67+
68+
stableSelectors.card // "knighted-card"
69+
```
70+
71+
Need a different prefix? Configure the loader’s `stableNamespace` option (or pass `--stable-namespace` to the CLI) so both runtime and generated types stay in sync.
72+
73+
## Default export with stable selectors
74+
75+
```ts
76+
import type { KnightedCssCombinedModule } from '@knighted/css/loader'
77+
import combined, { stableSelectors } from './module.js?knighted-css&combined&types'
78+
79+
const { default: Component, knightedCss } = combined as KnightedCssCombinedModule<
80+
typeof import('./module.js')
81+
>
82+
83+
stableSelectors.badge // "knighted-badge"
84+
```
85+
86+
## Default and named exports with stable selectors
87+
88+
```ts
89+
import type { KnightedCssCombinedModule } from '@knighted/css/loader'
90+
import combined, { stableSelectors } from './module.js?knighted-css&combined&types'
91+
92+
const {
93+
default: Component,
94+
helper,
95+
knightedCss,
96+
} = combined as KnightedCssCombinedModule<typeof import('./module.js')>
97+
98+
stableSelectors.card // "knighted-card" (or your configured namespace)
99+
```
100+
101+
Append `&named-only` before `&types` when you want to drop the synthetic default export while still receiving `stableSelectors`.
102+
53103
## Key Takeaways
54104

55105
- The loader always injects `knightedCss` alongside the module’s exports.
56106
- To avoid synthetic defaults (and TypeScript warnings) for modules that only expose named exports, add `&named-only` and use a namespace import.
57107
- Namespace imports plus `KnightedCssCombinedModule<typeof import('./module')>` work universally; default imports are optional conveniences when the source module exposes a default you actually consume.
108+
- Add `&types` when you also need the `stableSelectors` map. Configure the namespace globally (loader option or CLI flag) so runtime + generated types stay consistent.

0 commit comments

Comments
 (0)