Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,28 @@ jobs:
packages/playwright/playwright-report
if-no-files-found: ignore
retention-days: 7
e2e-windows:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.3.0
with:
node-version: '24.11.1'
- name: Install Dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium webkit
- name: Run Playwright
run: npm run test:e2e
- name: Upload Playwright artifacts
if: always()
uses: actions/upload-artifact@v4.4.3
with:
name: playwright-artifacts-windows
path: |
packages/playwright/test-results
packages/playwright/playwright-report
if-no-files-found: ignore
retention-days: 7
12 changes: 3 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
*.tgz
node_modules/
dist/
dist-webpack/
dist-auto-stable/
dist-hashed/
dist-bridge/
dist-bridge-webpack/
dist*/
coverage/
.c8/
.duel-cache/
Expand All @@ -14,7 +9,6 @@ coverage/
playwright-report/
test-results/
blob-report/
.knighted-css/
.knighted-css-auto/
.knighted-css-hashed/
.knighted-css*/
packages/playwright/src/**/*.knighted-css.ts
packages/playwright/src/mode/**/*.d.ts
62 changes: 61 additions & 1 deletion docs/loader.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Loader hook (`?knighted-css`)
# Loader hook

`@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`.

Expand Down Expand Up @@ -34,6 +34,66 @@ export default {
}
```

### Choosing a type generation mode

`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:

- `--mode module` (double-extension imports): use `.knighted-css` sidecar modules such as
`import stableSelectors from './button.css.knighted-css.js'`. This keeps resolution explicit and tends to be the most stable under large, complex builds.
- `--mode declaration` (idiomatic imports): emit `.d.ts` sidecars next to the original JS/TS module
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.

If you want the simplest, most transparent build behavior, start with `--mode module`.
If you want cleaner imports and are comfortable with resolver automation, choose `--mode declaration` and enable strict sidecars + a manifest for safety.

### Resolver plugin (declaration mode)

When you use `knighted-css-generate-types --mode declaration`, TypeScript expects the
augmented exports to be present on the original JS/TS module. Use the resolver plugin
to automatically append `?knighted-css` for any module import that has a generated
sidecar `.d.ts` file.

```js
// rspack.config.js
import { knightedCssResolverPlugin } from '@knighted/css/plugin'

export default {
resolve: {
plugins: [knightedCssResolverPlugin()],
},
}
```

```js
// webpack.config.js
const { knightedCssResolverPlugin } = require('@knighted/css/plugin')

module.exports = {
resolve: {
plugins: [knightedCssResolverPlugin()],
},
}
```

If you use declaration mode, consider enabling strict sidecar detection with a manifest. This
ensures only `.d.ts` files generated by `knighted-css-generate-types` trigger rewrites:

```js
import 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'),
}),
],
},
}
```

> [!NOTE]
> 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.

Expand Down
58 changes: 58 additions & 0 deletions docs/type-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ Wire it into `postinstall` or your build so new selectors land automatically.
- `--auto-stable` – enable auto-stable selector generation during extraction (mirrors the loader’s auto-stable behavior).
- `--hashed` – emit proxy modules that export `selectors` backed by loader-bridge hashed class names (mutually exclusive with `--auto-stable`).
- `--resolver` – path or package name exporting a `CssResolver` (default export or named `resolver`).
- `--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.
- `--manifest` – optional path to write a sidecar manifest for declaration mode (recommended when you want strict resolver behavior).

### Mode quick reference

| 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.

### Relationship to the loader

Expand All @@ -54,6 +65,53 @@ stableSelectors.card // "knighted-card"
knightedCss // compiled CSS string
```

## Declaration mode (augment existing modules)

Declaration mode emits `.d.ts` files instead of `.knighted-css.ts` proxies, so you can import directly from the module:

```sh
knighted-css-generate-types --root . --include src --mode declaration
```

```ts
import 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.

### Sidecar manifests + strict resolver mode

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:

```sh
knighted-css-generate-types --root . --include src --mode declaration \
--manifest .knighted-css/knighted-manifest.json
```

```js
import 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.

## Hashed selector proxies

Use `--hashed` when you want `.knighted-css` proxy modules to export `selectors` backed by
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 49 additions & 2 deletions packages/css/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ I needed a single source of truth for UI components that could drop into both li
- Deterministic selector duplication via `autoStable`: duplicate matching class selectors with a stable namespace (default `knighted-`) in both plain CSS and CSS Modules exports.
- 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.
- 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.

## Requirements

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

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

Run `knighted-css-generate-types` so every specifier that ends with `.knighted-css` produces a sibling manifest containing literal selector tokens:
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):

```ts
import stableSelectors from './button.module.scss.knighted-css.js'
Expand Down Expand Up @@ -142,6 +142,53 @@ selectors.card // hashed CSS Modules class name
> include class names that are not exported by the module (e.g. sprinkles output), while the
> runtime `selectors` map only includes exported locals from the loader bridge.

Prefer module-level imports without the double extension? Use declaration mode to emit `.d.ts` augmentations next to JS/TS modules that import styles:

```sh
knighted-css-generate-types --root . --include src --mode declaration
```

```ts
import Button, { knightedCss, stableSelectors } from './button.js'
```

See [docs/type-generation.md](../../docs/type-generation.md#mode-quick-reference) for a quick comparison of module vs declaration mode tradeoffs.

> [!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.

Install the resolver plugin via `@knighted/css/plugin` and wire it into your bundler resolver:

```js
// rspack.config.js
import { knightedCssResolverPlugin } from '@knighted/css/plugin'

export default {
resolve: {
plugins: [knightedCssResolverPlugin()],
},
}
```

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`):

```js
import 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'),
}),
],
},
}
```

Refer to [docs/type-generation.md](../../docs/type-generation.md) for CLI options and workflow tips.

### Combined + runtime selectors
Expand Down
11 changes: 9 additions & 2 deletions packages/css/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@knighted/css",
"version": "1.1.1",
"version": "1.2.0-rc.0",
"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.",
"type": "module",
"main": "./dist/css.js",
Expand All @@ -22,6 +22,9 @@
"generate-types": [
"./dist/generateTypes.d.ts"
],
"plugin": [
"./dist/plugin.d.ts"
],
"*": [
"./types.d.ts"
]
Expand Down Expand Up @@ -57,6 +60,11 @@
"import": "./dist/generateTypes.js",
"default": "./dist/generateTypes.js"
},
"./plugin": {
"types": "./dist/plugin.d.ts",
"import": "./dist/plugin.js",
"require": "./dist/cjs/plugin.cjs"
},
"./stableSelectors": {
"types": "./dist/stableSelectors.d.ts",
"import": "./dist/stableSelectors.js",
Expand Down Expand Up @@ -87,7 +95,6 @@
},
"scripts": {
"build": "duel && node ./scripts/copy-types-stub.js",
"pretest": "npm run build",
"check-types": "tsc --noEmit --project tsconfig.json && tsc --noEmit --project tsconfig.tests.json",
"test": "c8 --reporter=text --reporter=text-summary --reporter=lcov --include \"src/**/*.ts\" tsx --test test/**/*.test.ts",
"prepack": "npm run build"
Expand Down
Loading