@knighted/css 1.0.0-alpha.3 → 1.0.0-rc.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/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/cjs/loader.cjs +95 -17
- package/dist/cjs/loader.d.cts +5 -1
- 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 +5 -1
- package/dist/loader.js +93 -17
- package/package.json +1 -1
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/cjs/loader.cjs
CHANGED
|
@@ -1,25 +1,103 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pitch = void 0;
|
|
3
4
|
const css_js_1 = require("./css.cjs");
|
|
4
5
|
const DEFAULT_EXPORT_NAME = 'knightedCss';
|
|
6
|
+
const COMBINED_QUERY_FLAG = 'combined';
|
|
5
7
|
const loader = async function loader(source) {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
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`;
|
|
8
|
+
const cssOptions = resolveCssOptions(this);
|
|
9
|
+
const css = await extractCss(this, cssOptions);
|
|
10
|
+
const injection = buildInjection(css);
|
|
11
|
+
const input = toSourceString(source);
|
|
19
12
|
const isStyleModule = this.resourcePath.endsWith('.css.ts');
|
|
20
|
-
|
|
21
|
-
? `${injection}export default {};\n`
|
|
22
|
-
: `${input}${injection}`;
|
|
23
|
-
return output;
|
|
13
|
+
return isStyleModule ? `${injection}export default {};\n` : `${input}${injection}`;
|
|
24
14
|
};
|
|
15
|
+
const pitch = function pitch() {
|
|
16
|
+
if (!hasCombinedQuery(this.resourceQuery)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const request = buildProxyRequest(this);
|
|
20
|
+
const cssOptions = resolveCssOptions(this);
|
|
21
|
+
return extractCss(this, cssOptions).then(css => createCombinedModule(request, css));
|
|
22
|
+
};
|
|
23
|
+
exports.pitch = pitch;
|
|
24
|
+
loader.pitch = exports.pitch;
|
|
25
25
|
exports.default = loader;
|
|
26
|
+
function resolveCssOptions(ctx) {
|
|
27
|
+
const rawOptions = (typeof ctx.getOptions === 'function' ? ctx.getOptions() : {});
|
|
28
|
+
return {
|
|
29
|
+
...rawOptions,
|
|
30
|
+
cwd: rawOptions.cwd ?? ctx.rootContext ?? process.cwd(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function extractCss(ctx, options) {
|
|
34
|
+
const { css, files } = await (0, css_js_1.cssWithMeta)(ctx.resourcePath, options);
|
|
35
|
+
const uniqueFiles = new Set([ctx.resourcePath, ...files]);
|
|
36
|
+
for (const file of uniqueFiles) {
|
|
37
|
+
ctx.addDependency(file);
|
|
38
|
+
}
|
|
39
|
+
return css;
|
|
40
|
+
}
|
|
41
|
+
function toSourceString(source) {
|
|
42
|
+
return typeof source === 'string' ? source : source.toString('utf8');
|
|
43
|
+
}
|
|
44
|
+
function buildInjection(css) {
|
|
45
|
+
return `\n\nexport const ${DEFAULT_EXPORT_NAME} = ${JSON.stringify(css)};\n`;
|
|
46
|
+
}
|
|
47
|
+
function hasCombinedQuery(query) {
|
|
48
|
+
if (!query)
|
|
49
|
+
return false;
|
|
50
|
+
const trimmed = query.startsWith('?') ? query.slice(1) : query;
|
|
51
|
+
if (!trimmed)
|
|
52
|
+
return false;
|
|
53
|
+
return trimmed
|
|
54
|
+
.split('&')
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.some(part => isQueryFlag(part, COMBINED_QUERY_FLAG));
|
|
57
|
+
}
|
|
58
|
+
function buildProxyRequest(ctx) {
|
|
59
|
+
const sanitizedQuery = buildSanitizedQuery(ctx.resourceQuery);
|
|
60
|
+
const request = `${ctx.resourcePath}${sanitizedQuery}`;
|
|
61
|
+
const context = ctx.context ?? ctx.rootContext ?? process.cwd();
|
|
62
|
+
if (ctx.utils && typeof ctx.utils.contextify === 'function') {
|
|
63
|
+
return ctx.utils.contextify(context, request);
|
|
64
|
+
}
|
|
65
|
+
return request;
|
|
66
|
+
}
|
|
67
|
+
function buildSanitizedQuery(query) {
|
|
68
|
+
if (!query)
|
|
69
|
+
return '';
|
|
70
|
+
const entries = splitQuery(query).filter(part => {
|
|
71
|
+
return !isQueryFlag(part, COMBINED_QUERY_FLAG) && !isQueryFlag(part, 'knighted-css');
|
|
72
|
+
});
|
|
73
|
+
return entries.length > 0 ? `?${entries.join('&')}` : '';
|
|
74
|
+
}
|
|
75
|
+
function splitQuery(query) {
|
|
76
|
+
const trimmed = query.startsWith('?') ? query.slice(1) : query;
|
|
77
|
+
if (!trimmed)
|
|
78
|
+
return [];
|
|
79
|
+
return trimmed.split('&').filter(Boolean);
|
|
80
|
+
}
|
|
81
|
+
function isQueryFlag(entry, flag) {
|
|
82
|
+
const [rawKey] = entry.split('=');
|
|
83
|
+
try {
|
|
84
|
+
return decodeURIComponent(rawKey) === flag;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return rawKey === flag;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function createCombinedModule(request, css) {
|
|
91
|
+
const requestLiteral = JSON.stringify(request);
|
|
92
|
+
const defaultExport = `const __knightedDefault =
|
|
93
|
+
typeof __knightedModule.default !== 'undefined'
|
|
94
|
+
? __knightedModule.default
|
|
95
|
+
: __knightedModule;`;
|
|
96
|
+
return [
|
|
97
|
+
`import * as __knightedModule from ${requestLiteral};`,
|
|
98
|
+
`export * from ${requestLiteral};`,
|
|
99
|
+
defaultExport,
|
|
100
|
+
'export default __knightedDefault;',
|
|
101
|
+
buildInjection(css),
|
|
102
|
+
].join('\n');
|
|
103
|
+
}
|
package/dist/cjs/loader.d.cts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { LoaderDefinitionFunction } from 'webpack';
|
|
1
|
+
import type { LoaderDefinitionFunction, PitchLoaderDefinitionFunction } from 'webpack';
|
|
2
2
|
import { type CssOptions } from './css.cjs';
|
|
3
|
+
export type KnightedCssCombinedModule<TModule> = TModule & {
|
|
4
|
+
knightedCss: string;
|
|
5
|
+
};
|
|
3
6
|
export interface KnightedCssLoaderOptions extends CssOptions {
|
|
4
7
|
}
|
|
5
8
|
declare const loader: LoaderDefinitionFunction<KnightedCssLoaderOptions>;
|
|
9
|
+
export declare const pitch: PitchLoaderDefinitionFunction<KnightedCssLoaderOptions>;
|
|
6
10
|
export default loader;
|
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,6 +1,10 @@
|
|
|
1
|
-
import type { LoaderDefinitionFunction } from 'webpack';
|
|
1
|
+
import type { LoaderDefinitionFunction, PitchLoaderDefinitionFunction } from 'webpack';
|
|
2
2
|
import { type CssOptions } from './css.js';
|
|
3
|
+
export type KnightedCssCombinedModule<TModule> = TModule & {
|
|
4
|
+
knightedCss: string;
|
|
5
|
+
};
|
|
3
6
|
export interface KnightedCssLoaderOptions extends CssOptions {
|
|
4
7
|
}
|
|
5
8
|
declare const loader: LoaderDefinitionFunction<KnightedCssLoaderOptions>;
|
|
9
|
+
export declare const pitch: PitchLoaderDefinitionFunction<KnightedCssLoaderOptions>;
|
|
6
10
|
export default loader;
|
package/dist/loader.js
CHANGED
|
@@ -1,23 +1,99 @@
|
|
|
1
1
|
import { cssWithMeta } from './css.js';
|
|
2
2
|
const DEFAULT_EXPORT_NAME = 'knightedCss';
|
|
3
|
+
const COMBINED_QUERY_FLAG = 'combined';
|
|
3
4
|
const loader = async function loader(source) {
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
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`;
|
|
5
|
+
const cssOptions = resolveCssOptions(this);
|
|
6
|
+
const css = await extractCss(this, cssOptions);
|
|
7
|
+
const injection = buildInjection(css);
|
|
8
|
+
const input = toSourceString(source);
|
|
17
9
|
const isStyleModule = this.resourcePath.endsWith('.css.ts');
|
|
18
|
-
|
|
19
|
-
? `${injection}export default {};\n`
|
|
20
|
-
: `${input}${injection}`;
|
|
21
|
-
return output;
|
|
10
|
+
return isStyleModule ? `${injection}export default {};\n` : `${input}${injection}`;
|
|
22
11
|
};
|
|
12
|
+
export const pitch = function pitch() {
|
|
13
|
+
if (!hasCombinedQuery(this.resourceQuery)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const request = buildProxyRequest(this);
|
|
17
|
+
const cssOptions = resolveCssOptions(this);
|
|
18
|
+
return extractCss(this, cssOptions).then(css => createCombinedModule(request, css));
|
|
19
|
+
};
|
|
20
|
+
loader.pitch = pitch;
|
|
23
21
|
export default loader;
|
|
22
|
+
function resolveCssOptions(ctx) {
|
|
23
|
+
const rawOptions = (typeof ctx.getOptions === 'function' ? ctx.getOptions() : {});
|
|
24
|
+
return {
|
|
25
|
+
...rawOptions,
|
|
26
|
+
cwd: rawOptions.cwd ?? ctx.rootContext ?? process.cwd(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function extractCss(ctx, options) {
|
|
30
|
+
const { css, files } = await cssWithMeta(ctx.resourcePath, options);
|
|
31
|
+
const uniqueFiles = new Set([ctx.resourcePath, ...files]);
|
|
32
|
+
for (const file of uniqueFiles) {
|
|
33
|
+
ctx.addDependency(file);
|
|
34
|
+
}
|
|
35
|
+
return css;
|
|
36
|
+
}
|
|
37
|
+
function toSourceString(source) {
|
|
38
|
+
return typeof source === 'string' ? source : source.toString('utf8');
|
|
39
|
+
}
|
|
40
|
+
function buildInjection(css) {
|
|
41
|
+
return `\n\nexport const ${DEFAULT_EXPORT_NAME} = ${JSON.stringify(css)};\n`;
|
|
42
|
+
}
|
|
43
|
+
function hasCombinedQuery(query) {
|
|
44
|
+
if (!query)
|
|
45
|
+
return false;
|
|
46
|
+
const trimmed = query.startsWith('?') ? query.slice(1) : query;
|
|
47
|
+
if (!trimmed)
|
|
48
|
+
return false;
|
|
49
|
+
return trimmed
|
|
50
|
+
.split('&')
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.some(part => isQueryFlag(part, COMBINED_QUERY_FLAG));
|
|
53
|
+
}
|
|
54
|
+
function buildProxyRequest(ctx) {
|
|
55
|
+
const sanitizedQuery = buildSanitizedQuery(ctx.resourceQuery);
|
|
56
|
+
const request = `${ctx.resourcePath}${sanitizedQuery}`;
|
|
57
|
+
const context = ctx.context ?? ctx.rootContext ?? process.cwd();
|
|
58
|
+
if (ctx.utils && typeof ctx.utils.contextify === 'function') {
|
|
59
|
+
return ctx.utils.contextify(context, request);
|
|
60
|
+
}
|
|
61
|
+
return request;
|
|
62
|
+
}
|
|
63
|
+
function buildSanitizedQuery(query) {
|
|
64
|
+
if (!query)
|
|
65
|
+
return '';
|
|
66
|
+
const entries = splitQuery(query).filter(part => {
|
|
67
|
+
return !isQueryFlag(part, COMBINED_QUERY_FLAG) && !isQueryFlag(part, 'knighted-css');
|
|
68
|
+
});
|
|
69
|
+
return entries.length > 0 ? `?${entries.join('&')}` : '';
|
|
70
|
+
}
|
|
71
|
+
function splitQuery(query) {
|
|
72
|
+
const trimmed = query.startsWith('?') ? query.slice(1) : query;
|
|
73
|
+
if (!trimmed)
|
|
74
|
+
return [];
|
|
75
|
+
return trimmed.split('&').filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
function isQueryFlag(entry, flag) {
|
|
78
|
+
const [rawKey] = entry.split('=');
|
|
79
|
+
try {
|
|
80
|
+
return decodeURIComponent(rawKey) === flag;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return rawKey === flag;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function createCombinedModule(request, css) {
|
|
87
|
+
const requestLiteral = JSON.stringify(request);
|
|
88
|
+
const defaultExport = `const __knightedDefault =
|
|
89
|
+
typeof __knightedModule.default !== 'undefined'
|
|
90
|
+
? __knightedModule.default
|
|
91
|
+
: __knightedModule;`;
|
|
92
|
+
return [
|
|
93
|
+
`import * as __knightedModule from ${requestLiteral};`,
|
|
94
|
+
`export * from ${requestLiteral};`,
|
|
95
|
+
defaultExport,
|
|
96
|
+
'export default __knightedDefault;',
|
|
97
|
+
buildInjection(css),
|
|
98
|
+
].join('\n');
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knighted/css",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
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",
|