|
1 | | -# [`@knighted/css`](https://github.com/knightedcodemonkey/css) |
| 1 | +# [`@knighted/css`](https://www.npmjs.com/package/@knighted/css) |
2 | 2 |
|
3 | 3 |  |
4 | 4 | [](https://codecov.io/gh/knightedcodemonkey/css) |
5 | 5 | [](https://www.npmjs.com/package/@knighted/css) |
6 | 6 |
|
7 | | -`@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. |
| 7 | +`@knighted/css` is a zero-bundler CSS pipeline for JavaScript and TypeScript projects. Point it at an entry module and it walks the graph, compiles every CSS-like dependency (CSS, Sass/SCSS, Less, vanilla-extract), and hands back both a concatenated stylesheet string and optional `.knighted-css.*` selector manifests for type-safe loaders. |
8 | 8 |
|
9 | | -## Why |
| 9 | +## What it does (at a glance) |
10 | 10 |
|
11 | | -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. |
| 11 | +- **Graph walking**: Follows `import` trees the same way Node does (tsconfig `paths`, package `exports`/`imports`, hash specifiers, etc.) using [`oxc-resolver`](https://github.com/oxc-project/oxc-resolver). |
| 12 | +- **Multi-dialect compilation**: Runs Sass, Less, Lightning CSS, or vanilla-extract integrations on demand so every dependency ends up as plain CSS. |
| 13 | +- **Loader + CLI**: Ship CSS at runtime via `?knighted-css` loader queries or ahead of time via the `css()` API and the `knighted-css-generate-types` command. |
| 14 | +- **Shadow DOM + SSR ready**: Inline styles in server renders, ship them alongside web components, or keep classic DOM apps in sync—all without wiring a full bundler. |
12 | 15 |
|
13 | | -## Quick Links |
| 16 | +See the [docs/](./docs) directory for deep dives on loaders, type generation, specificity boosts, Sass aliases, and the combined import queries. |
14 | 17 |
|
15 | | -- [Features](#features) |
16 | | -- [Requirements](#requirements) |
17 | | -- [Installation](#installation) |
18 | | -- [Quick Start](#quick-start) |
19 | | -- [API](#api) |
20 | | -- [Entry points (`import`)](#entry-points-at-a-glance) |
21 | | -- [Examples](#examples) |
22 | | -- [Demo](#demo) |
| 18 | +## Workspaces in this repo |
23 | 19 |
|
24 | | -## Features |
| 20 | +| Workspace | NPM Name | What it contains | |
| 21 | +| --------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 22 | +| `packages/css` | [`@knighted/css`](https://www.npmjs.com/package/@knighted/css) | The production library: graph walker, compilation pipeline, loader helpers, CLI, and docs. Published to npm and meant for real builds. | |
| 23 | +| `packages/playwright` | `@knighted/css-playwright-fixture` | The end-to-end demo + regression suite. Playwright drives Lit + React examples, hash-import workspace scenarios, and SSR checks to ensure the core package keeps working across bundlers. | |
25 | 24 |
|
26 | | -- Traverses module graphs with a built-in walker to find transitive style imports (no bundler required). |
27 | | -- Resolution parity via [`oxc-resolver`](https://github.com/oxc-project/oxc-resolver): tsconfig `paths`, package `exports` + `imports`, and extension aliasing (e.g., `.css.js` → `.css.ts`) are honored without wiring up a bundler. |
28 | | -- Compiles `*.css`, `*.scss`, `*.sass`, `*.less`, and `*.css.ts` (vanilla-extract) files out of the box. |
29 | | -- Optional post-processing via [`lightningcss`](https://github.com/parcel-bundler/lightningcss) for minification, prefixing, media query optimizations, or specificity boosts. |
30 | | -- Pluggable resolver/filter hooks for custom module resolution (e.g., Rspack/Vite/webpack aliases) or selective inclusion. |
31 | | -- First-class loader (`@knighted/css/loader`) so bundlers can import compiled CSS alongside their modules via `?knighted-css`. |
32 | | -- 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. |
| 25 | +Each workspace is a standalone npm project. Run commands from the repo root with `npm run <script> -w <workspace>` or `npm run <script> --workspaces` to fan out when needed. |
33 | 26 |
|
34 | | -## Requirements |
35 | | - |
36 | | -- Node.js `>= 22.17.0` |
37 | | -- npm `>= 10.9.0` |
38 | | -- Install peer toolchains you intend to use (`sass`, `less`, `@vanilla-extract/integration`, etc.). |
39 | | - |
40 | | -## Installation |
41 | | - |
42 | | -```bash |
43 | | -npm install @knighted/css |
44 | | -``` |
45 | | - |
46 | | -Install the peers your project is using, for example `less`, or `sass`, etc. |
47 | | - |
48 | | -## Quick Start |
| 27 | +## Quick start |
49 | 28 |
|
50 | 29 | ```ts |
51 | | -// scripts/extract-styles.ts |
52 | 30 | import { css } from '@knighted/css' |
53 | 31 |
|
54 | | -const styles = await css('./src/components/app.ts', { |
| 32 | +const sheet = await css('./src/entry.tsx', { |
55 | 33 | cwd: process.cwd(), |
56 | 34 | lightningcss: { minify: true }, |
57 | 35 | }) |
58 | 36 |
|
59 | | -console.log(styles) |
60 | | -``` |
61 | | - |
62 | | -Run it with `tsx`/`node` and you will see a fully inlined stylesheet for `app.ts` and every style import it references, regardless of depth. |
63 | | - |
64 | | -## API |
65 | | - |
66 | | -```ts |
67 | | -type CssOptions = { |
68 | | - extensions?: string[] // customize file extensions to scan |
69 | | - cwd?: string // working directory (defaults to process.cwd()) |
70 | | - filter?: (filePath: string) => boolean |
71 | | - lightningcss?: boolean | LightningTransformOptions |
72 | | - specificityBoost?: { |
73 | | - visitor?: LightningTransformOptions<never>['visitor'] |
74 | | - strategy?: SpecificityStrategy |
75 | | - match?: SpecificitySelector[] |
76 | | - } |
77 | | - moduleGraph?: ModuleGraphOptions |
78 | | - resolver?: ( |
79 | | - specifier: string, |
80 | | - ctx: { cwd: string; from?: string }, |
81 | | - ) => string | Promise<string | undefined> |
82 | | - peerResolver?: (name: string) => Promise<unknown> // for custom module loading |
83 | | -} |
84 | | - |
85 | | -async function css(entry: string, options?: CssOptions): Promise<string> |
86 | | -``` |
87 | | - |
88 | | -## Entry points at a glance |
89 | | - |
90 | | -### Runtime loader hook (`?knighted-css`) |
91 | | - |
92 | | -Import any module with the `?knighted-css` query to receive the compiled stylesheet string: |
93 | | - |
94 | | -```ts |
95 | | -import { knightedCss } from './button.js?knighted-css' |
96 | | -``` |
97 | | - |
98 | | -See [docs/loader.md](./docs/loader.md) for the full configuration, combined imports, and `&types` runtime selector map guidance. |
99 | | - |
100 | | -### Type generation hook (`*.knighted-css*`) |
101 | | - |
102 | | -Run `knighted-css-generate-types` so every specifier that ends with `.knighted-css` produces a sibling manifest containing literal selector tokens: |
103 | | - |
104 | | -```ts |
105 | | -import stableSelectors from './button.module.scss.knighted-css.js' |
106 | | -``` |
107 | | - |
108 | | -Refer to [docs/type-generation.md](./docs/type-generation.md) for CLI options and workflow tips. |
109 | | - |
110 | | -### Combined + runtime selectors |
111 | | - |
112 | | -Need the module exports, `knightedCss`, and a runtime `stableSelectors` map from one import? Use `?knighted-css&combined&types` (plus optional `&named-only`). Example: |
113 | | - |
114 | | -```ts |
115 | | -import type { KnightedCssCombinedModule } from '@knighted/css/loader' |
116 | | -import { asKnightedCssCombinedModule } from '@knighted/css/loader-helpers' |
117 | | -import type { ButtonStableSelectors } from './button.css.knighted-css.js' |
118 | | -import * as buttonModule from './button.js?knighted-css&combined&types' |
119 | | -
|
120 | | -const { |
121 | | - default: Button, |
122 | | - knightedCss, |
123 | | - stableSelectors, |
124 | | -} = asKnightedCssCombinedModule< |
125 | | - typeof import('./button.js'), |
126 | | - { stableSelectors: Readonly<Record<keyof ButtonStableSelectors, string>> } |
127 | | ->(buttonModule) |
128 | | -
|
129 | | -stableSelectors.shell |
130 | | -``` |
131 | | - |
132 | | -> [!NOTE] |
133 | | -> `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). |
134 | | -> 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. |
135 | | - |
136 | | -## Examples |
137 | | - |
138 | | -- [Generate standalone stylesheets](#generate-standalone-stylesheets) |
139 | | -- [Inline CSS during SSR](#inline-css-during-ssr) |
140 | | -- [Custom resolver](#custom-resolver-enhanced-resolve-example) |
141 | | -- [Specificity boost](#specificity-boost) |
142 | | -- [Bundler loader](./docs/loader.md#loader-example) |
143 | | - |
144 | | -### Generate standalone stylesheets |
145 | | - |
146 | | -```ts |
147 | | -import { writeFile } from 'node:fs/promises' |
148 | | -import { css } from '@knighted/css' |
149 | | -
|
150 | | -// Build-time script that gathers all CSS imported by a React route |
151 | | -const sheet = await css('./src/routes/marketing-page.tsx', { |
152 | | - lightningcss: { minify: true, targets: { chrome: 120, safari: 17 } }, |
153 | | -}) |
154 | | -
|
155 | | -await writeFile('./dist/marketing-page.css', sheet) |
156 | | -``` |
157 | | - |
158 | | -### Inline CSS during SSR |
159 | | - |
160 | | -```ts |
161 | | -import { renderToString } from 'react-dom/server' |
162 | | -import { css } from '@knighted/css' |
163 | | -
|
164 | | -export async function render(url: string) { |
165 | | - const styles = await css('./src/routes/root.tsx') |
166 | | - const html = renderToString(<App url={url} />) |
167 | | - return `<!doctype html><style>${styles}</style>${html}` |
168 | | -} |
169 | | -``` |
170 | | - |
171 | | -### Custom resolver (enhanced-resolve example) |
172 | | - |
173 | | -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): |
174 | | - |
175 | | -> [!TIP] |
176 | | -> Hash-prefixed specifiers defined in `package.json#imports` resolve automatically—no extra loader or `css()` options required. Reach for a custom resolver only when you need behavior beyond what `oxc-resolver` already mirrors. |
177 | | - |
178 | | -> [!NOTE] |
179 | | -> Sass-specific prefixes such as `pkg:#button` live outside Node’s resolver and still need a shim. See [docs/sass-import-aliases.md](./docs/sass-import-aliases.md) for a drop-in helper that strips those markers before `@knighted/css` walks the graph. |
180 | | - |
181 | | -```ts |
182 | | -import { ResolverFactory } from 'enhanced-resolve' |
183 | | -import { css } from '@knighted/css' |
184 | | -
|
185 | | -const resolver = ResolverFactory.createResolver({ |
186 | | - extensions: ['.ts', '.tsx', '.js'], |
187 | | - mainFiles: ['index'], |
188 | | -}) |
189 | | -
|
190 | | -async function resolveWithEnhanced(id: string, cwd: string): Promise<string | undefined> { |
191 | | - return new Promise((resolve, reject) => { |
192 | | - resolver.resolve({}, cwd, id, {}, (err, result) => { |
193 | | - if (err) return reject(err) |
194 | | - resolve(result ?? undefined) |
195 | | - }) |
196 | | - }) |
197 | | -} |
198 | | -
|
199 | | -const styles = await css('./src/routes/page.tsx', { |
200 | | - resolver: (specifier, { cwd }) => resolveWithEnhanced(specifier, cwd), |
201 | | -}) |
202 | | -``` |
203 | | - |
204 | | -This keeps `@knighted/css` resolution in sync with your bundler’s alias/extension rules. |
205 | | - |
206 | | -### Specificity boost |
207 | | - |
208 | | -Use `specificityBoost` to tweak selector behavior: |
209 | | - |
210 | | -- **Strategies (built-in)**: |
211 | | - - `repeat-class` duplicates the last class in matching selectors to raise specificity (useful when you need a real specificity bump). |
212 | | - - `append-where` appends `:where(.token)` (zero specificity) for a harmless, order-based tie-breaker without changing matching. |
213 | | -- **Custom visitor**: Supply your own Lightning CSS visitor via `specificityBoost.visitor` for full control. |
214 | | -- **match filtering**: Provide `match: (string | RegExp)[]` to target selectors. Matches are OR’d; if any entry matches, the strategy applies. If omitted/empty, all selectors are eligible. |
215 | | - |
216 | | -Example: |
217 | | - |
218 | | -```ts |
219 | | -import { css } from '@knighted/css' |
220 | | - |
221 | | -const styles = await css('./src/entry.ts', { |
222 | | - lightningcss: { minify: true }, |
223 | | - specificityBoost: { |
224 | | - match: ['.card', /^\.btn/], // OR match |
225 | | - strategy: { type: 'repeat-class', times: 1 }, |
226 | | - }, |
227 | | -}) |
| 37 | +console.log(sheet) // use during SSR, static builds, or to inline Shadow DOM styles |
228 | 38 | ``` |
229 | 39 |
|
230 | | -If you omit `match`, the strategy applies to all selectors. Use `append-where` when you don’t want to change specificity; use `repeat-class` when you do. |
231 | | -
|
232 | | -> [!NOTE] |
233 | | -> For the built-in strategies, the last class in a matching selector is the one that gets duplicated/appended. If you have multiple similar classes, tighten your `match` (string or RegExp) to target exactly the selector you want boosted. |
234 | | -
|
235 | | -> [!TIP] |
236 | | -> See [docs/specificity-boost-visitor.md](./docs/specificity-boost-visitor.md) for a concrete visitor example. |
| 40 | +- Need runtime imports? See [docs/loader.md](./docs/loader.md). |
| 41 | +- Want strong selector types? Run `npx knighted-css-generate-types` and follow [docs/type-generation.md](./docs/type-generation.md). |
| 42 | +- Hash-prefixed or Sass-specific specifiers? Guidance lives in [docs/hash-import-fixture.md](./docs/hash-import-fixture.md) and [docs/sass-import-aliases.md](./docs/sass-import-aliases.md). |
237 | 43 |
|
238 | | -## Demo |
| 44 | +## Contributing & Support |
239 | 45 |
|
240 | | -Want to see everything wired together? Check the full demo app at [css-jsx-app](https://github.com/morganney/css-jsx-app). |
| 46 | +1. Install deps with `npm install`. |
| 47 | +2. Run `npm run build` to compile `@knighted/css`. |
| 48 | +3. Use `npm run test` for unit coverage and `npm run test:e2e` for the Playwright matrix. |
241 | 49 |
|
242 | | -> [!TIP] |
243 | | -> This repo also includes a [playwright workspace](./packages/playwright/src/lit-react/lit-host.ts) which serves as an end-to-end demo. |
| 50 | +Issues and feature ideas are always welcome via [GitHub issues](https://github.com/knightedcodemonkey/css/issues). |
244 | 51 |
|
245 | 52 | ## License |
246 | 53 |
|
|
0 commit comments