@knighted/css 1.0.0-alpha.0
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 +117 -0
- package/dist/cjs/css.cjs +197 -0
- package/dist/cjs/css.d.cts +22 -0
- package/dist/css.d.ts +22 -0
- package/dist/css.js +190 -0
- package/package.json +105 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# [`@knighted/css`](https://github.com/knightedcodemonkey/css)
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
[](https://codecov.io/gh/knightedcodemonkey/css)
|
|
5
|
+
[](https://www.npmjs.com/package/@knighted/css)
|
|
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.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Traverses module graphs using [`dependency-tree`](https://github.com/dependents/node-dependency-tree) to find transitive style imports.
|
|
12
|
+
- Compiles `*.css`, `*.scss`, `*.sass`, `*.less`, and `*.css.ts` (vanilla-extract) files out of the box.
|
|
13
|
+
- Optional post-processing via [`lightningcss`](https://github.com/parcel-bundler/lightningcss) for minification, prefixing, and media query optimizations.
|
|
14
|
+
- Pluggable resolver/filter hooks for custom module resolution (e.g., Rspack/Vite/webpack aliases) or selective inclusion.
|
|
15
|
+
- Peer-resolution helper for optional toolchains (`sass`, `less`, `@vanilla-extract/integration`) so consumers control their dependency graph.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node.js `>= 22.15.0`
|
|
20
|
+
- npm `>= 10.9.0`
|
|
21
|
+
- Install peer toolchains you intend to use (`sass`, `less`, `@vanilla-extract/integration`, `@vanilla-extract/recipes`, etc.).
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @knighted/css \
|
|
27
|
+
sass less \
|
|
28
|
+
@vanilla-extract/css @vanilla-extract/integration @vanilla-extract/recipes
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Only install the peers you need—if your project never touches Less, you can skip `less`, etc.
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// scripts/extract-styles.ts
|
|
37
|
+
import { css } from '@knighted/css'
|
|
38
|
+
|
|
39
|
+
const styles = await css('./src/components/app.ts', {
|
|
40
|
+
cwd: process.cwd(),
|
|
41
|
+
lightningcss: { minify: true },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
console.log(styles)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
type CssOptions = {
|
|
53
|
+
extensions?: string[] // customize file extensions to scan
|
|
54
|
+
cwd?: string // working directory (defaults to process.cwd())
|
|
55
|
+
filter?: (filePath: string) => boolean
|
|
56
|
+
lightningcss?: boolean | LightningTransformOptions
|
|
57
|
+
dependencyTree?: DependencyTreeOptions
|
|
58
|
+
resolver?: (
|
|
59
|
+
specifier: string,
|
|
60
|
+
ctx: { cwd: string },
|
|
61
|
+
) => string | Promise<string | undefined>
|
|
62
|
+
peerResolver?: (name: string) => Promise<unknown> // for custom module loading
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function css(entry: string, options?: CssOptions): Promise<string>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Typical customizations:
|
|
69
|
+
|
|
70
|
+
- **filter** – Skip certain paths (e.g., storybook-only styles) before compilation.
|
|
71
|
+
- **resolver** – Resolve virtual specifiers the way your bundler does (the repo ships test fixtures for webpack, Vite, and Rspack).
|
|
72
|
+
- **lightningcss** – Pass `true` for defaults or a config object for minification/autoprefixing.
|
|
73
|
+
|
|
74
|
+
## Examples
|
|
75
|
+
|
|
76
|
+
### Extract styles for Lit components
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { writeFile } from 'node:fs/promises'
|
|
80
|
+
import { css } from '@knighted/css'
|
|
81
|
+
|
|
82
|
+
const sheet = await css('./src/lit/my-widget.ts', {
|
|
83
|
+
lightningcss: { minify: true, targets: { chrome: 120 } },
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
await writeFile('./dist/my-widget.css', sheet)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Inline CSS during SSR
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { renderToString } from 'react-dom/server'
|
|
93
|
+
import { css } from '@knighted/css'
|
|
94
|
+
|
|
95
|
+
export async function render(url: string) {
|
|
96
|
+
const styles = await css('./src/routes/root.tsx')
|
|
97
|
+
const html = renderToString(<App url={url} />)
|
|
98
|
+
return `<!doctype html><style>${styles}</style>${html}`
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Scripts
|
|
103
|
+
|
|
104
|
+
- `npm run build` – Produce CJS/ESM outputs via `@knighted/duel`.
|
|
105
|
+
- `npm test` – Runs the Node test suite with `tsx` and reports coverage via `c8`.
|
|
106
|
+
- `npm run lint` – Static analysis through `oxlint`.
|
|
107
|
+
|
|
108
|
+
## Contributing
|
|
109
|
+
|
|
110
|
+
1. Clone the repo and install dependencies with `npm install`.
|
|
111
|
+
2. Run `npm test` to ensure fixtures compile across Sass/Less/vanilla-extract.
|
|
112
|
+
3. Add/adjust fixtures in `fixtures/` when adding new language features to keep coverage high.
|
|
113
|
+
4. Open a PR with a description of the change and tests.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT © Knighted Code Monkey
|
package/dist/cjs/css.cjs
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_EXTENSIONS = void 0;
|
|
7
|
+
exports.css = css;
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const dependency_tree_1 = __importDefault(require("dependency-tree"));
|
|
11
|
+
const lightningcss_1 = require("lightningcss");
|
|
12
|
+
exports.DEFAULT_EXTENSIONS = ['.css', '.scss', '.sass', '.less', '.css.ts'];
|
|
13
|
+
/**
|
|
14
|
+
* Extract and compile all CSS-like dependencies for a given module.
|
|
15
|
+
*/
|
|
16
|
+
async function css(entry, options = {}) {
|
|
17
|
+
const cwd = options.cwd ? node_path_1.default.resolve(options.cwd) : process.cwd();
|
|
18
|
+
const entryPath = await resolveEntry(entry, cwd, options.resolver);
|
|
19
|
+
const extensions = (options.extensions ?? exports.DEFAULT_EXTENSIONS).map(ext => ext.toLowerCase());
|
|
20
|
+
const files = collectStyleDependencies(entryPath, {
|
|
21
|
+
cwd,
|
|
22
|
+
extensions,
|
|
23
|
+
filter: options.filter,
|
|
24
|
+
dependencyTreeOptions: options.dependencyTree,
|
|
25
|
+
});
|
|
26
|
+
if (files.length === 0) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
const chunks = [];
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
const chunk = await compileStyleModule(file, {
|
|
32
|
+
cwd,
|
|
33
|
+
peerResolver: options.peerResolver,
|
|
34
|
+
});
|
|
35
|
+
if (chunk) {
|
|
36
|
+
chunks.push(chunk);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let output = chunks.join('\n');
|
|
40
|
+
if (options.lightningcss) {
|
|
41
|
+
const lightningOptions = normalizeLightningOptions(options.lightningcss);
|
|
42
|
+
const { code } = (0, lightningcss_1.transform)({
|
|
43
|
+
...lightningOptions,
|
|
44
|
+
filename: lightningOptions.filename ?? 'extracted.css',
|
|
45
|
+
code: Buffer.from(output),
|
|
46
|
+
});
|
|
47
|
+
output = code.toString();
|
|
48
|
+
}
|
|
49
|
+
return output;
|
|
50
|
+
}
|
|
51
|
+
async function resolveEntry(entry, cwd, resolver) {
|
|
52
|
+
if (typeof resolver === 'function') {
|
|
53
|
+
const resolved = await resolver(entry, { cwd });
|
|
54
|
+
if (resolved) {
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (node_path_1.default.isAbsolute(entry)) {
|
|
59
|
+
return entry;
|
|
60
|
+
}
|
|
61
|
+
return node_path_1.default.resolve(cwd, entry);
|
|
62
|
+
}
|
|
63
|
+
function collectStyleDependencies(entryPath, { cwd, extensions, filter, dependencyTreeOptions, }) {
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const order = [];
|
|
66
|
+
const shouldInclude = typeof filter === 'function'
|
|
67
|
+
? filter
|
|
68
|
+
: (filePath) => !filePath.includes('node_modules');
|
|
69
|
+
const entryIsStyle = Boolean(matchExtension(entryPath, extensions));
|
|
70
|
+
let treeList = [];
|
|
71
|
+
if (!entryIsStyle) {
|
|
72
|
+
const dependencyConfig = {
|
|
73
|
+
...dependencyTreeOptions,
|
|
74
|
+
filename: entryPath,
|
|
75
|
+
directory: cwd,
|
|
76
|
+
filter: shouldInclude,
|
|
77
|
+
};
|
|
78
|
+
treeList = dependency_tree_1.default.toList(dependencyConfig);
|
|
79
|
+
}
|
|
80
|
+
const candidates = entryIsStyle ? [entryPath] : [entryPath, ...treeList];
|
|
81
|
+
for (const candidate of candidates) {
|
|
82
|
+
const match = matchExtension(candidate, extensions);
|
|
83
|
+
if (!match || seen.has(candidate))
|
|
84
|
+
continue;
|
|
85
|
+
seen.add(candidate);
|
|
86
|
+
order.push({ path: node_path_1.default.resolve(candidate), ext: match });
|
|
87
|
+
}
|
|
88
|
+
return order;
|
|
89
|
+
}
|
|
90
|
+
function matchExtension(filePath, extensions) {
|
|
91
|
+
const lower = filePath.toLowerCase();
|
|
92
|
+
return extensions.find(ext => lower.endsWith(ext));
|
|
93
|
+
}
|
|
94
|
+
async function compileStyleModule(file, { cwd, peerResolver }) {
|
|
95
|
+
switch (file.ext) {
|
|
96
|
+
case '.css':
|
|
97
|
+
return node_fs_1.promises.readFile(file.path, 'utf8');
|
|
98
|
+
case '.scss':
|
|
99
|
+
case '.sass':
|
|
100
|
+
return compileSass(file.path, file.ext === '.sass', peerResolver);
|
|
101
|
+
case '.less':
|
|
102
|
+
return compileLess(file.path, peerResolver);
|
|
103
|
+
case '.css.ts':
|
|
104
|
+
return compileVanillaExtract(file.path, cwd, peerResolver);
|
|
105
|
+
default:
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function compileSass(filePath, indented, peerResolver) {
|
|
110
|
+
const sassModule = await optionalPeer('sass', 'Sass', peerResolver);
|
|
111
|
+
const sass = sassModule;
|
|
112
|
+
const result = sass.compile(filePath, {
|
|
113
|
+
style: 'expanded',
|
|
114
|
+
});
|
|
115
|
+
return result.css;
|
|
116
|
+
}
|
|
117
|
+
async function compileLess(filePath, peerResolver) {
|
|
118
|
+
const mod = await optionalPeer('less', 'Less', peerResolver);
|
|
119
|
+
const less = unwrapModuleNamespace(mod);
|
|
120
|
+
const source = await node_fs_1.promises.readFile(filePath, 'utf8');
|
|
121
|
+
const result = await less.render(source, { filename: filePath });
|
|
122
|
+
return result.css;
|
|
123
|
+
}
|
|
124
|
+
async function compileVanillaExtract(filePath, cwd, peerResolver) {
|
|
125
|
+
const mod = await optionalPeer('@vanilla-extract/integration', 'Vanilla Extract', peerResolver);
|
|
126
|
+
const namespace = unwrapModuleNamespace(mod);
|
|
127
|
+
const compileFn = namespace.compile;
|
|
128
|
+
const transformPlugin = namespace.vanillaExtractTransformPlugin;
|
|
129
|
+
const processVanillaFile = namespace.processVanillaFile;
|
|
130
|
+
const getSourceFromVirtualCssFile = namespace.getSourceFromVirtualCssFile;
|
|
131
|
+
if (!compileFn ||
|
|
132
|
+
!getSourceFromVirtualCssFile ||
|
|
133
|
+
!transformPlugin ||
|
|
134
|
+
!processVanillaFile) {
|
|
135
|
+
throw new Error('@knighted/css: Unable to load "@vanilla-extract/integration". Please ensure the package exports compile helpers.');
|
|
136
|
+
}
|
|
137
|
+
const identOption = process.env.NODE_ENV === 'production' ? 'short' : 'debug';
|
|
138
|
+
const { source } = await compileFn({
|
|
139
|
+
filePath,
|
|
140
|
+
cwd,
|
|
141
|
+
identOption,
|
|
142
|
+
esbuildOptions: {
|
|
143
|
+
plugins: [transformPlugin({ identOption })],
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
const processedSource = await processVanillaFile({
|
|
147
|
+
source,
|
|
148
|
+
filePath,
|
|
149
|
+
identOption,
|
|
150
|
+
outputCss: true,
|
|
151
|
+
});
|
|
152
|
+
const imports = [];
|
|
153
|
+
const importRegex = /['"](?<id>[^'"]+\.vanilla\.css\?source=[^'"]+)['"]/gimu;
|
|
154
|
+
let match;
|
|
155
|
+
while ((match = importRegex.exec(processedSource)) !== null) {
|
|
156
|
+
const id = match.groups?.id ?? match[1];
|
|
157
|
+
if (!id)
|
|
158
|
+
continue;
|
|
159
|
+
const virtualFile = await getSourceFromVirtualCssFile(id);
|
|
160
|
+
if (virtualFile?.source) {
|
|
161
|
+
imports.push(virtualFile.source);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return imports.join('\n');
|
|
165
|
+
}
|
|
166
|
+
const defaultPeerLoader = name => import(name);
|
|
167
|
+
async function optionalPeer(name, label, loader) {
|
|
168
|
+
const importer = loader ?? defaultPeerLoader;
|
|
169
|
+
try {
|
|
170
|
+
return (await importer(name));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error &&
|
|
174
|
+
typeof error === 'object' &&
|
|
175
|
+
'code' in error &&
|
|
176
|
+
typeof error.code === 'string' &&
|
|
177
|
+
/MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND/.test(error.code)) {
|
|
178
|
+
throw new Error(`@knighted/css: Attempted to process ${label}, but "${name}" is not installed. Please add it to your project.`);
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function unwrapModuleNamespace(mod) {
|
|
184
|
+
if (typeof mod === 'object' &&
|
|
185
|
+
mod !== null &&
|
|
186
|
+
'default' in mod &&
|
|
187
|
+
mod.default) {
|
|
188
|
+
return mod.default;
|
|
189
|
+
}
|
|
190
|
+
return mod;
|
|
191
|
+
}
|
|
192
|
+
function normalizeLightningOptions(config) {
|
|
193
|
+
if (!config || config === true) {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
return config;
|
|
197
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DependencyTreeOptions } from 'dependency-tree';
|
|
2
|
+
import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
|
|
3
|
+
export declare const DEFAULT_EXTENSIONS: string[];
|
|
4
|
+
type LightningCssConfig = boolean | Partial<Omit<LightningTransformOptions<never>, 'code'>>;
|
|
5
|
+
export type CssResolver = (specifier: string, ctx: {
|
|
6
|
+
cwd: string;
|
|
7
|
+
}) => string | Promise<string | undefined>;
|
|
8
|
+
type PeerLoader = (name: string) => Promise<unknown>;
|
|
9
|
+
export interface CssOptions {
|
|
10
|
+
extensions?: string[];
|
|
11
|
+
cwd?: string;
|
|
12
|
+
filter?: (filePath: string) => boolean;
|
|
13
|
+
lightningcss?: LightningCssConfig;
|
|
14
|
+
dependencyTree?: Partial<Omit<DependencyTreeOptions, 'filename' | 'directory'>>;
|
|
15
|
+
resolver?: CssResolver;
|
|
16
|
+
peerResolver?: PeerLoader;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Extract and compile all CSS-like dependencies for a given module.
|
|
20
|
+
*/
|
|
21
|
+
export declare function css(entry: string, options?: CssOptions): Promise<string>;
|
|
22
|
+
export {};
|
package/dist/css.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DependencyTreeOptions } from 'dependency-tree';
|
|
2
|
+
import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
|
|
3
|
+
export declare const DEFAULT_EXTENSIONS: string[];
|
|
4
|
+
type LightningCssConfig = boolean | Partial<Omit<LightningTransformOptions<never>, 'code'>>;
|
|
5
|
+
export type CssResolver = (specifier: string, ctx: {
|
|
6
|
+
cwd: string;
|
|
7
|
+
}) => string | Promise<string | undefined>;
|
|
8
|
+
type PeerLoader = (name: string) => Promise<unknown>;
|
|
9
|
+
export interface CssOptions {
|
|
10
|
+
extensions?: string[];
|
|
11
|
+
cwd?: string;
|
|
12
|
+
filter?: (filePath: string) => boolean;
|
|
13
|
+
lightningcss?: LightningCssConfig;
|
|
14
|
+
dependencyTree?: Partial<Omit<DependencyTreeOptions, 'filename' | 'directory'>>;
|
|
15
|
+
resolver?: CssResolver;
|
|
16
|
+
peerResolver?: PeerLoader;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Extract and compile all CSS-like dependencies for a given module.
|
|
20
|
+
*/
|
|
21
|
+
export declare function css(entry: string, options?: CssOptions): Promise<string>;
|
|
22
|
+
export {};
|
package/dist/css.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import dependencyTree from 'dependency-tree';
|
|
4
|
+
import { transform as lightningTransform, } from 'lightningcss';
|
|
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
|
+
export async function css(entry, options = {}) {
|
|
10
|
+
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
11
|
+
const entryPath = await resolveEntry(entry, cwd, options.resolver);
|
|
12
|
+
const extensions = (options.extensions ?? DEFAULT_EXTENSIONS).map(ext => ext.toLowerCase());
|
|
13
|
+
const files = collectStyleDependencies(entryPath, {
|
|
14
|
+
cwd,
|
|
15
|
+
extensions,
|
|
16
|
+
filter: options.filter,
|
|
17
|
+
dependencyTreeOptions: options.dependencyTree,
|
|
18
|
+
});
|
|
19
|
+
if (files.length === 0) {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
const chunks = [];
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
const chunk = await compileStyleModule(file, {
|
|
25
|
+
cwd,
|
|
26
|
+
peerResolver: options.peerResolver,
|
|
27
|
+
});
|
|
28
|
+
if (chunk) {
|
|
29
|
+
chunks.push(chunk);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let output = chunks.join('\n');
|
|
33
|
+
if (options.lightningcss) {
|
|
34
|
+
const lightningOptions = normalizeLightningOptions(options.lightningcss);
|
|
35
|
+
const { code } = lightningTransform({
|
|
36
|
+
...lightningOptions,
|
|
37
|
+
filename: lightningOptions.filename ?? 'extracted.css',
|
|
38
|
+
code: Buffer.from(output),
|
|
39
|
+
});
|
|
40
|
+
output = code.toString();
|
|
41
|
+
}
|
|
42
|
+
return output;
|
|
43
|
+
}
|
|
44
|
+
async function resolveEntry(entry, cwd, resolver) {
|
|
45
|
+
if (typeof resolver === 'function') {
|
|
46
|
+
const resolved = await resolver(entry, { cwd });
|
|
47
|
+
if (resolved) {
|
|
48
|
+
return resolved;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (path.isAbsolute(entry)) {
|
|
52
|
+
return entry;
|
|
53
|
+
}
|
|
54
|
+
return path.resolve(cwd, entry);
|
|
55
|
+
}
|
|
56
|
+
function collectStyleDependencies(entryPath, { cwd, extensions, filter, dependencyTreeOptions, }) {
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const order = [];
|
|
59
|
+
const shouldInclude = typeof filter === 'function'
|
|
60
|
+
? filter
|
|
61
|
+
: (filePath) => !filePath.includes('node_modules');
|
|
62
|
+
const entryIsStyle = Boolean(matchExtension(entryPath, extensions));
|
|
63
|
+
let treeList = [];
|
|
64
|
+
if (!entryIsStyle) {
|
|
65
|
+
const dependencyConfig = {
|
|
66
|
+
...dependencyTreeOptions,
|
|
67
|
+
filename: entryPath,
|
|
68
|
+
directory: cwd,
|
|
69
|
+
filter: shouldInclude,
|
|
70
|
+
};
|
|
71
|
+
treeList = dependencyTree.toList(dependencyConfig);
|
|
72
|
+
}
|
|
73
|
+
const candidates = entryIsStyle ? [entryPath] : [entryPath, ...treeList];
|
|
74
|
+
for (const candidate of candidates) {
|
|
75
|
+
const match = matchExtension(candidate, extensions);
|
|
76
|
+
if (!match || seen.has(candidate))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(candidate);
|
|
79
|
+
order.push({ path: path.resolve(candidate), ext: match });
|
|
80
|
+
}
|
|
81
|
+
return order;
|
|
82
|
+
}
|
|
83
|
+
function matchExtension(filePath, extensions) {
|
|
84
|
+
const lower = filePath.toLowerCase();
|
|
85
|
+
return extensions.find(ext => lower.endsWith(ext));
|
|
86
|
+
}
|
|
87
|
+
async function compileStyleModule(file, { cwd, peerResolver }) {
|
|
88
|
+
switch (file.ext) {
|
|
89
|
+
case '.css':
|
|
90
|
+
return fs.readFile(file.path, 'utf8');
|
|
91
|
+
case '.scss':
|
|
92
|
+
case '.sass':
|
|
93
|
+
return compileSass(file.path, file.ext === '.sass', peerResolver);
|
|
94
|
+
case '.less':
|
|
95
|
+
return compileLess(file.path, peerResolver);
|
|
96
|
+
case '.css.ts':
|
|
97
|
+
return compileVanillaExtract(file.path, cwd, peerResolver);
|
|
98
|
+
default:
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function compileSass(filePath, indented, peerResolver) {
|
|
103
|
+
const sassModule = await optionalPeer('sass', 'Sass', peerResolver);
|
|
104
|
+
const sass = sassModule;
|
|
105
|
+
const result = sass.compile(filePath, {
|
|
106
|
+
style: 'expanded',
|
|
107
|
+
});
|
|
108
|
+
return result.css;
|
|
109
|
+
}
|
|
110
|
+
async function compileLess(filePath, peerResolver) {
|
|
111
|
+
const mod = await optionalPeer('less', 'Less', peerResolver);
|
|
112
|
+
const less = unwrapModuleNamespace(mod);
|
|
113
|
+
const source = await fs.readFile(filePath, 'utf8');
|
|
114
|
+
const result = await less.render(source, { filename: filePath });
|
|
115
|
+
return result.css;
|
|
116
|
+
}
|
|
117
|
+
async function compileVanillaExtract(filePath, cwd, peerResolver) {
|
|
118
|
+
const mod = await optionalPeer('@vanilla-extract/integration', 'Vanilla Extract', peerResolver);
|
|
119
|
+
const namespace = unwrapModuleNamespace(mod);
|
|
120
|
+
const compileFn = namespace.compile;
|
|
121
|
+
const transformPlugin = namespace.vanillaExtractTransformPlugin;
|
|
122
|
+
const processVanillaFile = namespace.processVanillaFile;
|
|
123
|
+
const getSourceFromVirtualCssFile = namespace.getSourceFromVirtualCssFile;
|
|
124
|
+
if (!compileFn ||
|
|
125
|
+
!getSourceFromVirtualCssFile ||
|
|
126
|
+
!transformPlugin ||
|
|
127
|
+
!processVanillaFile) {
|
|
128
|
+
throw new Error('@knighted/css: Unable to load "@vanilla-extract/integration". Please ensure the package exports compile helpers.');
|
|
129
|
+
}
|
|
130
|
+
const identOption = process.env.NODE_ENV === 'production' ? 'short' : 'debug';
|
|
131
|
+
const { source } = await compileFn({
|
|
132
|
+
filePath,
|
|
133
|
+
cwd,
|
|
134
|
+
identOption,
|
|
135
|
+
esbuildOptions: {
|
|
136
|
+
plugins: [transformPlugin({ identOption })],
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const processedSource = await processVanillaFile({
|
|
140
|
+
source,
|
|
141
|
+
filePath,
|
|
142
|
+
identOption,
|
|
143
|
+
outputCss: true,
|
|
144
|
+
});
|
|
145
|
+
const imports = [];
|
|
146
|
+
const importRegex = /['"](?<id>[^'"]+\.vanilla\.css\?source=[^'"]+)['"]/gimu;
|
|
147
|
+
let match;
|
|
148
|
+
while ((match = importRegex.exec(processedSource)) !== null) {
|
|
149
|
+
const id = match.groups?.id ?? match[1];
|
|
150
|
+
if (!id)
|
|
151
|
+
continue;
|
|
152
|
+
const virtualFile = await getSourceFromVirtualCssFile(id);
|
|
153
|
+
if (virtualFile?.source) {
|
|
154
|
+
imports.push(virtualFile.source);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return imports.join('\n');
|
|
158
|
+
}
|
|
159
|
+
const defaultPeerLoader = name => import(name);
|
|
160
|
+
async function optionalPeer(name, label, loader) {
|
|
161
|
+
const importer = loader ?? defaultPeerLoader;
|
|
162
|
+
try {
|
|
163
|
+
return (await importer(name));
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error &&
|
|
167
|
+
typeof error === 'object' &&
|
|
168
|
+
'code' in error &&
|
|
169
|
+
typeof error.code === 'string' &&
|
|
170
|
+
/MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND/.test(error.code)) {
|
|
171
|
+
throw new Error(`@knighted/css: Attempted to process ${label}, but "${name}" is not installed. Please add it to your project.`);
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function unwrapModuleNamespace(mod) {
|
|
177
|
+
if (typeof mod === 'object' &&
|
|
178
|
+
mod !== null &&
|
|
179
|
+
'default' in mod &&
|
|
180
|
+
mod.default) {
|
|
181
|
+
return mod.default;
|
|
182
|
+
}
|
|
183
|
+
return mod;
|
|
184
|
+
}
|
|
185
|
+
function normalizeLightningOptions(config) {
|
|
186
|
+
if (!config || config === true) {
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
return config;
|
|
190
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@knighted/css",
|
|
3
|
+
"version": "1.0.0-alpha.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/css.js",
|
|
7
|
+
"types": "./dist/css.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/css.d.ts",
|
|
11
|
+
"import": "./dist/css.js",
|
|
12
|
+
"require": "./dist/cjs/css.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">= 22.15.0",
|
|
17
|
+
"npm": ">= 10.9.0"
|
|
18
|
+
},
|
|
19
|
+
"engineStrict": true,
|
|
20
|
+
"keywords": [
|
|
21
|
+
"css",
|
|
22
|
+
"extract-css",
|
|
23
|
+
"styles",
|
|
24
|
+
"dependency-graph",
|
|
25
|
+
"module-dependencies",
|
|
26
|
+
"lit",
|
|
27
|
+
"web-components",
|
|
28
|
+
"shadow-dom",
|
|
29
|
+
"sass",
|
|
30
|
+
"scss",
|
|
31
|
+
"less",
|
|
32
|
+
"vanilla-extract",
|
|
33
|
+
"lightningcss",
|
|
34
|
+
"build-time",
|
|
35
|
+
"static-analysis",
|
|
36
|
+
"css-in-js-zero-runtime"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "duel",
|
|
40
|
+
"test": "c8 --reporter=text --reporter=html tsx --test test/**/*.test.ts",
|
|
41
|
+
"prettier": "prettier --write .",
|
|
42
|
+
"prettier:check": "prettier --check .",
|
|
43
|
+
"lint": "oxlint src test",
|
|
44
|
+
"prepack": "npm run build",
|
|
45
|
+
"prepare": "husky"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"dependency-tree": "^9.0.0",
|
|
49
|
+
"lightningcss": "^1.30.2"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@vanilla-extract/integration": "^8.0.0",
|
|
53
|
+
"less": "^4.2.0",
|
|
54
|
+
"sass": "^1.80.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"@vanilla-extract/integration": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"less": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"sass": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@knighted/duel": "^2.1.6",
|
|
69
|
+
"@types/less": "^3.0.8",
|
|
70
|
+
"@vanilla-extract/css": "^1.15.2",
|
|
71
|
+
"@vanilla-extract/integration": "^8.0.6",
|
|
72
|
+
"@vanilla-extract/recipes": "^0.5.7",
|
|
73
|
+
"c8": "^10.1.2",
|
|
74
|
+
"husky": "^9.1.7",
|
|
75
|
+
"less": "^4.2.0",
|
|
76
|
+
"lint-staged": "^16.2.7",
|
|
77
|
+
"oxlint": "^0.4.1",
|
|
78
|
+
"prettier": "^3.7.4",
|
|
79
|
+
"sass": "^1.80.7",
|
|
80
|
+
"tsx": "^4.19.2",
|
|
81
|
+
"typescript": "^5.9.3"
|
|
82
|
+
},
|
|
83
|
+
"files": [
|
|
84
|
+
"dist"
|
|
85
|
+
],
|
|
86
|
+
"author": "KCM <knightedcodemonkey@gmail.com>",
|
|
87
|
+
"license": "MIT",
|
|
88
|
+
"repository": {
|
|
89
|
+
"type": "git",
|
|
90
|
+
"url": "git+https://github.com/knightedcodemonkey/css.git"
|
|
91
|
+
},
|
|
92
|
+
"bugs": {
|
|
93
|
+
"url": "https://github.com/knightedcodemonkey/css/issues"
|
|
94
|
+
},
|
|
95
|
+
"prettier": {
|
|
96
|
+
"arrowParens": "avoid",
|
|
97
|
+
"printWidth": 90,
|
|
98
|
+
"semi": false,
|
|
99
|
+
"singleQuote": true
|
|
100
|
+
},
|
|
101
|
+
"lint-staged": {
|
|
102
|
+
"*.{js,jsx,ts,tsx,mjs,cjs,cts,mts}": "oxlint",
|
|
103
|
+
"*.{js,jsx,ts,tsx,mjs,cjs,cts,mts,json,md,css,scss,html}": "prettier --check"
|
|
104
|
+
}
|
|
105
|
+
}
|