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.
npx knighted-css-generate-types --root . --include srcTypical script entry:
{
"scripts": {
"types:css": "knighted-css-generate-types --root . --include src"
}
}Wire it into postinstall or your build so new selectors land automatically.
--root/-r– project root (defaults toprocess.cwd()).--include/-i– additional directories or files to scan (repeatable).--out-dir– directory for the selector module manifest cache (defaults to<root>/.knighted-css).--stable-namespace– namespace prefix shared by the generated selector maps and loader runtime.--auto-stable– enable auto-stable selector generation during extraction (mirrors the loader’s auto-stable behavior).--hashed– emit proxy modules that exportselectorsbacked by loader-bridge hashed class names (mutually exclusive with--auto-stable).--resolver– path or package name exporting aCssResolver(default export or namedresolver).--mode–module(default) emits.knighted-css.tsproxy modules.declarationemits.d.tsmodule augmentations next to the referenced JS/TS modules, so you can keep standard imports likeimport { knightedCss } from './button.js'while the generator still discovers them via.knighted-cssspecifiers.--manifest– optional path to write a sidecar manifest for declaration mode (recommended when you want strict resolver behavior).
| Mode | Import style | Generated files | Bundler resolver plugin | Best for |
|---|---|---|---|---|
module (default) |
Double-extension (.knighted-css) |
.knighted-css.* proxy modules |
Not required | Maximum transparency and stability in large builds |
declaration |
Plain JS/TS imports | .d.ts augmentations next to modules |
Required (append ?knighted-css) |
Cleaner imports when you accept resolver overhead |
If you use declaration mode, prefer enabling strict sidecars + a manifest so the resolver only rewrites imports that the CLI generated. See docs/plugin.md for resolver plugin details and configuration options.
.knighted-css*imports include the generated selector map and, for module specifiers, re-exports plusknightedCss.?knighted-cssimports are purely runtime (see docs/loader.md). Append&typesonly when you also need the selector map at runtime; the compiler still reads the literal tokens from the generated modules.
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.
import selectors from './button.module.scss.knighted-css.js'
selectors.card // "knighted-card"import Button, { knightedCss, stableSelectors } from './button.knighted-css.js'
stableSelectors.card // "knighted-card"
knightedCss // compiled CSS stringDeclaration mode emits .d.ts files instead of .knighted-css.ts proxies, so you can import directly from the module:
knighted-css-generate-types --root . --include src --mode declarationimport Button, { knightedCss, stableSelectors } from './button.js'Important
Declaration mode requires a resolver plugin to append ?knighted-css (and &combined when applicable)
at build time so runtime exports match the generated types.
See docs/plugin.md for resolver plugin configuration.
The CLI only emits .d.ts sidecars for files that pass all of the following checks:
- Inside your include set: it walks each
--includeentry recursively, skipping common build folders (node_modules,dist,build,.knighted-css, etc.), and only considers script files (.ts,.tsx,.js,.jsx,.mts,.cts,.mjs,.cjs). Existing.d.tsfiles are ignored. - Within the project root: if a candidate file resolves outside
--root, the CLI skips it and logs a warning. - Imports styles: the file must import/require a style resource directly (e.g.
.css,.scss,.sass,.less, or vanilla-extract.css.ts/.css.js) or resolve to one via tsconfig paths / resolver hooks. - Produces selectors: the extracted CSS must be non-empty and yield at least one selector token; otherwise the sidecar is skipped.
In other words, declaration mode is opt-in by usage: only JS/TS/JSX/TSX modules that actually pull in
styles get a generated .d.ts augmentation.
Declaration mode emits .d.ts files with a // @knighted-css marker. If you want the resolver plugin
to only opt into those explicit sidecars (and avoid accidentally matching unrelated .d.ts files),
enable strict mode and pass a manifest created by the CLI:
knighted-css-generate-types --root . --include src --mode declaration \
--manifest .knighted-css/knighted-manifest.jsonimport path from 'node:path'
import { knightedCssResolverPlugin } from '@knighted/css/plugin'
export default {
resolve: {
plugins: [
knightedCssResolverPlugin({
strictSidecar: true,
manifestPath: path.resolve('.knighted-css/knighted-manifest.json'),
}),
],
},
}The manifest maps each source module to its generated .d.ts path. When strictSidecar is enabled,
the plugin only rewrites imports if the sidecar exists and includes the marker. That keeps
resolution deterministic even when other tooling generates .d.ts files alongside your modules.
Use --hashed when you want .knighted-css proxy modules to export selectors backed by
CSS Modules hashing instead of stable selector strings. This keeps the module and selector
types while preserving hashed class names at runtime.
Note
--hashed derives the selector list from the compiled CSS, so the generated sidecar can
include class names that are not exported by the module (for example, sprinkles output from
vanilla-extract). At runtime, selectors reflects only exported locals from the loader bridge, so the runtime map can be a subset of the generated sidecar.
Important
--hashed requires the bundler to route ?knighted-css imports through
@knighted/css/loader-bridge, so the proxy can read knightedCss and
knightedCssModules from the bridge output.
Example CLI:
knighted-css-generate-types --root . --include src --hashedExample usage:
import Button, { knightedCss, selectors } from './button.knighted-css.js'
selectors.card // hashed class name from CSS Modules
knightedCss // compiled CSS stringBecause 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.
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.
// rspack.config.js
import { exec } from 'node:child_process'
export default {
// ... your existing config
plugins: [
{
apply(compiler) {
compiler.hooks.watchRun.tapPromise('knighted-css-generate-types', () => {
const modified = Array.from(compiler.modifiedFiles ?? [])
const includes = modified.length > 0 ? modified : ['src']
const includeArgs = includes.flatMap(entry => ['--include', entry])
const command = ['knighted-css-generate-types', '--root', '.', ...includeArgs]
return new Promise((resolve, reject) => {
exec(command.join(' '), error => {
if (error) {
reject(error)
return
}
resolve()
})
})
})
},
},
],
}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.