@knighted/css 1.0.0-alpha.2 → 1.0.0-alpha.4
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/dist/cjs/css.cjs +11 -0
- package/dist/cjs/css.d.cts +6 -0
- package/dist/cjs/helpers.cjs +131 -0
- package/dist/cjs/helpers.d.cts +27 -0
- package/dist/css.d.ts +6 -0
- package/dist/css.js +12 -1
- package/dist/helpers.d.ts +27 -0
- package/dist/helpers.js +124 -0
- package/dist/loader.d.ts +6 -4
- package/package.json +8 -62
- package/README.md +0 -190
- package/dist/index.d.ts +0 -4
- package/dist/loader-queries.d.ts +0 -7
package/dist/cjs/css.cjs
CHANGED
|
@@ -10,6 +10,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
10
10
|
const node_fs_1 = require("node:fs");
|
|
11
11
|
const dependency_tree_1 = __importDefault(require("dependency-tree"));
|
|
12
12
|
const lightningcss_1 = require("lightningcss");
|
|
13
|
+
const helpers_js_1 = require("./helpers.cjs");
|
|
13
14
|
exports.DEFAULT_EXTENSIONS = ['.css', '.scss', '.sass', '.less', '.css.ts'];
|
|
14
15
|
async function css(entry, options = {}) {
|
|
15
16
|
const { css: output } = await cssWithMeta(entry, options);
|
|
@@ -41,6 +42,13 @@ async function cssWithMeta(entry, options = {}) {
|
|
|
41
42
|
let output = chunks.join('\n');
|
|
42
43
|
if (options.lightningcss) {
|
|
43
44
|
const lightningOptions = normalizeLightningOptions(options.lightningcss);
|
|
45
|
+
const boostVisitor = (0, helpers_js_1.buildSpecificityVisitor)(options.specificityBoost);
|
|
46
|
+
const combinedVisitor = boostVisitor && lightningOptions.visitor
|
|
47
|
+
? (0, lightningcss_1.composeVisitors)([boostVisitor, lightningOptions.visitor])
|
|
48
|
+
: (boostVisitor ?? lightningOptions.visitor);
|
|
49
|
+
if (combinedVisitor) {
|
|
50
|
+
lightningOptions.visitor = combinedVisitor;
|
|
51
|
+
}
|
|
44
52
|
const { code } = (0, lightningcss_1.transform)({
|
|
45
53
|
...lightningOptions,
|
|
46
54
|
filename: lightningOptions.filename ?? 'extracted.css',
|
|
@@ -48,6 +56,9 @@ async function cssWithMeta(entry, options = {}) {
|
|
|
48
56
|
});
|
|
49
57
|
output = code.toString();
|
|
50
58
|
}
|
|
59
|
+
if (options.specificityBoost?.strategy && !options.specificityBoost.visitor) {
|
|
60
|
+
output = (0, helpers_js_1.applyStringSpecificityBoost)(output, options.specificityBoost);
|
|
61
|
+
}
|
|
51
62
|
return { css: output, files: files.map(file => file.path) };
|
|
52
63
|
}
|
|
53
64
|
async function resolveEntry(entry, cwd, resolver) {
|
package/dist/cjs/css.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Options as DependencyTreeOpts } from 'dependency-tree';
|
|
2
2
|
import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
|
|
3
|
+
import { type SpecificitySelector, type SpecificityStrategy } from './helpers.cjs';
|
|
3
4
|
export declare const DEFAULT_EXTENSIONS: string[];
|
|
4
5
|
type LightningCssConfig = boolean | Partial<Omit<LightningTransformOptions<never>, 'code'>>;
|
|
5
6
|
export type CssResolver = (specifier: string, ctx: {
|
|
@@ -11,6 +12,11 @@ export interface CssOptions {
|
|
|
11
12
|
cwd?: string;
|
|
12
13
|
filter?: (filePath: string) => boolean;
|
|
13
14
|
lightningcss?: LightningCssConfig;
|
|
15
|
+
specificityBoost?: {
|
|
16
|
+
visitor?: LightningTransformOptions<never>['visitor'];
|
|
17
|
+
strategy?: SpecificityStrategy;
|
|
18
|
+
match?: SpecificitySelector[];
|
|
19
|
+
};
|
|
14
20
|
dependencyTree?: Partial<Omit<DependencyTreeOpts, 'filename' | 'directory'>>;
|
|
15
21
|
resolver?: CssResolver;
|
|
16
22
|
peerResolver?: PeerLoader;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSpecificityVisitor = buildSpecificityVisitor;
|
|
4
|
+
exports.serializeSelector = serializeSelector;
|
|
5
|
+
exports.escapeRegex = escapeRegex;
|
|
6
|
+
exports.findLastClassName = findLastClassName;
|
|
7
|
+
exports.applyStringSpecificityBoost = applyStringSpecificityBoost;
|
|
8
|
+
function buildSpecificityVisitor(boost) {
|
|
9
|
+
if (!boost)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (boost.visitor)
|
|
12
|
+
return boost.visitor;
|
|
13
|
+
if (!boost.strategy)
|
|
14
|
+
return undefined;
|
|
15
|
+
const matchers = (boost.match ?? []).map(m => typeof m === 'string' ? new RegExp(`^${escapeRegex(m)}$`) : m);
|
|
16
|
+
const shouldApply = (selectorStr) => matchers.length === 0 ? true : matchers.some(rx => rx.test(selectorStr));
|
|
17
|
+
if (boost.strategy.type === 'repeat-class') {
|
|
18
|
+
const times = Math.max(1, boost.strategy.times ?? 1);
|
|
19
|
+
const visitor = {
|
|
20
|
+
Rule: {
|
|
21
|
+
style(rule) {
|
|
22
|
+
if (!rule || !Array.isArray(rule.selectors))
|
|
23
|
+
return rule;
|
|
24
|
+
const newSelectors = rule.selectors.map((sel) => {
|
|
25
|
+
const selectorStr = serializeSelector(sel);
|
|
26
|
+
if (!shouldApply(selectorStr))
|
|
27
|
+
return sel;
|
|
28
|
+
const lastClassName = findLastClassName(selectorStr);
|
|
29
|
+
if (!lastClassName)
|
|
30
|
+
return sel;
|
|
31
|
+
const repeats = Array.from({ length: times }, () => ({
|
|
32
|
+
type: 'class',
|
|
33
|
+
value: lastClassName,
|
|
34
|
+
}));
|
|
35
|
+
return [...sel, ...repeats];
|
|
36
|
+
});
|
|
37
|
+
return { ...rule, selectors: newSelectors };
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return visitor;
|
|
42
|
+
}
|
|
43
|
+
if (boost.strategy.type === 'append-where') {
|
|
44
|
+
const token = boost.strategy.token;
|
|
45
|
+
const visitor = {
|
|
46
|
+
Rule: {
|
|
47
|
+
style(rule) {
|
|
48
|
+
if (!rule || !Array.isArray(rule.selectors))
|
|
49
|
+
return rule;
|
|
50
|
+
const newSelectors = rule.selectors.map((sel) => {
|
|
51
|
+
const selectorStr = serializeSelector(sel);
|
|
52
|
+
if (!shouldApply(selectorStr))
|
|
53
|
+
return sel;
|
|
54
|
+
return [
|
|
55
|
+
...sel,
|
|
56
|
+
{
|
|
57
|
+
type: 'pseudo-class',
|
|
58
|
+
kind: 'where',
|
|
59
|
+
selectors: [[{ type: 'class', value: token.replace(/^\./, '') }]],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
});
|
|
63
|
+
return { ...rule, selectors: newSelectors };
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
return visitor;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
function serializeSelector(sel) {
|
|
72
|
+
return sel
|
|
73
|
+
.map(node => {
|
|
74
|
+
if (node.type === 'class')
|
|
75
|
+
return `.${node.value ?? node.name ?? ''}`;
|
|
76
|
+
if (node.type === 'id')
|
|
77
|
+
return `#${node.value ?? node.name ?? ''}`;
|
|
78
|
+
if (node.type === 'type')
|
|
79
|
+
return node.name ?? '';
|
|
80
|
+
if (node.type === 'pseudo-class')
|
|
81
|
+
return `:${node.kind ?? ''}`;
|
|
82
|
+
if (node.type === 'combinator')
|
|
83
|
+
return ` ${node.value ?? ''} `;
|
|
84
|
+
return '';
|
|
85
|
+
})
|
|
86
|
+
.join('')
|
|
87
|
+
.trim();
|
|
88
|
+
}
|
|
89
|
+
function escapeRegex(str) {
|
|
90
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
91
|
+
}
|
|
92
|
+
function findLastClassName(selector) {
|
|
93
|
+
let match;
|
|
94
|
+
let last;
|
|
95
|
+
const rx = /\.([A-Za-z0-9_-]+)/g;
|
|
96
|
+
while ((match = rx.exec(selector)) !== null) {
|
|
97
|
+
last = match[1];
|
|
98
|
+
}
|
|
99
|
+
return last;
|
|
100
|
+
}
|
|
101
|
+
function applyStringSpecificityBoost(css, boost) {
|
|
102
|
+
if (!boost.strategy)
|
|
103
|
+
return css;
|
|
104
|
+
const matchers = (boost.match ?? []).map(m => typeof m === 'string' ? new RegExp(`\\.${escapeRegex(m)}(?![\\w-])`, 'g') : m);
|
|
105
|
+
const applyAll = matchers.length === 0;
|
|
106
|
+
if (boost.strategy.type === 'repeat-class') {
|
|
107
|
+
const times = Math.max(1, boost.strategy.times ?? 1);
|
|
108
|
+
const duplicate = (cls) => cls + cls.repeat(times);
|
|
109
|
+
if (applyAll) {
|
|
110
|
+
return css.replace(/\.[A-Za-z0-9_-]+/g, m => duplicate(m));
|
|
111
|
+
}
|
|
112
|
+
let result = css;
|
|
113
|
+
for (const rx of matchers) {
|
|
114
|
+
result = result.replace(rx, m => duplicate(m));
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
if (boost.strategy.type === 'append-where') {
|
|
119
|
+
const token = boost.strategy.token.replace(/^\./, '');
|
|
120
|
+
const suffix = `:where(.${token})`;
|
|
121
|
+
if (applyAll) {
|
|
122
|
+
return css.replace(/\.[A-Za-z0-9_-]+/g, m => `${m}${suffix}`);
|
|
123
|
+
}
|
|
124
|
+
let result = css;
|
|
125
|
+
for (const rx of matchers) {
|
|
126
|
+
result = result.replace(rx, m => `${m}${suffix}`);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
return css;
|
|
131
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
|
|
2
|
+
export type SpecificitySelector = string | RegExp;
|
|
3
|
+
export type LightningVisitor = LightningTransformOptions<Record<string, never>>['visitor'];
|
|
4
|
+
export type SpecificityStrategy = {
|
|
5
|
+
type: 'append-where';
|
|
6
|
+
token: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'repeat-class';
|
|
9
|
+
times?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function buildSpecificityVisitor(boost?: {
|
|
12
|
+
visitor?: LightningVisitor;
|
|
13
|
+
strategy?: SpecificityStrategy;
|
|
14
|
+
match?: SpecificitySelector[];
|
|
15
|
+
}): LightningVisitor | undefined;
|
|
16
|
+
export declare function serializeSelector(sel: Array<{
|
|
17
|
+
type: string;
|
|
18
|
+
value?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
kind?: string;
|
|
21
|
+
}>): string;
|
|
22
|
+
export declare function escapeRegex(str: string): string;
|
|
23
|
+
export declare function findLastClassName(selector: string): string | undefined;
|
|
24
|
+
export declare function applyStringSpecificityBoost(css: string, boost: {
|
|
25
|
+
strategy?: SpecificityStrategy;
|
|
26
|
+
match?: SpecificitySelector[];
|
|
27
|
+
}): string;
|
package/dist/css.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Options as DependencyTreeOpts } from 'dependency-tree';
|
|
2
2
|
import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
|
|
3
|
+
import { type SpecificitySelector, type SpecificityStrategy } from './helpers.js';
|
|
3
4
|
export declare const DEFAULT_EXTENSIONS: string[];
|
|
4
5
|
type LightningCssConfig = boolean | Partial<Omit<LightningTransformOptions<never>, 'code'>>;
|
|
5
6
|
export type CssResolver = (specifier: string, ctx: {
|
|
@@ -11,6 +12,11 @@ export interface CssOptions {
|
|
|
11
12
|
cwd?: string;
|
|
12
13
|
filter?: (filePath: string) => boolean;
|
|
13
14
|
lightningcss?: LightningCssConfig;
|
|
15
|
+
specificityBoost?: {
|
|
16
|
+
visitor?: LightningTransformOptions<never>['visitor'];
|
|
17
|
+
strategy?: SpecificityStrategy;
|
|
18
|
+
match?: SpecificitySelector[];
|
|
19
|
+
};
|
|
14
20
|
dependencyTree?: Partial<Omit<DependencyTreeOpts, 'filename' | 'directory'>>;
|
|
15
21
|
resolver?: CssResolver;
|
|
16
22
|
peerResolver?: PeerLoader;
|
package/dist/css.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import dependencyTree from 'dependency-tree';
|
|
4
|
-
import { transform as lightningTransform, } from 'lightningcss';
|
|
4
|
+
import { composeVisitors, transform as lightningTransform, } from 'lightningcss';
|
|
5
|
+
import { applyStringSpecificityBoost, buildSpecificityVisitor, } from './helpers.js';
|
|
5
6
|
export const DEFAULT_EXTENSIONS = ['.css', '.scss', '.sass', '.less', '.css.ts'];
|
|
6
7
|
export async function css(entry, options = {}) {
|
|
7
8
|
const { css: output } = await cssWithMeta(entry, options);
|
|
@@ -33,6 +34,13 @@ export async function cssWithMeta(entry, options = {}) {
|
|
|
33
34
|
let output = chunks.join('\n');
|
|
34
35
|
if (options.lightningcss) {
|
|
35
36
|
const lightningOptions = normalizeLightningOptions(options.lightningcss);
|
|
37
|
+
const boostVisitor = buildSpecificityVisitor(options.specificityBoost);
|
|
38
|
+
const combinedVisitor = boostVisitor && lightningOptions.visitor
|
|
39
|
+
? composeVisitors([boostVisitor, lightningOptions.visitor])
|
|
40
|
+
: (boostVisitor ?? lightningOptions.visitor);
|
|
41
|
+
if (combinedVisitor) {
|
|
42
|
+
lightningOptions.visitor = combinedVisitor;
|
|
43
|
+
}
|
|
36
44
|
const { code } = lightningTransform({
|
|
37
45
|
...lightningOptions,
|
|
38
46
|
filename: lightningOptions.filename ?? 'extracted.css',
|
|
@@ -40,6 +48,9 @@ export async function cssWithMeta(entry, options = {}) {
|
|
|
40
48
|
});
|
|
41
49
|
output = code.toString();
|
|
42
50
|
}
|
|
51
|
+
if (options.specificityBoost?.strategy && !options.specificityBoost.visitor) {
|
|
52
|
+
output = applyStringSpecificityBoost(output, options.specificityBoost);
|
|
53
|
+
}
|
|
43
54
|
return { css: output, files: files.map(file => file.path) };
|
|
44
55
|
}
|
|
45
56
|
async function resolveEntry(entry, cwd, resolver) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
|
|
2
|
+
export type SpecificitySelector = string | RegExp;
|
|
3
|
+
export type LightningVisitor = LightningTransformOptions<Record<string, never>>['visitor'];
|
|
4
|
+
export type SpecificityStrategy = {
|
|
5
|
+
type: 'append-where';
|
|
6
|
+
token: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'repeat-class';
|
|
9
|
+
times?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function buildSpecificityVisitor(boost?: {
|
|
12
|
+
visitor?: LightningVisitor;
|
|
13
|
+
strategy?: SpecificityStrategy;
|
|
14
|
+
match?: SpecificitySelector[];
|
|
15
|
+
}): LightningVisitor | undefined;
|
|
16
|
+
export declare function serializeSelector(sel: Array<{
|
|
17
|
+
type: string;
|
|
18
|
+
value?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
kind?: string;
|
|
21
|
+
}>): string;
|
|
22
|
+
export declare function escapeRegex(str: string): string;
|
|
23
|
+
export declare function findLastClassName(selector: string): string | undefined;
|
|
24
|
+
export declare function applyStringSpecificityBoost(css: string, boost: {
|
|
25
|
+
strategy?: SpecificityStrategy;
|
|
26
|
+
match?: SpecificitySelector[];
|
|
27
|
+
}): string;
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export function buildSpecificityVisitor(boost) {
|
|
2
|
+
if (!boost)
|
|
3
|
+
return undefined;
|
|
4
|
+
if (boost.visitor)
|
|
5
|
+
return boost.visitor;
|
|
6
|
+
if (!boost.strategy)
|
|
7
|
+
return undefined;
|
|
8
|
+
const matchers = (boost.match ?? []).map(m => typeof m === 'string' ? new RegExp(`^${escapeRegex(m)}$`) : m);
|
|
9
|
+
const shouldApply = (selectorStr) => matchers.length === 0 ? true : matchers.some(rx => rx.test(selectorStr));
|
|
10
|
+
if (boost.strategy.type === 'repeat-class') {
|
|
11
|
+
const times = Math.max(1, boost.strategy.times ?? 1);
|
|
12
|
+
const visitor = {
|
|
13
|
+
Rule: {
|
|
14
|
+
style(rule) {
|
|
15
|
+
if (!rule || !Array.isArray(rule.selectors))
|
|
16
|
+
return rule;
|
|
17
|
+
const newSelectors = rule.selectors.map((sel) => {
|
|
18
|
+
const selectorStr = serializeSelector(sel);
|
|
19
|
+
if (!shouldApply(selectorStr))
|
|
20
|
+
return sel;
|
|
21
|
+
const lastClassName = findLastClassName(selectorStr);
|
|
22
|
+
if (!lastClassName)
|
|
23
|
+
return sel;
|
|
24
|
+
const repeats = Array.from({ length: times }, () => ({
|
|
25
|
+
type: 'class',
|
|
26
|
+
value: lastClassName,
|
|
27
|
+
}));
|
|
28
|
+
return [...sel, ...repeats];
|
|
29
|
+
});
|
|
30
|
+
return { ...rule, selectors: newSelectors };
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
return visitor;
|
|
35
|
+
}
|
|
36
|
+
if (boost.strategy.type === 'append-where') {
|
|
37
|
+
const token = boost.strategy.token;
|
|
38
|
+
const visitor = {
|
|
39
|
+
Rule: {
|
|
40
|
+
style(rule) {
|
|
41
|
+
if (!rule || !Array.isArray(rule.selectors))
|
|
42
|
+
return rule;
|
|
43
|
+
const newSelectors = rule.selectors.map((sel) => {
|
|
44
|
+
const selectorStr = serializeSelector(sel);
|
|
45
|
+
if (!shouldApply(selectorStr))
|
|
46
|
+
return sel;
|
|
47
|
+
return [
|
|
48
|
+
...sel,
|
|
49
|
+
{
|
|
50
|
+
type: 'pseudo-class',
|
|
51
|
+
kind: 'where',
|
|
52
|
+
selectors: [[{ type: 'class', value: token.replace(/^\./, '') }]],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
});
|
|
56
|
+
return { ...rule, selectors: newSelectors };
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return visitor;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
export function serializeSelector(sel) {
|
|
65
|
+
return sel
|
|
66
|
+
.map(node => {
|
|
67
|
+
if (node.type === 'class')
|
|
68
|
+
return `.${node.value ?? node.name ?? ''}`;
|
|
69
|
+
if (node.type === 'id')
|
|
70
|
+
return `#${node.value ?? node.name ?? ''}`;
|
|
71
|
+
if (node.type === 'type')
|
|
72
|
+
return node.name ?? '';
|
|
73
|
+
if (node.type === 'pseudo-class')
|
|
74
|
+
return `:${node.kind ?? ''}`;
|
|
75
|
+
if (node.type === 'combinator')
|
|
76
|
+
return ` ${node.value ?? ''} `;
|
|
77
|
+
return '';
|
|
78
|
+
})
|
|
79
|
+
.join('')
|
|
80
|
+
.trim();
|
|
81
|
+
}
|
|
82
|
+
export function escapeRegex(str) {
|
|
83
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
84
|
+
}
|
|
85
|
+
export function findLastClassName(selector) {
|
|
86
|
+
let match;
|
|
87
|
+
let last;
|
|
88
|
+
const rx = /\.([A-Za-z0-9_-]+)/g;
|
|
89
|
+
while ((match = rx.exec(selector)) !== null) {
|
|
90
|
+
last = match[1];
|
|
91
|
+
}
|
|
92
|
+
return last;
|
|
93
|
+
}
|
|
94
|
+
export function applyStringSpecificityBoost(css, boost) {
|
|
95
|
+
if (!boost.strategy)
|
|
96
|
+
return css;
|
|
97
|
+
const matchers = (boost.match ?? []).map(m => typeof m === 'string' ? new RegExp(`\\.${escapeRegex(m)}(?![\\w-])`, 'g') : m);
|
|
98
|
+
const applyAll = matchers.length === 0;
|
|
99
|
+
if (boost.strategy.type === 'repeat-class') {
|
|
100
|
+
const times = Math.max(1, boost.strategy.times ?? 1);
|
|
101
|
+
const duplicate = (cls) => cls + cls.repeat(times);
|
|
102
|
+
if (applyAll) {
|
|
103
|
+
return css.replace(/\.[A-Za-z0-9_-]+/g, m => duplicate(m));
|
|
104
|
+
}
|
|
105
|
+
let result = css;
|
|
106
|
+
for (const rx of matchers) {
|
|
107
|
+
result = result.replace(rx, m => duplicate(m));
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
if (boost.strategy.type === 'append-where') {
|
|
112
|
+
const token = boost.strategy.token.replace(/^\./, '');
|
|
113
|
+
const suffix = `:where(.${token})`;
|
|
114
|
+
if (applyAll) {
|
|
115
|
+
return css.replace(/\.[A-Za-z0-9_-]+/g, m => `${m}${suffix}`);
|
|
116
|
+
}
|
|
117
|
+
let result = css;
|
|
118
|
+
for (const rx of matchers) {
|
|
119
|
+
result = result.replace(rx, m => `${m}${suffix}`);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
return css;
|
|
124
|
+
}
|
package/dist/loader.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
1
|
+
import type { LoaderDefinitionFunction } from 'webpack';
|
|
2
|
+
import { type CssOptions } from './css.js';
|
|
3
|
+
export interface KnightedCssLoaderOptions extends CssOptions {
|
|
4
|
+
}
|
|
5
|
+
declare const loader: LoaderDefinitionFunction<KnightedCssLoaderOptions>;
|
|
6
|
+
export default loader;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knighted/css",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.4",
|
|
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/
|
|
7
|
+
"types": "./dist/css.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./dist/
|
|
10
|
+
"types": "./dist/css.d.ts",
|
|
11
11
|
"import": "./dist/css.js",
|
|
12
12
|
"require": "./dist/cjs/css.cjs"
|
|
13
13
|
},
|
|
@@ -17,46 +17,23 @@
|
|
|
17
17
|
"require": "./dist/cjs/loader.cjs"
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
|
-
"engines": {
|
|
21
|
-
"node": ">= 22.15.0",
|
|
22
|
-
"npm": ">= 10.9.0"
|
|
23
|
-
},
|
|
24
|
-
"engineStrict": true,
|
|
25
20
|
"keywords": [
|
|
26
21
|
"css",
|
|
27
|
-
"extract-css",
|
|
28
|
-
"styles",
|
|
29
22
|
"dependency-graph",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"shadow-dom",
|
|
23
|
+
"build-time",
|
|
24
|
+
"compile",
|
|
25
|
+
"styles",
|
|
34
26
|
"sass",
|
|
35
27
|
"scss",
|
|
36
28
|
"less",
|
|
37
29
|
"vanilla-extract",
|
|
38
|
-
"lightningcss"
|
|
39
|
-
"build-time",
|
|
40
|
-
"static-analysis",
|
|
41
|
-
"css-in-js-zero-runtime"
|
|
30
|
+
"lightningcss"
|
|
42
31
|
],
|
|
43
32
|
"scripts": {
|
|
44
33
|
"build": "duel",
|
|
45
|
-
"postbuild": "cp src/types/*.d.ts dist/",
|
|
46
34
|
"check-types": "tsc --noEmit",
|
|
47
35
|
"test": "c8 --reporter=text --reporter=text-summary --reporter=lcov --include \"src/**/*.ts\" tsx --test test/**/*.test.ts",
|
|
48
|
-
"
|
|
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",
|
|
55
|
-
"prettier": "prettier --write .",
|
|
56
|
-
"prettier:check": "prettier --check .",
|
|
57
|
-
"lint": "oxlint src test",
|
|
58
|
-
"prepack": "npm run build",
|
|
59
|
-
"prepare": "husky"
|
|
36
|
+
"prepack": "npm run build"
|
|
60
37
|
},
|
|
61
38
|
"dependencies": {
|
|
62
39
|
"dependency-tree": "^11.2.0",
|
|
@@ -78,27 +55,6 @@
|
|
|
78
55
|
"optional": true
|
|
79
56
|
}
|
|
80
57
|
},
|
|
81
|
-
"devDependencies": {
|
|
82
|
-
"@knighted/duel": "^2.1.6",
|
|
83
|
-
"@playwright/test": "^1.48.2",
|
|
84
|
-
"@rspack/cli": "^1.0.0",
|
|
85
|
-
"@rspack/core": "^1.0.0",
|
|
86
|
-
"@types/less": "^3.0.8",
|
|
87
|
-
"@types/webpack": "^5.28.5",
|
|
88
|
-
"@vanilla-extract/css": "^1.15.2",
|
|
89
|
-
"@vanilla-extract/integration": "^8.0.6",
|
|
90
|
-
"@vanilla-extract/recipes": "^0.5.7",
|
|
91
|
-
"c8": "^10.1.2",
|
|
92
|
-
"http-server": "^14.1.1",
|
|
93
|
-
"husky": "^9.1.7",
|
|
94
|
-
"less": "^4.2.0",
|
|
95
|
-
"lint-staged": "^16.2.7",
|
|
96
|
-
"oxlint": "^0.4.1",
|
|
97
|
-
"prettier": "^3.7.4",
|
|
98
|
-
"sass": "^1.80.7",
|
|
99
|
-
"tsx": "^4.19.2",
|
|
100
|
-
"typescript": "^5.9.3"
|
|
101
|
-
},
|
|
102
58
|
"files": [
|
|
103
59
|
"dist"
|
|
104
60
|
],
|
|
@@ -110,15 +66,5 @@
|
|
|
110
66
|
},
|
|
111
67
|
"bugs": {
|
|
112
68
|
"url": "https://github.com/knightedcodemonkey/css/issues"
|
|
113
|
-
},
|
|
114
|
-
"prettier": {
|
|
115
|
-
"arrowParens": "avoid",
|
|
116
|
-
"printWidth": 90,
|
|
117
|
-
"semi": false,
|
|
118
|
-
"singleQuote": true
|
|
119
|
-
},
|
|
120
|
-
"lint-staged": {
|
|
121
|
-
"*.{js,jsx,ts,tsx,mjs,cjs,cts,mts}": "oxlint",
|
|
122
|
-
"*.{js,jsx,ts,tsx,mjs,cjs,cts,mts,json,md,css,scss,html}": "prettier --check"
|
|
123
69
|
}
|
|
124
70
|
}
|
package/README.md
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
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 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
|
-
|
|
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
|
-
```
|
|
28
|
-
|
|
29
|
-
Install the peers your project is using, for example `less`, or `sass`, etc.
|
|
30
|
-
|
|
31
|
-
## Quick Start
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
// scripts/extract-styles.ts
|
|
35
|
-
import { css } from '@knighted/css'
|
|
36
|
-
|
|
37
|
-
const styles = await css('./src/components/app.ts', {
|
|
38
|
-
cwd: process.cwd(),
|
|
39
|
-
lightningcss: { minify: true },
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
console.log(styles)
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
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.
|
|
46
|
-
|
|
47
|
-
## API
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
type CssOptions = {
|
|
51
|
-
extensions?: string[] // customize file extensions to scan
|
|
52
|
-
cwd?: string // working directory (defaults to process.cwd())
|
|
53
|
-
filter?: (filePath: string) => boolean
|
|
54
|
-
lightningcss?: boolean | LightningTransformOptions
|
|
55
|
-
dependencyTree?: DependencyTreeOptions
|
|
56
|
-
resolver?: (
|
|
57
|
-
specifier: string,
|
|
58
|
-
ctx: { cwd: string },
|
|
59
|
-
) => string | Promise<string | undefined>
|
|
60
|
-
peerResolver?: (name: string) => Promise<unknown> // for custom module loading
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function css(entry: string, options?: CssOptions): Promise<string>
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Typical customizations:
|
|
67
|
-
|
|
68
|
-
- **filter** – Skip certain paths (e.g., storybook-only styles) before compilation.
|
|
69
|
-
- **resolver** – Resolve virtual specifiers the way your bundler does (the repo ships test fixtures for webpack, Vite, and Rspack).
|
|
70
|
-
- **lightningcss** – Pass `true` for defaults or a config object for minification/autoprefixing.
|
|
71
|
-
|
|
72
|
-
## Examples
|
|
73
|
-
|
|
74
|
-
### Generate standalone stylesheets
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
import { writeFile } from 'node:fs/promises'
|
|
78
|
-
import { css } from '@knighted/css'
|
|
79
|
-
|
|
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 } },
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
await writeFile('./dist/marketing-page.css', sheet)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Inline CSS during SSR
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
import { renderToString } from 'react-dom/server'
|
|
92
|
-
import { css } from '@knighted/css'
|
|
93
|
-
|
|
94
|
-
export async function render(url: string) {
|
|
95
|
-
const styles = await css('./src/routes/root.tsx')
|
|
96
|
-
const html = renderToString(<App url={url} />)
|
|
97
|
-
return `<!doctype html><style>${styles}</style>${html}`
|
|
98
|
-
}
|
|
99
|
-
```
|
|
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
|
-
|
|
175
|
-
## Scripts
|
|
176
|
-
|
|
177
|
-
- `npm run build` – Produce CJS/ESM outputs via `@knighted/duel`.
|
|
178
|
-
- `npm test` – Runs the Node test suite with `tsx` and reports coverage via `c8`.
|
|
179
|
-
- `npm run lint` – Static analysis through `oxlint`.
|
|
180
|
-
|
|
181
|
-
## Contributing
|
|
182
|
-
|
|
183
|
-
1. Clone the repo and install dependencies with `npm install`.
|
|
184
|
-
2. Run `npm test` to ensure fixtures compile across Sass/Less/vanilla-extract.
|
|
185
|
-
3. Add/adjust fixtures in `fixtures/` when adding new language features to keep coverage high.
|
|
186
|
-
4. Open a PR with a description of the change and tests.
|
|
187
|
-
|
|
188
|
-
## License
|
|
189
|
-
|
|
190
|
-
MIT © Knighted Code Monkey
|
package/dist/index.d.ts
DELETED
package/dist/loader-queries.d.ts
DELETED