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
18 changes: 18 additions & 0 deletions .github/instructions/pr-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
name: PR review instructions
description: Guidance for Copilot when reviewing pull requests.
applyTo: '**/*'
---

When reviewing PRs in this repository:

- Focus on correctness, edge cases, and TypeScript type safety.
- Call out potential bugs, duplicated logic, and opportunities to simplify.
- Verify generated artifacts (dist, coverage, test-results, .knighted-css) are not modified.
- Prefer minimal, localized changes and preserve existing public APIs.
- Check for new or missing tests when behavior changes.
- Flag NodeNext/ESM import correctness, especially extension handling.
- Call out performance regressions in graph walking or CSS extraction.
- Note any security or path traversal risks in resolver or loader changes.
- Ensure docs/readme updates match behavior changes when user-facing.
- Summarize risks and required follow-ups clearly.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
run: npm run check:cycles
- name: Test
run: npm test
- name: Generate Playwright Types
run: npm run types -w @knighted/css-playwright-fixture
- name: Check Types
run: npm run check-types
- name: Report Coverage
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ test-results/
blob-report/
.knighted-css/
.knighted-css-auto/
packages/playwright/src/**/*.knighted-css.ts
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Workspace-scoped (preferred when changing @knighted/css only):
- ESM only (`type: module`).
- Prettier: single quotes, no semicolons, `printWidth: 90`, `arrowParens: avoid`.
- Keep functions small and side-effect aware; prefer pure helpers where possible.
- Prefer multiline comment style (`/* ... */`) when a comment spans more than one line.

### Example style (good)

Expand Down
52 changes: 45 additions & 7 deletions docs/combined-queries.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Knighted CSS Combined Loader Reference

This document summarizes how `?knighted-css&combined` behaves for different module export shapes and how to structure your imports accordingly. Use it as guidance when filing documentation feedback for `@knighted/css`.
This document summarizes how `?knighted-css&combined` behaves for different module export shapes and how to structure your imports accordingly, plus how the double-extension proxy import replaces most combined-query use cases.

> [!NOTE]
> TypeScript reads literal selector tokens from the generated `.knighted-css.ts` modules (emitted by `knighted-css-generate-types`). Append `&types` to combined imports only when you also need `stableSelectors` at runtime—the loader still exports the map, while the double-extension modules keep your editors in sync.
> TypeScript reads literal selector tokens from the generated `.knighted-css.ts` modules (emitted by `knighted-css-generate-types`). The double-extension modules also re-export the original module exports and `knightedCss`, so a single import can provide component exports, typed selectors, and the compiled stylesheet string without helper casts.

> [!TIP]
> Prefer importing `asKnightedCssCombinedModule` from `@knighted/css/loader-helpers` when you want a runtime helper—the file has zero Node dependencies, so both browser and Node builds stay green.
> Prefer the double-extension import when you run `knighted-css-generate-types`—it removes the need for `asKnightedCssCombinedModule`. Use the helper only for `?knighted-css&combined` queries or when you cannot rely on generated proxy modules.

## Decision Matrix

Expand Down Expand Up @@ -43,6 +43,12 @@ const { Component, knightedCss } =
> [!NOTE]
> Namespace imports (`import * as combined …`) are the most reliable pattern for `&named-only` queries because you intentionally drop the default export. Keep using the helper type to narrow the namespace.

If you run `knighted-css-generate-types`, use the unified proxy import instead:

```ts
import { Component, knightedCss, stableSelectors } from './module.knighted-css.js'
```

## Default export only

_Use when your component only exposes a default export and you want `knightedCss` beside it._
Expand All @@ -55,6 +61,12 @@ const { default: Component, knightedCss } =
asKnightedCssCombinedModule<typeof import('./module.js')>(combinedModule)
```

With generated proxies, use a single import instead:

```ts
import Component, { knightedCss, stableSelectors } from './module.knighted-css.js'
```

## Default and named exports

_Use when you consume both the default export and its named helpers from the same module._
Expand All @@ -70,12 +82,20 @@ const {
} = asKnightedCssCombinedModule<typeof import('./module.js')>(combinedModule)
```

With generated proxies, a single import covers everything:

```ts
import Component, { helper, knightedCss, stableSelectors } from './module.knighted-css.js'
```

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

## Adding stable selectors (`&types`)

Append `&types` whenever you need the runtime `stableSelectors` map in addition to `knightedCss`. Configure the loader’s `stableNamespace` option (or pass `--stable-namespace` to the CLI) so runtime exports and generated `.knighted-css.ts` modules stay aligned.

