diff --git a/README.md b/README.md index 48de82e..e4ab782 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,11 @@ [![codecov](https://codecov.io/gh/knightedcodemonkey/css/graph/badge.svg?token=q93Qqwvq6l)](https://codecov.io/gh/knightedcodemonkey/css) [![NPM version](https://img.shields.io/npm/v/@knighted/css.svg)](https://www.npmjs.com/package/@knighted/css) -`@knighted/css` is a build-time helper that walks a JavaScript/TypeScript module graph, finds every CSS-like dependency (plain CSS, Sass/SCSS, Less, vanilla-extract), compiles them, and returns a single concatenated stylesheet string. It is designed for workflows where you want fully materialized styles ahead of time—feeding Lit components, server-rendered routes, static site builds, or any pipeline that needs all CSS for a specific entry point without running a full bundler. +`@knighted/css` walks your JavaScript/TypeScript module graph, compiles every CSS-like dependency (plain CSS, Sass/SCSS, Less, vanilla-extract), and ships both the concatenated stylesheet string and optional `.knighted-css.*` imports that keep selectors typed. Use it when you need fully materialized styles ahead of runtime—Shadow DOM surfaces, server-rendered routes, static site builds, or any entry point that should inline CSS without spinning up a full bundler. + +## Why + +I needed a single source of truth for UI components that could drop into both light DOM pages and Shadow DOM hosts, without losing encapsulated styling in the latter. ## Quick Links @@ -13,7 +17,9 @@ - [Installation](#installation) - [Quick Start](#quick-start) - [API](#api) +- [Entry points (`import`)](#entry-points-at-a-glance) - [Examples](#examples) +- [Demo](#demo) ## Features @@ -23,6 +29,7 @@ - Optional post-processing via [`lightningcss`](https://github.com/parcel-bundler/lightningcss) for minification, prefixing, media query optimizations, or specificity boosts. - Pluggable resolver/filter hooks for custom module resolution (e.g., Rspack/Vite/webpack aliases) or selective inclusion. - First-class loader (`@knighted/css/loader`) so bundlers can import compiled CSS alongside their modules via `?knighted-css`. +- 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. ## Requirements @@ -78,319 +85,79 @@ type CssOptions = { async function css(entry: string, options?: CssOptions): Promise ``` -Typical customizations: - -- **filter** – Skip certain paths (e.g., storybook-only styles) before compilation. -- **resolver** – Resolve virtual specifiers the way your bundler does (the repo ships test fixtures for webpack, Vite, and Rspack). -- **moduleGraph** – Configure tsconfig path aliases, extra script extensions, or custom `package.json` conditions for the built-in dependency walker. -- **lightningcss** – Pass `true` for defaults or a config object for minification/autoprefixing. -- **specificityBoost** – Provide a Lightning CSS visitor to bump specificity on selected selectors (e.g., duplicate a class for matching selectors). - -## Examples - -- [Generate standalone stylesheets](#generate-standalone-stylesheets) -- [Inline CSS during SSR](#inline-css-during-ssr) -- [Bundler loader](#bundler-loader-knighted-css-query) -- [Custom resolver](#custom-resolver-enhanced-resolve-example) -- [Specificity boost](#specificity-boost) - -### Generate standalone stylesheets - -```ts -import { writeFile } from 'node:fs/promises' -import { css } from '@knighted/css' - -// Build-time script that gathers all CSS imported by a React route -const sheet = await css('./src/routes/marketing-page.tsx', { - lightningcss: { minify: true, targets: { chrome: 120, safari: 17 } }, -}) - -await writeFile('./dist/marketing-page.css', sheet) -``` - -### Inline CSS during SSR - -```ts -import { renderToString } from 'react-dom/server' -import { css } from '@knighted/css' +## Entry points at a glance -export async function render(url: string) { - const styles = await css('./src/routes/root.tsx') - const html = renderToString() - return `${html}` -} -``` +### Runtime loader hook (`?knighted-css`) -### Bundler loader (`?knighted-css` query) - -When using Webpack/Rspack, add the provided loader so importing a module with a specific query also returns the compiled stylesheet. Recommended DX: import your component as usual, and import the CSS separately via the query import. - -```js -// webpack.config.js -module.exports = { - module: { - rules: [ - { - test: /\.[jt]sx?$/, - resourceQuery: /knighted-css/, - use: [ - { - loader: '@knighted/css/loader', - options: { - lightningcss: { minify: true }, // all css() options supported - }, - }, - ], - }, - ], - }, -} -``` +Import any module with the `?knighted-css` query to receive the compiled stylesheet string: ```ts -// lit wrapper -import { reactJsx } from '@knighted/jsx/react' -import { createRoot, type Root } from 'react-dom/client' -import { LitElement, html, unsafeCSS } from 'lit' -import { customElement } from 'lit/decorators.js' -import { Showcase } from './showcase.tsx' -import { knightedCss as showcaseCss } from './showcase.tsx?knighted-css' - -@customElement('lit-host') -export class LitHost extends LitElement { - static styles = [unsafeCSS(showcaseCss)] - #reactRoot?: Root - - firstUpdated(): void { - this.#mountReact() - } - - disconnectedCallback(): void { - this.#reactRoot?.unmount() - super.disconnectedCallback() - } - - #mountReact(): void { - if (!this.#reactRoot) { - const outlet = this.renderRoot.querySelector( - '[data-react-root]', - ) as HTMLDivElement | null - if (!outlet) return - this.#reactRoot = createRoot(outlet) - } - this.#reactRoot.render(reactJsx`<${Showcase} label="Launch CSS Build" />`) - } - - render() { - return html`
` - } -} +import { knightedCss } from './button.js?knighted-css' ``` -The loader appends `export const knightedCss = "/* compiled css */"` to the module when imported with `?knighted-css`. Keep your main module import separate to preserve its typing; use the query import only for the CSS string. +See [docs/loader.md](./docs/loader.md) for the full configuration, combined imports, and `&types` runtime selector map guidance. -> [!TIP] -> The Playwright Rspack demo shows how a Lit host can import specific dialects with `?knighted-css` and pipe them straight into `LitElement.styles`. See [packages/playwright/src/lit-react/lit-host.ts](packages/playwright/src/lit-react/lit-host.ts) for the shadow-root wiring. +### Type generation hook (`*.knighted-css*`) -#### CSS Modules and stable selectors - -CSS Modules hash class names after the loader extracts selectors, so the stylesheet captured by `?knighted-css` never sees those hashed tokens. Provide a second, stable selector (class or data attribute) alongside the module-generated one so both the DOM and the loader share a common hook. A minimal example: - -```tsx -
-``` - -### Stable selector modules (`*.knighted-css.ts`) - -Import the generated selector module anywhere you want literal tokens: +Run `knighted-css-generate-types` so every specifier that ends with `.knighted-css` produces a sibling manifest containing literal selector tokens: ```ts -import { stableSelectors } from './styles.css.knighted-css.js' - -stableSelectors.demo // "knighted-demo" -type StableSelectors = typeof stableSelectors +import stableSelectors from './button.module.scss.knighted-css.js' ``` -Run `npx knighted-css-generate-types --root .` to scan for `.knighted-css` specifiers and keep those modules current. The CLI: +Refer to [docs/type-generation.md](./docs/type-generation.md) for CLI options and workflow tips. -- extracts selectors via the loader, applies your `stableNamespace`, and sorts the tokens deterministically -- writes sibling `*.knighted-css.ts` files next to each stylesheet so editors resolve them immediately -- records everything inside `/.knighted-css/selector-modules.json` (or your custom `--out-dir`) and removes stale modules when imports disappear -- warns whenever a specifier cannot be resolved or escapes the configured project root +### Combined + runtime selectors -Because these are regular modules—not `.d.ts` shims—you can import the default export or the named `stableSelectors`, and helper types (`KnightedCssStableSelectors`, `KnightedCssStableSelectorToken`) ship alongside the literal map. Pass `--stable-namespace` so the CLI and loader agree on prefixes, and use `--include` / `--out-dir` to expand the scan or relocate the manifest cache (the selector modules themselves always live beside the source stylesheet). Re-run the command whenever you touch `.knighted-css` imports (wire it into a `knighted:types` script or a watch task). - -Sass/Less projects can import the shared mixins directly: - -```scss -@use '@knighted/css/stable' as knighted; - -.button { - @include knighted.stable('button') { - // declarations duplicated for .button and .knighted-button - } -} -``` - -Set `$knighted-stable-namespace: 'acme'` before the `@use` statement to change the default prefix, or override per call with `$namespace: 'storybook'`. Additional helpers let you emit only the fallback selector (`@include knighted.stable-only('token')`) or supply explicit `@at-root` selectors when nesting is inconvenient (`@include knighted.stable-at-root('.card', 'card')`). - -For runtime usage (vanilla-extract, CSS Modules, JSX utilities), pull in the TypeScript helpers: - -```ts -import { stableClassName } from '@knighted/css/stableSelectors' - -function Badge() { - return -} -``` - -`stableClass('token')` returns a class name you can drop straight into `className`, and `createStableClassFactory({ namespace: 'docs' })` gives you a scoped generator to reuse across components. Need the literal CSS selector? Call `stableSelector('token')`. All helpers sanitize tokens automatically so the emitted hooks stay deterministic. - -Need a zero-JS approach? Import the optional layer helper and co-locate your fallback selectors: - -```css -@import '@knighted/css/stable/stable.css'; - -@layer knighted.stable { - .knighted-alert { - /* declarations */ - } -} -``` - -Override the namespace via `:root { --knighted-stable-namespace: 'acme'; }` if you want a different prefix in pure CSS. - -#### Type-safe selector maps (`?knighted-css&types`) - -Use `?knighted-css&types` when you need the `stableSelectors` map at runtime (Lit styles, SSR, tests, etc.). TypeScript already sees the literal tokens via the generated `.knighted-css` modules, but the loader surfaces the same data for application code: - -```ts -import { knightedCss, stableSelectors } from './styles.css?knighted-css&types' - -stableSelectors.demo // "knighted-demo" -type StableSelectors = typeof stableSelectors -``` - -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: +Need the module exports, `knightedCss`, and a runtime `stableSelectors` map from one import? Use `?knighted-css&combined&types` (plus optional `&named-only`). Example: ```ts import type { KnightedCssCombinedModule } from '@knighted/css/loader' import combined, { stableSelectors } from './button.js?knighted-css&combined&types' -const { knightedCss } = combined as KnightedCssCombinedModule< +const { default: Button, knightedCss } = combined as KnightedCssCombinedModule< typeof import('./button.js') > - -stableSelectors.demo // "knighted-demo" -``` - -Namespaces default to `knighted`, but you can configure a global fallback via the loader’s `stableNamespace` option (match it with the CLI’s `--stable-namespace` flag so runtime + generated modules agree): - -```js -{ - loader: '@knighted/css/loader', - options: { - stableNamespace: 'storybook', - }, -} -``` - -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.). - -#### TypeScript support for loader queries - -Loader query types still ship directly with `@knighted/css`. Reference them once in your project—either by adding `"types": ["@knighted/css/loader-queries"]` to `tsconfig.json` or dropping `/// ` into a global `.d.ts`—and the following ambient modules become available everywhere: - -- `*?knighted-css` imports expose a `knightedCss: string` export. -- `*?knighted-css&types` exposes both `knightedCss` and `stableSelectors`, the readonly selector map. -- `*?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. -- `*?knighted-css&combined&types` variants add the same `stableSelectors` map on top of the combined behavior so a single import can surface everything. - -No vendor copies are necessary—the declarations live inside `@knighted/css`, you just need to point your TypeScript config at the shipped `loader-queries` subpath once. Use them for runtime loader imports and lean on the `.knighted-css` modules for editor-time selector literals. - -#### Keeping selector modules up to date - -The CLI walks every file you include (defaults to the project root, skipping `node_modules`, `dist`, etc.), finds specifiers ending in `.knighted-css`, reuses the loader to extract CSS, and writes deterministic `*.knighted-css.ts` siblings next to the real stylesheets. Each module exports the literal selector map plus helper types, and the manifest stored in `/.knighted-css/selector-modules.json` keeps the cache tidy by removing stale files. Because the generator writes actual TypeScript modules, editors pick them up immediately through normal resolution—no registries or `typeRoots` wiring required. - -Wire the CLI into `package.json` so local workflows stay fresh: - -```json -{ - "scripts": { - "knighted:types": "knighted-css-generate-types --root . --include src" - } -} ``` -Key flags: - -- `--root` / `-r` – project root (defaults to `process.cwd()`). -- `--include` / `-i` – additional directories or files to scan (repeatable). -- `--out-dir` – directory for the selector module manifest cache (defaults to `/.knighted-css`). -- `--stable-namespace` – namespace prefix shared by the generated selector maps and loader runtime. - -Re-run the CLI (or hook it into a watcher) whenever you touch `.knighted-css` imports so new tokens land in the literal selector modules. - -#### Combined module + CSS import - -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: +> [!NOTE] +> `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). -```ts -import type { KnightedCssCombinedModule } from '@knighted/css/loader' -import combined from './button.tsx?knighted-css&combined' +## Examples -const { Button, knightedCss } = combined as KnightedCssCombinedModule< - typeof import('./button') -> -``` +- [Generate standalone stylesheets](#generate-standalone-stylesheets) +- [Inline CSS during SSR](#inline-css-during-ssr) +- [Custom resolver](#custom-resolver-enhanced-resolve-example) +- [Specificity boost](#specificity-boost) +- [Bundler loader](./docs/loader.md#loader-example) -Combined imports mirror the source module’s default export strategy. Need to guarantee “named exports only” regardless of what the source module does? Append `&named-only` (alias: `&no-default`) to the query—the typing stays the same: +### Generate standalone stylesheets ```ts -import combined from './button.tsx?knighted-css&combined&named-only' -``` - -The `named-only` flag suppresses the synthetic default entirely, which is handy for codebases that consistently destructure combined modules or rely on namespace imports for type narrowing. - -You can mix and match: regular `?knighted-css` imports keep strong module typings and just add the CSS string, while `?knighted-css&combined` dedupes your CSS loader pipeline when you need everything at once. Need a quick reference for which query to use? Check the [Combined query matrix](./docs/combined-queries.md). - -#### vanilla-extract loader guidance +import { writeFile } from 'node:fs/promises' +import { css } from '@knighted/css' -vanilla-extract files (`*.css.ts`) compile down to CommonJS by default. That works out of the box for the loader—both `?knighted-css` and `?knighted-css&combined` queries emit `module.exports` artifacts plus the injected `knightedCss` string. Most bundlers happily consume that shape. When you _also_ need the compiled module to behave like a native ESM module (e.g., your bundler expects `export` statements so it can treeshake or when you import via extension aliases), enable the loader’s opt-in transform: +// Build-time script that gathers all CSS imported by a React route +const sheet = await css('./src/routes/marketing-page.tsx', { + lightningcss: { minify: true, targets: { chrome: 120, safari: 17 } }, +}) -```js -// rspack.config.js (excerpt) -{ - test: /\.css\.ts$/, - use: [ - { - loader: '@knighted/css/loader', - options: { - lightningcss: { minify: true }, - vanilla: { transformToEsm: true }, - }, - }, - // swc/esbuild/etc. - ], -} +await writeFile('./dist/marketing-page.css', sheet) ``` -The `vanilla.transformToEsm` flag runs a small post-pass that strips the CJS boilerplate emitted by `@vanilla-extract/integration` and re-exports the discovered bindings via native `export { name }` statements. That makes combined imports behave exactly like the source module, which is useful for frameworks that rely on strict ESM semantics (our Lit + React Playwright app is the canonical example in this repo). - -> [!IMPORTANT] -> Only enable `vanilla.transformToEsm` when your bundler really requires ESM output. Leaving the transform off keeps the vanilla-extract module identical to what the upstream compiler produced, which is often preferable if the rest of your toolchain expects CommonJS. The loader no longer toggles this transform automatically—combined imports stay fast, but you remain in full control of when the conversion occurs. +### Inline CSS during SSR -If your build pipeline can gracefully consume both module syntaxes (for example, webpack or Rspack projects that treat the vanilla-extract integration bundle as CommonJS), you may get the desired behavior simply by forcing those files through the “auto” parser instead of rewriting them: +```ts +import { renderToString } from 'react-dom/server' +import { css } from '@knighted/css' -```js -{ - test: /@vanilla-extract\/integration/, - type: 'javascript/auto', +export async function render(url: string) { + const styles = await css('./src/routes/root.tsx') + const html = renderToString() + return `${html}` } ``` -That hint keeps the upstream CommonJS helpers intact while still letting the rest of your app compile as native ESM. It’s worth trying first if you’d rather avoid the transform and your bundler already mixes module systems without issue. Flip `vanilla.transformToEsm` back on whenever you hit a toolchain that insists on pure ESM output. - ### Custom resolver (enhanced-resolve example) The built-in walker already leans on [`oxc-resolver`](https://github.com/oxc-project/oxc-resolver), so tsconfig `paths`, package `exports` conditions, and common extension aliases work out of the box. If you still need to mirror bespoke behavior (virtual modules, framework-specific loaders, etc.), plug in a custom resolver. Here’s how to use [`enhanced-resolve`](https://github.com/webpack/enhanced-resolve): @@ -420,10 +187,6 @@ const styles = await css('./src/routes/page.tsx', { This keeps `@knighted/css` resolution in sync with your bundler’s alias/extension rules. -### Sass alias specifiers - -If your Sass files rely on virtual specifiers such as `pkg:#styles/modules/typography.scss`, forward the same resolver you use for JavaScript imports. `@knighted/css` normalizes any resolver-backed custom scheme to a real `file://` URL before Dart Sass evaluates it, so a file loaded via `pkg:#…` still has a stable canonical URL. That keeps Sass’s internal `new URL('./tokens.scss', context.containingUrl)` calls working, which means relative `@use`/`@import` statements inside those alias-backed files continue to resolve just like they do in your bundler. - ### Specificity boost Use `specificityBoost` to tweak selector behavior: @@ -456,6 +219,13 @@ If you omit `match`, the strategy applies to all selectors. Use `append-where` w > [!TIP] > See [docs/specificity-boost-visitor.md](./docs/specificity-boost-visitor.md) for a concrete visitor example. +## Demo + +Want to see everything wired together? Check the full demo app at [css-jsx-app](https://github.com/morganney/css-jsx-app). + +> [!TIP] +> This repo also includes a [playwright workspace](./packages/playwright/src/lit-react/lit-host.ts) which serves as an end-to-end demo. + ## License MIT © Knighted Code Monkey diff --git a/docs/loader.md b/docs/loader.md new file mode 100644 index 0000000..3968054 --- /dev/null +++ b/docs/loader.md @@ -0,0 +1,96 @@ +# Loader hook (`?knighted-css`) + +`@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`. + +## Loader example + +```ts +import { knightedCss } from './button.js?knighted-css' + +export const styles = knightedCss +``` + +Add a bundler rule that pipes `?knighted-css` imports through `@knighted/css/loader` plus your transpiler of choice. See the main README for a complete rule configuration. + +```js +// rspack.config.js +export default { + module: { + rules: [ + { + test: /\.[jt]sx?$/, + resourceQuery: /knighted-css/, + use: [ + { + loader: '@knighted/css/loader', + options: { + lightningcss: { minify: true }, // all css() options supported + }, + }, + ], + }, + ], + }, +} +``` + +### Combined imports + +Need the component exports **and** the compiled CSS from a single import? Use `?knighted-css&combined` and narrow the result with `KnightedCssCombinedModule` to keep TypeScript happy: + +```ts +import type { KnightedCssCombinedModule } from '@knighted/css/loader' +import buttonModule from './button.js?knighted-css&combined' + +const { default: Button, knightedCss } = buttonModule as KnightedCssCombinedModule< + typeof import('./button.js') +> +``` + +Append `&named-only` (alias: `&no-default`) if you never consume the default export. Refer to [docs/combined-queries.md](./combined-queries.md) for the full matrix of query flags and destructuring patterns. + +### Runtime selectors (`&types`) + +When you need the runtime `stableSelectors` map alongside `knightedCss`, append `&types` to either the plain or combined import: + +```ts +import { knightedCss, stableSelectors } from './card.js?knighted-css&types' +``` + +> [!NOTE] +> TypeScript does not infer the stable selector literal types from this import; use the generated `.knighted-css.*` modules described in [docs/type-generation.md](./type-generation.md) for compile-time safety. The runtime map is helpful for tests, telemetry, or non-TypeScript environments. + +### vanilla-extract loader guidance + +vanilla-extract files (`*.css.ts`) compile down to CommonJS by default. That works out of the box for the loader—both `?knighted-css` and `?knighted-css&combined` queries emit `module.exports` artifacts plus the injected `knightedCss` string. Most bundlers happily consume that shape. When you _also_ need the compiled module to behave like a native ESM module (e.g., your bundler expects `export` statements so it can treeshake or when you import via extension aliases), enable the loader’s opt-in transform: + +```js +{ + test: /\.css\.ts$/, + use: [ + { + loader: '@knighted/css/loader', + options: { + lightningcss: { minify: true }, + vanilla: { transformToEsm: true }, + }, + }, + ], +} +``` + +The `vanilla.transformToEsm` flag runs a small post-pass that strips the CJS boilerplate emitted by `@vanilla-extract/integration` and re-exports the discovered bindings via native `export { name }` statements. That makes combined imports behave exactly like the source module, which is useful for frameworks that rely on strict ESM semantics (our Lit + React Playwright app is the canonical example in this repo). + +> [!IMPORTANT] +> Only enable `vanilla.transformToEsm` when your bundler really requires ESM output. Leaving the transform off keeps the vanilla-extract module identical to what the upstream compiler produced, which is often preferable if the rest of your toolchain expects CommonJS. The loader no longer toggles this transform automatically—combined imports stay fast, but you remain in full control of when the conversion occurs. + +If your build pipeline can gracefully consume both module syntaxes (for example, webpack or Rspack projects that treat the vanilla-extract integration bundle as CommonJS), you may get the desired behavior simply by forcing those files through the “auto” parser instead of rewriting them: + +```js +{ + test: /@vanilla-extract\/integration/, + type: 'javascript/auto', +} +``` + +That hint keeps the upstream CommonJS helpers intact while still letting the rest of your app compile as native ESM. It’s worth trying first if you’d rather avoid the transform and your bundler already mixes module systems without issue. Flip `vanilla.transformToEsm` back on whenever you hit a toolchain that insists on pure ESM output. diff --git a/docs/type-generation.md b/docs/type-generation.md new file mode 100644 index 0000000..6a15327 --- /dev/null +++ b/docs/type-generation.md @@ -0,0 +1,45 @@ +# Type generation (`*.knighted-css*`) + +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. + +## Running the CLI + +```sh +npx knighted-css-generate-types --root . --include src +``` + +Typical script entry: + +```json +{ + "scripts": { + "types:css": "knighted-css-generate-types --root . --include src" + } +} +``` + +Wire it into `postinstall` or your build so new selectors land automatically. + +## Minimal usage + +```ts +import selectors from './button.module.scss.knighted-css.js' + +selectors.card // "knighted-card" +``` + +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. + +### Options + +- `--root` / `-r` – project root (defaults to `process.cwd()`). +- `--include` / `-i` – additional directories or files to scan (repeatable). +- `--out-dir` – directory for the selector module manifest cache (defaults to `/.knighted-css`). +- `--stable-namespace` – namespace prefix shared by the generated selector maps and loader runtime. + +### Relationship to the loader + +- `.knighted-css*` imports are purely for types; they never include the compiled CSS string. +- `?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. + +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.