@knighted/css 1.0.0-alpha.0 → 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![codecov](https://codecov.io/gh/knightedcodemonkey/css/graph/badge.svg?token=q93Qqwvq6l)](https://codecov.io/gh/knightedcodemonkey/css)
5
5
  [![NPM version](https://img.shields.io/npm/v/@knighted/css.svg)](https://www.npmjs.com/package/@knighted/css)
6
6
 
7
- `@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 to power zero-runtime styling workflows like Lit custom elements, server-side rendering, or pre-rendering pipelines where you need all CSS for a specific entry point without running a full bundler.
7
+ `@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.
8
8
 
9
9
  ## Features
10
10
 
@@ -23,12 +23,10 @@
23
23
  ## Installation
24
24
 
25
25
  ```bash
26
- npm install @knighted/css \
27
- sass less \
28
- @vanilla-extract/css @vanilla-extract/integration @vanilla-extract/recipes
26
+ npm install @knighted/css
29
27
  ```
30
28
 
31
- Only install the peers you need—if your project never touches Less, you can skip `less`, etc.
29
+ Install the peers your project is using, for example `less`, or `sass`, etc.
32
30
 
33
31
  ## Quick Start
34
32
 
@@ -73,17 +71,18 @@ Typical customizations:
73
71
 
74
72
  ## Examples
75
73
 
76
- ### Extract styles for Lit components
74
+ ### Generate standalone stylesheets
77
75
 
78
76
  ```ts
79
77
  import { writeFile } from 'node:fs/promises'
80
78
  import { css } from '@knighted/css'
81
79
 
82
- const sheet = await css('./src/lit/my-widget.ts', {
83
- lightningcss: { minify: true, targets: { chrome: 120 } },
80
+ // Build-time script that gathers all CSS imported by a React route
81
+ const sheet = await css('./src/routes/marketing-page.tsx', {
82
+ lightningcss: { minify: true, targets: { chrome: 120, safari: 17 } },
84
83
  })
85
84
 
86
- await writeFile('./dist/my-widget.css', sheet)
85
+ await writeFile('./dist/marketing-page.css', sheet)
87
86
  ```
88
87
 
89
88
  ### Inline CSS during SSR
@@ -99,6 +98,80 @@ export async function render(url: string) {
99
98
  }
100
99
  ```
101
100
 
101
+ ### Bundler loader (`?knighted-css` query)
102
+
103
+ 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.
104
+
105
+ ```js
106
+ // webpack.config.js
107
+ module.exports = {
108
+ module: {
109
+ rules: [
110
+ {
111
+ test: /\.[jt]sx?$/,
112
+ resourceQuery: /knighted-css/,
113
+ use: [
114
+ {
115
+ loader: '@knighted/css/loader',
116
+ options: {
117
+ lightningcss: { minify: true }, // all css() options supported
118
+ },
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ },
124
+ }
125
+ ```
126
+
127
+ ```ts
128
+ // lit wrapper
129
+ import { LitElement, html, unsafeCSS } from 'lit'
130
+ import { Button } from './button.tsx'
131
+ import { knightedCss as reactStyles } from './button.tsx?knighted-css'
132
+
133
+ export class ButtonWrapper extends LitElement {
134
+ static styles = [unsafeCSS(reactStyles)]
135
+ render() {
136
+ return html`<${Button} />`
137
+ }
138
+ }
139
+
140
+ // Prefer import aliasing when you need a different local name:
141
+ // import { knightedCss as cardCss } from './button.tsx?knighted-css'
142
+ ```
143
+
144
+ 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.
145
+
146
+ ### Custom resolver (enhanced-resolve example)
147
+
148
+ If your project uses aliases or nonstandard resolution, plug in a custom resolver. Here’s how to use [`enhanced-resolve`](https://github.com/webpack/enhanced-resolve):
149
+
150
+ ```ts
151
+ import { ResolverFactory } from 'enhanced-resolve'
152
+ import { css } from '@knighted/css'
153
+
154
+ const resolver = ResolverFactory.createResolver({
155
+ extensions: ['.ts', '.tsx', '.js'],
156
+ mainFiles: ['index'],
157
+ })
158
+
159
+ async function resolveWithEnhanced(id: string, cwd: string): Promise<string | undefined> {
160
+ return new Promise((resolve, reject) => {
161
+ resolver.resolve({}, cwd, id, {}, (err, result) => {
162
+ if (err) return reject(err)
163
+ resolve(result ?? undefined)
164
+ })
165
+ })
166
+ }
167
+
168
+ const styles = await css('./src/routes/page.tsx', {
169
+ resolver: (specifier, { cwd }) => resolveWithEnhanced(specifier, cwd),
170
+ })
171
+ ```
172
+
173
+ This keeps `@knighted/css` resolution in sync with your bundler’s alias/extension rules.
174
+
102
175
  ## Scripts
103
176
 
104
177
  - `npm run build` – Produce CJS/ESM outputs via `@knighted/duel`.
package/dist/cjs/css.cjs CHANGED
@@ -5,15 +5,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.DEFAULT_EXTENSIONS = void 0;
7
7
  exports.css = css;
8
+ exports.cssWithMeta = cssWithMeta;
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const node_fs_1 = require("node:fs");
10
11
  const dependency_tree_1 = __importDefault(require("dependency-tree"));
11
12
  const lightningcss_1 = require("lightningcss");
12
13
  exports.DEFAULT_EXTENSIONS = ['.css', '.scss', '.sass', '.less', '.css.ts'];
13
- /**
14
- * Extract and compile all CSS-like dependencies for a given module.
15
- */
16
14
  async function css(entry, options = {}) {
15
+ const { css: output } = await cssWithMeta(entry, options);
16
+ return output;
17
+ }
18
+ async function cssWithMeta(entry, options = {}) {
17
19
  const cwd = options.cwd ? node_path_1.default.resolve(options.cwd) : process.cwd();
18
20
  const entryPath = await resolveEntry(entry, cwd, options.resolver);
19
21
  const extensions = (options.extensions ?? exports.DEFAULT_EXTENSIONS).map(ext => ext.toLowerCase());
@@ -24,7 +26,7 @@ async function css(entry, options = {}) {
24
26
  dependencyTreeOptions: options.dependencyTree,
25
27
  });
26
28
  if (files.length === 0) {
27
- return '';
29
+ return { css: '', files: [] };
28
30
  }
29
31
  const chunks = [];
30
32
  for (const file of files) {
@@ -46,7 +48,7 @@ async function css(entry, options = {}) {
46
48
  });
47
49
  output = code.toString();
48
50
  }
49
- return output;
51
+ return { css: output, files: files.map(file => file.path) };
50
52
  }
51
53
  async function resolveEntry(entry, cwd, resolver) {
52
54
  if (typeof resolver === 'function') {
@@ -1,4 +1,4 @@
1
- import type { DependencyTreeOptions } from 'dependency-tree';
1
+ import type { Options as DependencyTreeOpts } from 'dependency-tree';
2
2
  import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
3
3
  export declare const DEFAULT_EXTENSIONS: string[];
4
4
  type LightningCssConfig = boolean | Partial<Omit<LightningTransformOptions<never>, 'code'>>;
@@ -11,12 +11,17 @@ export interface CssOptions {
11
11
  cwd?: string;
12
12
  filter?: (filePath: string) => boolean;
13
13
  lightningcss?: LightningCssConfig;
14
- dependencyTree?: Partial<Omit<DependencyTreeOptions, 'filename' | 'directory'>>;
14
+ dependencyTree?: Partial<Omit<DependencyTreeOpts, 'filename' | 'directory'>>;
15
15
  resolver?: CssResolver;
16
16
  peerResolver?: PeerLoader;
17
17
  }
18
18
  /**
19
19
  * Extract and compile all CSS-like dependencies for a given module.
20
20
  */
21
+ export interface CssResult {
22
+ css: string;
23
+ files: string[];
24
+ }
21
25
  export declare function css(entry: string, options?: CssOptions): Promise<string>;
26
+ export declare function cssWithMeta(entry: string, options?: CssOptions): Promise<CssResult>;
22
27
  export {};
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const css_js_1 = require("./css.cjs");
4
+ const DEFAULT_EXPORT_NAME = 'knightedCss';
5
+ const loader = async function loader(source) {
6
+ const rawOptions = (typeof this.getOptions === 'function' ? this.getOptions() : {});
7
+ const cssOptions = rawOptions;
8
+ const normalizedOptions = {
9
+ ...cssOptions,
10
+ cwd: cssOptions.cwd ?? this.rootContext ?? process.cwd(),
11
+ };
12
+ const { css, files } = await (0, css_js_1.cssWithMeta)(this.resourcePath, normalizedOptions);
13
+ const uniqueFiles = new Set([this.resourcePath, ...files]);
14
+ for (const file of uniqueFiles) {
15
+ this.addDependency(file);
16
+ }
17
+ const input = typeof source === 'string' ? source : source.toString('utf8');
18
+ const injection = `\n\nexport const ${DEFAULT_EXPORT_NAME} = ${JSON.stringify(css)};\n`;
19
+ const isStyleModule = this.resourcePath.endsWith('.css.ts');
20
+ const output = isStyleModule
21
+ ? `${injection}export default {};\n`
22
+ : `${input}${injection}`;
23
+ return output;
24
+ };
25
+ exports.default = loader;
@@ -0,0 +1,6 @@
1
+ import type { LoaderDefinitionFunction } from 'webpack';
2
+ import { type CssOptions } from './css.cjs';
3
+ export interface KnightedCssLoaderOptions extends CssOptions {
4
+ }
5
+ declare const loader: LoaderDefinitionFunction<KnightedCssLoaderOptions>;
6
+ export default loader;
package/dist/css.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DependencyTreeOptions } from 'dependency-tree';
1
+ import type { Options as DependencyTreeOpts } from 'dependency-tree';
2
2
  import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
3
3
  export declare const DEFAULT_EXTENSIONS: string[];
4
4
  type LightningCssConfig = boolean | Partial<Omit<LightningTransformOptions<never>, 'code'>>;
@@ -11,12 +11,17 @@ export interface CssOptions {
11
11
  cwd?: string;
12
12
  filter?: (filePath: string) => boolean;
13
13
  lightningcss?: LightningCssConfig;
14
- dependencyTree?: Partial<Omit<DependencyTreeOptions, 'filename' | 'directory'>>;
14
+ dependencyTree?: Partial<Omit<DependencyTreeOpts, 'filename' | 'directory'>>;
15
15
  resolver?: CssResolver;
16
16
  peerResolver?: PeerLoader;
17
17
  }
18
18
  /**
19
19
  * Extract and compile all CSS-like dependencies for a given module.
20
20
  */
21
+ export interface CssResult {
22
+ css: string;
23
+ files: string[];
24
+ }
21
25
  export declare function css(entry: string, options?: CssOptions): Promise<string>;
26
+ export declare function cssWithMeta(entry: string, options?: CssOptions): Promise<CssResult>;
22
27
  export {};
package/dist/css.js CHANGED
@@ -3,10 +3,11 @@ import { promises as fs } from 'node:fs';
3
3
  import dependencyTree from 'dependency-tree';
4
4
  import { transform as lightningTransform, } from 'lightningcss';
5
5
  export const DEFAULT_EXTENSIONS = ['.css', '.scss', '.sass', '.less', '.css.ts'];
6
- /**
7
- * Extract and compile all CSS-like dependencies for a given module.
8
- */
9
6
  export async function css(entry, options = {}) {
7
+ const { css: output } = await cssWithMeta(entry, options);
8
+ return output;
9
+ }
10
+ export async function cssWithMeta(entry, options = {}) {
10
11
  const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
11
12
  const entryPath = await resolveEntry(entry, cwd, options.resolver);
12
13
  const extensions = (options.extensions ?? DEFAULT_EXTENSIONS).map(ext => ext.toLowerCase());
@@ -17,7 +18,7 @@ export async function css(entry, options = {}) {
17
18
  dependencyTreeOptions: options.dependencyTree,
18
19
  });
19
20
  if (files.length === 0) {
20
- return '';
21
+ return { css: '', files: [] };
21
22
  }
22
23
  const chunks = [];
23
24
  for (const file of files) {
@@ -39,7 +40,7 @@ export async function css(entry, options = {}) {
39
40
  });
40
41
  output = code.toString();
41
42
  }
42
- return output;
43
+ return { css: output, files: files.map(file => file.path) };
43
44
  }
44
45
  async function resolveEntry(entry, cwd, resolver) {
45
46
  if (typeof resolver === 'function') {
@@ -0,0 +1,4 @@
1
+ /// <reference path="./loader-queries.d.ts" />
2
+
3
+ export * from '../css.js'
4
+ export { default } from '../css.js'
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Ambient declaration for loader query imports like "./file.js?knighted-css".
3
+ * The loader appends a named export `knightedCss` containing the compiled CSS.
4
+ */
5
+ declare module '*?knighted-css*' {
6
+ export const knightedCss: string
7
+ }
@@ -0,0 +1,4 @@
1
+ /// <reference path="./loader-queries.d.ts" />
2
+
3
+ export * from '../loader.js'
4
+ export { default } from '../loader.js'
package/dist/loader.js ADDED
@@ -0,0 +1,23 @@
1
+ import { cssWithMeta } from './css.js';
2
+ const DEFAULT_EXPORT_NAME = 'knightedCss';
3
+ const loader = async function loader(source) {
4
+ const rawOptions = (typeof this.getOptions === 'function' ? this.getOptions() : {});
5
+ const cssOptions = rawOptions;
6
+ const normalizedOptions = {
7
+ ...cssOptions,
8
+ cwd: cssOptions.cwd ?? this.rootContext ?? process.cwd(),
9
+ };
10
+ const { css, files } = await cssWithMeta(this.resourcePath, normalizedOptions);
11
+ const uniqueFiles = new Set([this.resourcePath, ...files]);
12
+ for (const file of uniqueFiles) {
13
+ this.addDependency(file);
14
+ }
15
+ const input = typeof source === 'string' ? source : source.toString('utf8');
16
+ const injection = `\n\nexport const ${DEFAULT_EXPORT_NAME} = ${JSON.stringify(css)};\n`;
17
+ const isStyleModule = this.resourcePath.endsWith('.css.ts');
18
+ const output = isStyleModule
19
+ ? `${injection}export default {};\n`
20
+ : `${input}${injection}`;
21
+ return output;
22
+ };
23
+ export default loader;
package/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "name": "@knighted/css",
3
- "version": "1.0.0-alpha.0",
3
+ "version": "1.0.0-alpha.2",
4
4
  "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.",
5
5
  "type": "module",
6
6
  "main": "./dist/css.js",
7
- "types": "./dist/css.d.ts",
7
+ "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./dist/css.d.ts",
10
+ "types": "./dist/index.d.ts",
11
11
  "import": "./dist/css.js",
12
12
  "require": "./dist/cjs/css.cjs"
13
+ },
14
+ "./loader": {
15
+ "types": "./dist/loader.d.ts",
16
+ "import": "./dist/loader.js",
17
+ "require": "./dist/cjs/loader.cjs"
13
18
  }
14
19
  },
15
20
  "engines": {
@@ -37,7 +42,16 @@
37
42
  ],
38
43
  "scripts": {
39
44
  "build": "duel",
40
- "test": "c8 --reporter=text --reporter=html tsx --test test/**/*.test.ts",
45
+ "postbuild": "cp src/types/*.d.ts dist/",
46
+ "check-types": "tsc --noEmit",
47
+ "test": "c8 --reporter=text --reporter=text-summary --reporter=lcov --include \"src/**/*.ts\" tsx --test test/**/*.test.ts",
48
+ "build:fixture": "npx rspack --config test/fixtures/loader-rspack/rspack.config.js",
49
+ "preview:fixture": "npm run build:fixture && npx http-server test/fixtures/loader-rspack -p 4173",
50
+ "prebuild:fixture:playwright": "npm run build",
51
+ "build:fixture:playwright": "npx rspack --config test/fixtures/playwright/rspack.config.js",
52
+ "serve:fixture:playwright": "npx http-server test/fixtures/playwright/dist -p 4174",
53
+ "preview:fixture:playwright": "npm run build:fixture:playwright && npx http-server test/fixtures/playwright -p 4174",
54
+ "test:e2e": "npx playwright test",
41
55
  "prettier": "prettier --write .",
42
56
  "prettier:check": "prettier --check .",
43
57
  "lint": "oxlint src test",
@@ -45,7 +59,7 @@
45
59
  "prepare": "husky"
46
60
  },
47
61
  "dependencies": {
48
- "dependency-tree": "^9.0.0",
62
+ "dependency-tree": "^11.2.0",
49
63
  "lightningcss": "^1.30.2"
50
64
  },
51
65
  "peerDependencies": {
@@ -66,11 +80,16 @@
66
80
  },
67
81
  "devDependencies": {
68
82
  "@knighted/duel": "^2.1.6",
83
+ "@playwright/test": "^1.48.2",
84
+ "@rspack/cli": "^1.0.0",
85
+ "@rspack/core": "^1.0.0",
69
86
  "@types/less": "^3.0.8",
87
+ "@types/webpack": "^5.28.5",
70
88
  "@vanilla-extract/css": "^1.15.2",
71
89
  "@vanilla-extract/integration": "^8.0.6",
72
90
  "@vanilla-extract/recipes": "^0.5.7",
73
91
  "c8": "^10.1.2",
92
+ "http-server": "^14.1.1",
74
93
  "husky": "^9.1.7",
75
94
  "less": "^4.2.0",
76
95
  "lint-staged": "^16.2.7",