If you run `knighted-css-generate-types`, the double-extension proxy already exports `stableSelectors` and keeps the literal types in sync—no `&types` query or helper required.

### Named exports with stable selectors

_Use when you only consume named exports (no default) but still want typed `stableSelectors`._
Expand All @@ -93,6 +113,12 @@ const { Component, knightedCss, stableSelectors } = asKnightedCssCombinedModule<
stableSelectors.card // "knighted-card"
```

With generated proxies, use a single import instead:

```ts
import { Component, knightedCss, stableSelectors } from './module.knighted-css.js'
```

> [!TIP]
> Add `&named-only` before `&types` to drop the synthetic default export while still receiving `stableSelectors`.

Expand All @@ -117,6 +143,12 @@ const {
stableSelectors.badge // "knighted-badge"
```

With generated proxies, use a single import instead:

```ts
import Component, { knightedCss, stableSelectors } from './module.knighted-css.js'
```

### Default and named exports with stable selectors

_Use when you consume every export from the module and still want the selector map._
Expand All @@ -139,16 +171,22 @@ const {
stableSelectors.card // "knighted-card" (or your configured namespace)
```

With generated proxies, a single import covers everything:

```ts
import Component, { helper, knightedCss, stableSelectors } from './module.knighted-css.js'
```

## Key Takeaways

- The loader always injects `knightedCss` alongside the module’s exports.
- To avoid synthetic defaults (and TypeScript warnings) for modules that only expose named exports, add `&named-only` and use a namespace import.
- Namespace imports plus `KnightedCssCombinedModule<typeof import('./module')>` work universally; default imports are optional conveniences when the source module exposes a default you actually consume.
- Add `&types` when you also need the `stableSelectors` map. Configure the namespace globally (loader option or CLI flag) so runtime + generated types stay consistent.
- When you run `knighted-css-generate-types`, prefer the double-extension proxy import to get component exports, `knightedCss`, and `stableSelectors` in one place.
- Use `?knighted-css&combined` (plus `asKnightedCssCombinedModule`) when you need runtime combined imports without generated proxy modules.
- Add `&types` only when you need a runtime selector map and cannot rely on the generated proxy files.

## When to use `KnightedCssCombinedModule`

The helper type still earns its keep when you need to narrow combined results without `asKnightedCssCombinedModule` (for example, in test doubles or custom wrappers):
The helper type still earns its keep when you need to narrow combined results without `asKnightedCssCombinedModule` (for example, in test doubles, custom wrappers, or when you skip generated proxies):

```ts
import type { KnightedCssCombinedModule } from '@knighted/css/loader'
Expand Down
67 changes: 57 additions & 10 deletions docs/type-generation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 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.
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`.

## Running the CLI

Expand All @@ -20,6 +20,21 @@ Typical script entry:

Wire it into `postinstall` or your build so new selectors land automatically.

### 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 `<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).

### Relationship to the loader

- `.knighted-css*` imports include the generated selector map and, for module specifiers, re-exports plus `knightedCss`.
- `?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.

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.

## Minimal usage

```ts
Expand All @@ -28,18 +43,50 @@ import selectors from './button.module.scss.knighted-css.js'
selectors.card // "knighted-card"
```

## Unified proxy usage (module exports + CSS + selectors)

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

stableSelectors.card // "knighted-card"
knightedCss // compiled CSS string
```

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
## Rspack watch hook

- `--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 `<root>/.knighted-css`).
- `--stable-namespace` – namespace prefix shared by the generated selector maps and loader runtime.
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.

### Relationship to the loader
```js
// rspack.config.js
import { exec } from 'node:child_process'

- `.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.
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()
})
})
})
},
},
],
}
```

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.
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`.
9 changes: 9 additions & 0 deletions packages/css/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ Run `knighted-css-generate-types` so every specifier that ends with `.knighted-c
import stableSelectors from './button.module.scss.knighted-css.js'
```

When the `.knighted-css` import targets a JavaScript/TypeScript module, the generated proxy also re-exports the module’s exports and `knightedCss`, so a single import can provide component exports, typed selectors, and the compiled stylesheet string:

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

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

### Combined + runtime selectors
Expand All @@ -136,6 +142,9 @@ const {
stableSelectors.shell
```

> [!TIP]
> If you run `knighted-css-generate-types`, prefer the double-extension proxy import shown above instead of `?knighted-css&combined` and `asKnightedCssCombinedModule`.

> [!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).
> Prefer importing `asKnightedCssCombinedModule` from `@knighted/css/loader-helpers` instead of grabbing it from `@knighted/css/loader`—the helper lives in a Node-free chunk so both browser and server bundles stay happy.
Expand Down
Loading