@menapo/vicss 0.0.0-stage → 1.1.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/LICENSE +21 -0
- package/README.md +662 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1311 -0
- package/dist/config-loader-1kWS0cRU.js +162 -0
- package/dist/config-loader-CG76KA50.js +1 -0
- package/dist/config.d.ts +821 -0
- package/dist/config.js +2690 -0
- package/dist/index.d.ts +1528 -0
- package/dist/index.js +442 -0
- package/dist/lint.d.ts +114 -0
- package/dist/lint.js +0 -0
- package/dist/pool-F3qb30Fy.js +3389 -0
- package/dist/print-B_mn7wBu.js +5127 -0
- package/dist/snapshot-DAJ8FLU4.js +149 -0
- package/dist/theme-zBcr5DHi.js +2460 -0
- package/dist/types-file-D6Iu4kzv.js +2481 -0
- package/dist/vite.d.ts +270 -0
- package/dist/vite.js +2264 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +113 -0
- package/package.json +66 -3
|
@@ -0,0 +1,2481 @@
|
|
|
1
|
+
import { $ as isPaletteAtom, A as PROPERTIES, C as globToRegExp, J as placeholdersIn, P as scaleNumberKind, Q as isAnyPalette, T as checkAtoms, W as isRelating, _t as VicssError, at as SCALE_NAMESPACES, bt as compareStrings, et as keepAtom, ft as isScaleName, ht as scaleTokens, it as SCALES, lt as cssVarName, nt as paletteTarget, ot as SPACING_STEP, pt as lookupToken, tt as paletteKeyOf, x as hasPrivateAccess, y as FLAG_RESETS, z as markerStates } from "./print-B_mn7wBu.js";
|
|
2
|
+
import { _ as isSfc, h as extractScripts, n as WorkerLost, u as resolvesAlike, x as SourceEdits } from "./pool-F3qb30Fy.js";
|
|
3
|
+
import { d as baseRule, f as checkBases, l as responsiveConditions, n as compileTheme, u as baseAtoms } from "./theme-zBcr5DHi.js";
|
|
4
|
+
import { t as SNAPSHOT_FILE } from "./snapshot-DAJ8FLU4.js";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { glob, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { gzipSync } from "node:zlib";
|
|
10
|
+
import { createFilter, normalizePath } from "vite";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
//#region src/core/frame.ts
|
|
13
|
+
/**
|
|
14
|
+
* Code frames for diagnostics (PLAN §5.4, §5.14): the line an error points
|
|
15
|
+
* at, with the line before and after it, and a caret under the column.
|
|
16
|
+
* Three lines keep a frame readable in a terminal that lists every error
|
|
17
|
+
* of a project (`vicss check`) as well as in Vite's overlay.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* `line` is 1-based and `column` 0-based, as in `SourceLocation`. Lines end
|
|
21
|
+
* at `\n`, as `createLocator` counts them; a trailing `\r` is not shown.
|
|
22
|
+
* Undefined when `line` is outside `code`.
|
|
23
|
+
*/
|
|
24
|
+
function codeFrame(code, line, column) {
|
|
25
|
+
const lines = code.split("\n").map((text) => text.replace(/\r$/, ""));
|
|
26
|
+
const target = lines[line - 1];
|
|
27
|
+
if (target === void 0) return void 0;
|
|
28
|
+
const first = Math.max(1, line - 1);
|
|
29
|
+
const last = Math.min(lines.length, line + 1);
|
|
30
|
+
const width = String(last).length;
|
|
31
|
+
const out = [];
|
|
32
|
+
for (let n = first; n <= last; n++) {
|
|
33
|
+
const text = lines[n - 1];
|
|
34
|
+
out.push(`${n === line ? ">" : " "} ${String(n).padStart(width)} |${text ? ` ${text}` : ""}`);
|
|
35
|
+
if (n === line) {
|
|
36
|
+
const at = Math.max(0, Math.min(column, target.length));
|
|
37
|
+
const pad = target.slice(0, at).replace(/[^\t]/g, " ");
|
|
38
|
+
out.push(` ${" ".repeat(width)} | ${pad}^`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out.join("\n");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The manifest of a library's styles, compiled with the library's own
|
|
45
|
+
* system; `classes` maps each identity its code names to the class it ships.
|
|
46
|
+
*/
|
|
47
|
+
function createManifest(input, system, pkg, classes = {}) {
|
|
48
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
49
|
+
const use = (refs) => {
|
|
50
|
+
for (const ref of refs) tokens.set(refKey(ref), {
|
|
51
|
+
scale: ref.scale,
|
|
52
|
+
path: ref.path
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const conditions = /* @__PURE__ */ new Set();
|
|
56
|
+
const scales = /* @__PURE__ */ new Set();
|
|
57
|
+
const palettes = /* @__PURE__ */ new Set();
|
|
58
|
+
const toManifest = (atom) => {
|
|
59
|
+
use(atom.tokens);
|
|
60
|
+
for (const name of atom.chain) conditions.add(name);
|
|
61
|
+
if (isPaletteAtom(atom) && !isAnyPalette(atom)) palettes.add(atom.value);
|
|
62
|
+
const { variable, scales: named } = atom;
|
|
63
|
+
for (const scale of named ?? []) scales.add(scale);
|
|
64
|
+
return {
|
|
65
|
+
className: atom.className,
|
|
66
|
+
key: atom.key,
|
|
67
|
+
tier: atom.tier,
|
|
68
|
+
chain: [...atom.chain],
|
|
69
|
+
property: atom.property,
|
|
70
|
+
value: toPlaceholders(atom.value, atom.tokens, system),
|
|
71
|
+
tokens: atom.tokens.map(plainRef),
|
|
72
|
+
...typeof variable === "string" ? { variable } : {},
|
|
73
|
+
...named && named.length > 0 ? { scales: [...named] } : {},
|
|
74
|
+
...isAnyPalette(atom) ? { anyPalette: true } : {},
|
|
75
|
+
...atom.systemTokens ? { systemTokens: atom.systemTokens.map(plainRef) } : {}
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
const atoms = [...input.atoms].sort((a, b) => compareStrings(a.className, b.className)).map(toManifest);
|
|
79
|
+
const rules = [...input.rules].sort((a, b) => compareStrings(a.id, b.id)).map((rule) => {
|
|
80
|
+
use(rule.tokens);
|
|
81
|
+
if (rule.kind === "base") return {
|
|
82
|
+
id: rule.id,
|
|
83
|
+
kind: "base",
|
|
84
|
+
atoms: (rule.atoms ?? []).map(toManifest),
|
|
85
|
+
tokens: rule.tokens.map(plainRef)
|
|
86
|
+
};
|
|
87
|
+
if (rule.kind === "keyframes") return {
|
|
88
|
+
id: rule.id,
|
|
89
|
+
kind: "keyframes",
|
|
90
|
+
css: toPlaceholders(rule.css, rule.tokens, system),
|
|
91
|
+
tokens: rule.tokens.map(plainRef)
|
|
92
|
+
};
|
|
93
|
+
if (rule.overrides === void 0) throw new VicssError("BAD_MANIFEST", `the theme ${rule.id} carries no overrides to write into the manifest`);
|
|
94
|
+
return {
|
|
95
|
+
id: rule.id,
|
|
96
|
+
kind: "theme",
|
|
97
|
+
overrides: rule.overrides,
|
|
98
|
+
tokens: rule.tokens.map(plainRef)
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
use(input.tokens);
|
|
102
|
+
return {
|
|
103
|
+
format: 4,
|
|
104
|
+
package: pkg,
|
|
105
|
+
cssVarPrefix: system.cssVarPrefix,
|
|
106
|
+
atoms,
|
|
107
|
+
rules,
|
|
108
|
+
tokens: [...tokens.entries()].sort(([a], [b]) => compareStrings(a, b)).map(([, ref]) => ref),
|
|
109
|
+
scales: [...scales].sort(compareStrings),
|
|
110
|
+
conditions: [...conditions].sort(compareStrings),
|
|
111
|
+
palettes: [...palettes].sort(compareStrings),
|
|
112
|
+
classes: Object.fromEntries(Object.entries(classes).sort(([a], [b]) => compareStrings(a, b)))
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The library's styles for the app's system. Every token, condition and
|
|
117
|
+
* palette the library uses must exist in the app, and its styles may name
|
|
118
|
+
* none of the app's private tokens: a breach is an error that names the
|
|
119
|
+
* library and everything at fault.
|
|
120
|
+
*/
|
|
121
|
+
function applyManifest(manifest, system, library, options = {}) {
|
|
122
|
+
const missingTokens = manifest.tokens.filter((ref) => !lookupToken(system, ref.scale, ref.path)).map((ref) => `${ref.scale}.${ref.path}`);
|
|
123
|
+
const keys = manifest.tokens.filter((ref) => ref.scale === "colors" && lookupToken(system, ref.scale, ref.path)?.palette).map((ref) => paletteKeyOf(ref.path));
|
|
124
|
+
const groups = system.palettes?.groups;
|
|
125
|
+
const missingPalettes = manifest.palettes.filter((name) => !groups?.has(name));
|
|
126
|
+
const open = manifest.atoms.filter((atom) => atom.anyPalette);
|
|
127
|
+
if (!groups && open.length > 0 && missingPalettes.length === 0) missingPalettes.push("(any, chosen at runtime)");
|
|
128
|
+
if (system.palettes) {
|
|
129
|
+
const selected = open.length > 0 ? groups.keys() : manifest.palettes;
|
|
130
|
+
for (const name of /* @__PURE__ */ new Set([system.palettes.default, ...selected])) {
|
|
131
|
+
const has = groups.get(name);
|
|
132
|
+
if (!has) continue;
|
|
133
|
+
for (const key of keys) if (!has.has(key)) missingTokens.push(`colors.${paletteTarget(name, key)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const styles = [...manifest.atoms, ...manifest.rules.flatMap((rule) => rule.kind === "base" ? rule.atoms : [])];
|
|
137
|
+
const ruled = /* @__PURE__ */ new Set([...manifest.rules.flatMap((rule) => rule.kind === "base" ? [] : rule.tokens.map(refKey)), ...styles.flatMap((atom) => (atom.systemTokens ?? []).map(refKey))]);
|
|
138
|
+
const atomic = new Set(styles.flatMap((atom) => {
|
|
139
|
+
const written = new Set((atom.systemTokens ?? []).map(refKey));
|
|
140
|
+
return atom.tokens.map(refKey).filter((key) => !written.has(key));
|
|
141
|
+
}));
|
|
142
|
+
const hidden = options.privateAccess ? [] : manifest.tokens.filter((ref) => atomic.has(refKey(ref)) || !ruled.has(refKey(ref))).filter((ref) => lookupToken(system, ref.scale, ref.path)?.private).map((ref) => `${ref.scale}.${ref.path}`);
|
|
143
|
+
const missingConditions = manifest.conditions.filter((name) => !system.conditions.has(name)).map((name) => `_${name}`);
|
|
144
|
+
const missingScales = manifest.scales.filter((scale) => scaleTokens(system, scale).length === 0);
|
|
145
|
+
if (missingTokens.length > 0 || missingConditions.length > 0 || missingScales.length > 0 || missingPalettes.length > 0) {
|
|
146
|
+
const parts = [
|
|
147
|
+
missingTokens.length > 0 ? `tokens ${[...new Set(missingTokens)].join(", ")}` : "",
|
|
148
|
+
missingScales.length > 0 ? `tokens of the scales ${missingScales.join(", ")}` : "",
|
|
149
|
+
missingConditions.length > 0 ? `conditions ${missingConditions.join(", ")}` : "",
|
|
150
|
+
missingPalettes.length > 0 ? `palettes ${missingPalettes.join(", ")}` : ""
|
|
151
|
+
].filter(Boolean);
|
|
152
|
+
const palette = keys.length > 0 || missingPalettes.length > 0 || missingTokens.some((t) => t.startsWith(`colors.palette`));
|
|
153
|
+
const any = open.length > 0 && groups ? " Its colorPalette takes any of the app's palettes at runtime: narrow palettes.names to the groups that have them." : "";
|
|
154
|
+
throw new VicssError("LIBRARY_CONTRACT", `${library} uses ${parts.join(" and ")}, which the app's design system lacks`, { hint: `Add them to vicss.config.ts (the library's preset lists what it uses), or use a version of the library that matches the design system.${palette ? " palette.* keys need palettes (palettes in the config) whose groups have them." : ""}${any}` });
|
|
155
|
+
}
|
|
156
|
+
if (hidden.length > 0) throw new VicssError("LIBRARY_CONTRACT", `${library} names ${hidden.join(", ")}, which the app's design system keeps private`, { hint: `A library styles with the app's public tokens. Make these public (tokenVisibility), let the library name them (privateAccess: ["node_modules/${library}/**"]), or use a version of the library built on the semantic tokens.` });
|
|
157
|
+
const fromManifest = (atom) => {
|
|
158
|
+
const out = {
|
|
159
|
+
className: atom.className,
|
|
160
|
+
key: atom.key,
|
|
161
|
+
tier: atom.tier,
|
|
162
|
+
chain: [...atom.chain],
|
|
163
|
+
property: atom.property,
|
|
164
|
+
value: fromPlaceholders(atom.value, atom.tokens, system),
|
|
165
|
+
tokens: atom.tokens
|
|
166
|
+
};
|
|
167
|
+
if (atom.variable !== void 0) out.variable = atom.variable;
|
|
168
|
+
if (atom.scales !== void 0) out.scales = atom.scales;
|
|
169
|
+
return out;
|
|
170
|
+
};
|
|
171
|
+
const atoms = manifest.atoms.filter((atom) => !atom.anyPalette).map(fromManifest);
|
|
172
|
+
const classes = new Set(atoms.map((atom) => atom.className));
|
|
173
|
+
for (const atom of open) {
|
|
174
|
+
const prefix = atom.className.slice(0, -atom.value.length);
|
|
175
|
+
for (const name of groups?.keys() ?? []) {
|
|
176
|
+
if (classes.has(prefix + name)) continue;
|
|
177
|
+
classes.add(prefix + name);
|
|
178
|
+
atoms.push({
|
|
179
|
+
className: prefix + name,
|
|
180
|
+
key: atom.key,
|
|
181
|
+
tier: atom.tier,
|
|
182
|
+
chain: [...atom.chain],
|
|
183
|
+
property: atom.property,
|
|
184
|
+
value: name,
|
|
185
|
+
tokens: []
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
atoms.sort((a, b) => compareStrings(a.className, b.className));
|
|
190
|
+
return {
|
|
191
|
+
atoms,
|
|
192
|
+
rules: manifest.rules.map((rule) => {
|
|
193
|
+
if (rule.kind === "base") return baseRule(rule.id, rule.atoms.map(fromManifest), system);
|
|
194
|
+
if (rule.kind === "keyframes") return {
|
|
195
|
+
id: rule.id,
|
|
196
|
+
kind: "keyframes",
|
|
197
|
+
css: fromPlaceholders(rule.css, rule.tokens, system),
|
|
198
|
+
tokens: rule.tokens
|
|
199
|
+
};
|
|
200
|
+
let compiled;
|
|
201
|
+
try {
|
|
202
|
+
compiled = compileTheme(rule.overrides, system);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (!(error instanceof VicssError)) throw error;
|
|
205
|
+
throw new VicssError(error.code, `${library}: the theme ${rule.id}: ${error.message}`, error.hint === void 0 ? {} : { hint: error.hint });
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
...compiled,
|
|
209
|
+
id: rule.id,
|
|
210
|
+
css: compiled.css.replaceAll(compiled.id, rule.id)
|
|
211
|
+
};
|
|
212
|
+
}),
|
|
213
|
+
tokens: manifest.tokens,
|
|
214
|
+
classes: manifest.classes
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function customPropertyRenames(manifest, system) {
|
|
218
|
+
const from = manifest.cssVarPrefix;
|
|
219
|
+
if (from === system.cssVarPrefix) return void 0;
|
|
220
|
+
const exact = /* @__PURE__ */ new Map();
|
|
221
|
+
for (const ref of manifest.tokens) {
|
|
222
|
+
const token = lookupToken(system, ref.scale, ref.path);
|
|
223
|
+
if (token) exact.set(`var(${cssVarName(from, ref.scale, ref.path)})`, `var(${token.cssVar})`);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
exact,
|
|
227
|
+
prefixes: new Map(manifest.scales.map((scale) => [`var(--${from}${SCALE_NAMESPACES[scale]}-`, `var(--${system.cssVarPrefix}${SCALE_NAMESPACES[scale]}-`]))
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* A manifest read from disk, checked field by field: anything unexpected is
|
|
232
|
+
* an error naming the library, rather than CSS printed from half a file.
|
|
233
|
+
*/
|
|
234
|
+
function parseManifest(data, library) {
|
|
235
|
+
const fail = (why) => {
|
|
236
|
+
throw new VicssError("BAD_MANIFEST", `${library}'s vicss manifest is not valid: ${why}`, { hint: "Build the library again with the same version of @menapo/vicss as the app." });
|
|
237
|
+
};
|
|
238
|
+
if (!isRecord$1(data)) return fail("expected an object");
|
|
239
|
+
if (data.format !== 4) return fail(`format ${JSON.stringify(data.format)}, while this vicss reads format 4`);
|
|
240
|
+
if (typeof data.package !== "string") return fail("`package` is not a string");
|
|
241
|
+
if (typeof data.cssVarPrefix !== "string") return fail("`cssVarPrefix` is not a string");
|
|
242
|
+
if (!isScales(data.scales)) return fail("`scales` is not a list of scale names");
|
|
243
|
+
if (!Array.isArray(data.atoms) || !data.atoms.every(isManifestAtom)) return fail("`atoms` is not a list of atoms");
|
|
244
|
+
if (!Array.isArray(data.rules) || !data.rules.every(isManifestRule)) return fail("`rules` is not a list of keyframes, themes and flattened bases");
|
|
245
|
+
if (!isRefs(data.tokens)) return fail("`tokens` is not a list of token references");
|
|
246
|
+
if (!Array.isArray(data.conditions) || !data.conditions.every((c) => typeof c === "string")) return fail("`conditions` is not a list of names");
|
|
247
|
+
if (!Array.isArray(data.palettes) || !data.palettes.every((p) => typeof p === "string")) return fail("`palettes` is not a list of names");
|
|
248
|
+
if (!isRecord$1(data.classes) || !Object.values(data.classes).every((c) => typeof c === "string")) return fail("`classes` is not an object of class names");
|
|
249
|
+
return data;
|
|
250
|
+
}
|
|
251
|
+
/** `var(--color-accent)` → `{colors.accent}` for each token `refs` names. */
|
|
252
|
+
function toPlaceholders(text, refs, system) {
|
|
253
|
+
let out = text;
|
|
254
|
+
for (const ref of refs) {
|
|
255
|
+
const token = lookupToken(system, ref.scale, ref.path);
|
|
256
|
+
if (token) out = out.replaceAll(`var(${token.cssVar})`, `{${ref.scale}.${ref.path}}`);
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
/** `{colors.accent}` → the app's `var(--…)` for each token `refs` names; other braces stay. */
|
|
261
|
+
function fromPlaceholders(text, refs, system) {
|
|
262
|
+
let out = text;
|
|
263
|
+
for (const ref of refs) {
|
|
264
|
+
const token = lookupToken(system, ref.scale, ref.path);
|
|
265
|
+
out = out.replaceAll(`{${ref.scale}.${ref.path}}`, `var(${token.cssVar})`);
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
const refKey = (ref) => `${ref.scale}:${ref.path}`;
|
|
270
|
+
const plainRef = (ref) => ({
|
|
271
|
+
scale: ref.scale,
|
|
272
|
+
path: ref.path
|
|
273
|
+
});
|
|
274
|
+
function isRecord$1(value) {
|
|
275
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
276
|
+
}
|
|
277
|
+
function isRefs(value) {
|
|
278
|
+
return Array.isArray(value) && value.every((ref) => isRecord$1(ref) && typeof ref.scale === "string" && isScaleName(ref.scale) && typeof ref.path === "string");
|
|
279
|
+
}
|
|
280
|
+
function isScales(value) {
|
|
281
|
+
return Array.isArray(value) && value.every((scale) => typeof scale === "string" && isScaleName(scale));
|
|
282
|
+
}
|
|
283
|
+
function isManifestAtom(value) {
|
|
284
|
+
return isRecord$1(value) && typeof value.className === "string" && typeof value.key === "string" && typeof value.tier === "number" && Array.isArray(value.chain) && value.chain.every((name) => typeof name === "string") && typeof value.property === "string" && typeof value.value === "string" && isRefs(value.tokens) && (value.variable === void 0 || typeof value.variable === "string") && (value.scales === void 0 || isScales(value.scales)) && (value.anyPalette === void 0 || value.anyPalette === true) && (value.systemTokens === void 0 || isRefs(value.systemTokens));
|
|
285
|
+
}
|
|
286
|
+
function isManifestRule(value) {
|
|
287
|
+
if (!isRecord$1(value) || typeof value.id !== "string" || !isRefs(value.tokens)) return false;
|
|
288
|
+
if (value.kind === "keyframes") return typeof value.css === "string";
|
|
289
|
+
if (value.kind === "base") return Array.isArray(value.atoms) && value.atoms.every(isManifestAtom);
|
|
290
|
+
return value.kind === "theme" && value.overrides !== void 0;
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/core/types-gen.ts
|
|
294
|
+
/**
|
|
295
|
+
* The generated `vicss.d.ts` (PLAN §5.11, D7): the design system as flat
|
|
296
|
+
* literal unions that augment `Register` in `@menapo/vicss`, so style objects
|
|
297
|
+
* type-check against the project's tokens, conditions, text styles,
|
|
298
|
+
* utilities and strictness, recipes' responsive props against the
|
|
299
|
+
* breakpoints and containers, markers against the states the config
|
|
300
|
+
* gives an element, and `sx()`'s result against the project's framework
|
|
301
|
+
* (§5.12). One union per scale and no derived types keep the checker's
|
|
302
|
+
* work proportional to the tokens a file uses.
|
|
303
|
+
*/
|
|
304
|
+
/** The line width the members of a union are wrapped at, as a formatter would. */
|
|
305
|
+
const WIDTH = 100;
|
|
306
|
+
/** The declaration text of `vicss.d.ts` for `system`. */
|
|
307
|
+
function generateTypes(system, options = {}) {
|
|
308
|
+
const tokens = SCALES.map((scale) => {
|
|
309
|
+
const paths = [...system.tokens.get(scale)?.values() ?? []].filter((token) => !token.private).map((token) => token.path).filter((path) => !(scale === "spacing" && path === "step")).sort(compareStrings);
|
|
310
|
+
const members = paths.map((path) => JSON.stringify(path));
|
|
311
|
+
members.push(...paths.filter((path) => Number.isFinite(Number(path)) && String(Number(path)) === path));
|
|
312
|
+
if (scale === "spacing" && system.spacingStep !== void 0) members.unshift("number");
|
|
313
|
+
return union(`${scale}:`, members, " ");
|
|
314
|
+
});
|
|
315
|
+
const conditions = [...system.conditions.keys()].sort(compareStrings).map((name) => JSON.stringify(`_${name}`));
|
|
316
|
+
const textStyles = [...system.textStyles.keys()].sort(compareStrings).map((name) => JSON.stringify(name));
|
|
317
|
+
const utilities = [...system.utilities].sort(([a], [b]) => compareStrings(a, b)).map(([name, def]) => {
|
|
318
|
+
return ` ${name}: ${def.boolean ? "boolean" : def.scale ? JSON.stringify(def.scale) : "unknown"};`;
|
|
319
|
+
});
|
|
320
|
+
const setLines = [...setters$1(system)].sort(([a], [b]) => compareStrings(a, b)).map(([name, set]) => {
|
|
321
|
+
const members = set.has("*") ? ["string"] : [...set].sort(compareStrings).map((property) => JSON.stringify(property));
|
|
322
|
+
return union(`${name}:`, members, " ");
|
|
323
|
+
});
|
|
324
|
+
const names = [...[...PROPERTIES.values()].map((info) => [info.name, info.scale]), ...[...system.utilities].map(([name, def]) => [name, def.scale])];
|
|
325
|
+
const strict = SCALES.map((scale) => {
|
|
326
|
+
const level = system.strictness(scale);
|
|
327
|
+
const open = names.some(([name, of]) => of === scale && system.strictness(scale, name) !== "tokens");
|
|
328
|
+
return ` ${scale}: ${JSON.stringify(level === "tokens" && open ? "css" : level)};`;
|
|
329
|
+
});
|
|
330
|
+
const responsive = responsiveConditions(system).props.map((condition) => JSON.stringify(`_${condition.name}`)).sort(compareStrings);
|
|
331
|
+
const states = markerStates(system).sort(compareStrings).map((name) => JSON.stringify(name));
|
|
332
|
+
const palettes = system.palettes ? [union("palettes:", [...system.palettes.groups.keys()].sort(compareStrings).map((n) => JSON.stringify(n)), " ")] : [];
|
|
333
|
+
return `${[
|
|
334
|
+
"// Generated by vicss from the design system in vicss.config.ts. Do not edit:",
|
|
335
|
+
"// the Vite plugin rewrites it whenever the config loads, and `vicss types` in CI.",
|
|
336
|
+
"import \"@menapo/vicss\";",
|
|
337
|
+
"",
|
|
338
|
+
"declare module \"@menapo/vicss\" {",
|
|
339
|
+
" interface Register {",
|
|
340
|
+
" tokens: {",
|
|
341
|
+
...tokens,
|
|
342
|
+
" };",
|
|
343
|
+
union("conditions:", conditions, " "),
|
|
344
|
+
union("textStyles:", textStyles, " "),
|
|
345
|
+
" utilities: {",
|
|
346
|
+
...utilities,
|
|
347
|
+
" };",
|
|
348
|
+
" sets: {",
|
|
349
|
+
...setLines,
|
|
350
|
+
" };",
|
|
351
|
+
" strict: {",
|
|
352
|
+
...strict,
|
|
353
|
+
" };",
|
|
354
|
+
union("responsive:", responsive, " "),
|
|
355
|
+
union("markerStates:", states, " "),
|
|
356
|
+
...palettes,
|
|
357
|
+
...options.framework ? [` framework: ${JSON.stringify(options.framework)};`] : [],
|
|
358
|
+
" }",
|
|
359
|
+
"}",
|
|
360
|
+
"",
|
|
361
|
+
"export {};"
|
|
362
|
+
].join("\n")}\n`;
|
|
363
|
+
}
|
|
364
|
+
/** `head` and a union of `members` with `;`: one line when it fits, else one member per line. */
|
|
365
|
+
function union(head, members, indent) {
|
|
366
|
+
if (members.length === 0) return `${indent}${head} never;`;
|
|
367
|
+
const line = `${indent}${head} ${members.join(" | ")};`;
|
|
368
|
+
if (line.length <= WIDTH) return line;
|
|
369
|
+
return `${[`${indent}${head}`, ...members.map((member) => `${indent} | ${member}`)].join("\n")};`;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* The style keys each utility and `textStyle` set, which `Style<K>` records
|
|
373
|
+
* in their place (`p` sets `padding`), so `Style<"margin">` takes `css({ m: 4 })`.
|
|
374
|
+
* A utility's transform is a function of its value: it runs here on sample
|
|
375
|
+
* values (a token of its scale, a number, a flag), and what any of them sets
|
|
376
|
+
* counts. `"*"` stands for a utility none of them could run: any property.
|
|
377
|
+
*/
|
|
378
|
+
function setters$1(system) {
|
|
379
|
+
const out = /* @__PURE__ */ new Map();
|
|
380
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
381
|
+
const keysOf = (style, into) => {
|
|
382
|
+
if (typeof style !== "object" || style === null) return;
|
|
383
|
+
for (const [key, value] of Object.entries(style)) {
|
|
384
|
+
if (key.startsWith("_") || key.startsWith("__vx:")) {
|
|
385
|
+
if (system.conditions.has(key.slice(1)) || key.startsWith("__vx:")) keysOf(value, into);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const nested = key === "textStyle" || system.utilities.has(key) ? setOf(key) : void 0;
|
|
389
|
+
for (const property of nested ?? [key]) into.add(property);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
const setOf = (name) => {
|
|
393
|
+
const known = out.get(name);
|
|
394
|
+
if (known) return known;
|
|
395
|
+
if (visiting.has(name)) return /* @__PURE__ */ new Set();
|
|
396
|
+
visiting.add(name);
|
|
397
|
+
const set = /* @__PURE__ */ new Set();
|
|
398
|
+
if (name === "textStyle") for (const style of system.textStyles.values()) keysOf(style, set);
|
|
399
|
+
else {
|
|
400
|
+
const def = system.utilities.get(name);
|
|
401
|
+
let ran = false;
|
|
402
|
+
for (const sample of samples(system, def)) try {
|
|
403
|
+
keysOf(def.transform(sample), set);
|
|
404
|
+
ran = true;
|
|
405
|
+
} catch {}
|
|
406
|
+
keysOf(FLAG_RESETS.get(def), set);
|
|
407
|
+
if (!ran) set.add("*");
|
|
408
|
+
}
|
|
409
|
+
visiting.delete(name);
|
|
410
|
+
out.set(name, set);
|
|
411
|
+
return set;
|
|
412
|
+
};
|
|
413
|
+
for (const name of system.utilities.keys()) setOf(name);
|
|
414
|
+
setOf("textStyle");
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
function samples(system, def) {
|
|
418
|
+
if (def.boolean) return [true, false];
|
|
419
|
+
if (!def.scale) return [
|
|
420
|
+
"auto",
|
|
421
|
+
1,
|
|
422
|
+
true
|
|
423
|
+
];
|
|
424
|
+
const token = [...system.tokens.get(def.scale)?.keys() ?? []].find((path) => path !== SPACING_STEP);
|
|
425
|
+
const number = scaleNumberKind(def.scale) === void 0 ? [] : [1];
|
|
426
|
+
return [
|
|
427
|
+
...token === void 0 ? [] : [token],
|
|
428
|
+
...number,
|
|
429
|
+
"auto"
|
|
430
|
+
];
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* The installed vicss's version is part of every key: an upgrade can compile
|
|
434
|
+
* the same input to other atoms without anyone bumping FORMAT_VERSION.
|
|
435
|
+
*/
|
|
436
|
+
const VERSION = (() => {
|
|
437
|
+
try {
|
|
438
|
+
return createRequire(import.meta.url)("@menapo/vicss/package.json").version;
|
|
439
|
+
} catch {
|
|
440
|
+
return "";
|
|
441
|
+
}
|
|
442
|
+
})();
|
|
443
|
+
function toOutput(result) {
|
|
444
|
+
const { changed, code, map, warnings } = result;
|
|
445
|
+
return {
|
|
446
|
+
code: changed ? code : null,
|
|
447
|
+
map: map && {
|
|
448
|
+
names: map.names,
|
|
449
|
+
mappings: map.mappings
|
|
450
|
+
},
|
|
451
|
+
warnings
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* The compile result an entry stands for, for the module `id` whose code is
|
|
456
|
+
* `code`; without `own`, which only dev's check of framework files reads.
|
|
457
|
+
*/
|
|
458
|
+
function compiledOf(entry, id, code) {
|
|
459
|
+
const { output } = entry;
|
|
460
|
+
const names = entry.classes.filter((cls) => cls.startsWith("!")).map((cls) => cls.slice(1));
|
|
461
|
+
return {
|
|
462
|
+
code: output.code ?? code,
|
|
463
|
+
map: output.map && {
|
|
464
|
+
version: 3,
|
|
465
|
+
file: id,
|
|
466
|
+
sources: [id],
|
|
467
|
+
sourcesContent: [code],
|
|
468
|
+
...output.map
|
|
469
|
+
},
|
|
470
|
+
atoms: [...entry.atoms],
|
|
471
|
+
tokens: [...entry.tokens],
|
|
472
|
+
rules: [...entry.rules],
|
|
473
|
+
arbitrary: entry.arbitrary,
|
|
474
|
+
...names.length > 0 && { names },
|
|
475
|
+
dependencies: Object.keys(entry.deps),
|
|
476
|
+
changed: output.code !== null,
|
|
477
|
+
resolutions: entry.resolutions,
|
|
478
|
+
warnings: output.warnings
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function hashCode(code) {
|
|
482
|
+
return createHash("sha1").update(code).digest("base64url");
|
|
483
|
+
}
|
|
484
|
+
var CompileCache = class {
|
|
485
|
+
#entries = /* @__PURE__ */ new Map();
|
|
486
|
+
/** `configHash` covers the design system and the files that define it. */
|
|
487
|
+
key(code, configHash) {
|
|
488
|
+
return `${hashCode(code)}:${configHash}:14:${VERSION}`;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The entry for `id` if it is still valid. `depHash` reads a dependency's
|
|
492
|
+
* current hash (undefined when it is gone) and `resolve` what an import
|
|
493
|
+
* resolves to now; a scan memoises both, since a design system's modules
|
|
494
|
+
* are dependencies of most files.
|
|
495
|
+
*/
|
|
496
|
+
async lookup(id, key, depHash, resolve) {
|
|
497
|
+
const entry = this.#entries.get(id);
|
|
498
|
+
if (entry?.key !== key) return void 0;
|
|
499
|
+
for (const [dep, hash] of Object.entries(entry.deps)) if (await depHash(dep) !== hash) return void 0;
|
|
500
|
+
return await resolvesAlike(entry.resolutions, resolve) ? entry : void 0;
|
|
501
|
+
}
|
|
502
|
+
set(id, entry) {
|
|
503
|
+
this.#entries.set(id, entry);
|
|
504
|
+
}
|
|
505
|
+
clear() {
|
|
506
|
+
this.#entries.clear();
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Reads `<cacheDir>/vicss/scan.json`. Paths are stored relative to the root,
|
|
510
|
+
* so a project that moves keeps its cache. Anything unexpected — a missing,
|
|
511
|
+
* corrupt or foreign file — leaves the cache as it is.
|
|
512
|
+
*/
|
|
513
|
+
async read(file, root) {
|
|
514
|
+
let data;
|
|
515
|
+
try {
|
|
516
|
+
data = JSON.parse(await readFile(file, "utf8"));
|
|
517
|
+
} catch {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!isRecord(data) || data.format !== 14 || !isRecord(data.entries)) return;
|
|
521
|
+
const table = data.atoms;
|
|
522
|
+
const rules = data.rules;
|
|
523
|
+
if (!Array.isArray(table) || !table.every(isAtom)) return;
|
|
524
|
+
if (!Array.isArray(rules) || !rules.every(isRule)) return;
|
|
525
|
+
for (const [rel, entry] of Object.entries(data.entries)) {
|
|
526
|
+
if (!isStored(entry, table.length, rules.length)) continue;
|
|
527
|
+
const deps = {};
|
|
528
|
+
for (const [dep, hash] of Object.entries(entry.deps)) deps[absolute(root, dep)] = hash;
|
|
529
|
+
const resolutions = mapResolutions(entry.resolutions, (p) => absolute(root, p));
|
|
530
|
+
this.#entries.set(absolute(root, rel), {
|
|
531
|
+
...entry,
|
|
532
|
+
deps,
|
|
533
|
+
resolutions,
|
|
534
|
+
atoms: entry.atoms.map((index) => table[index]),
|
|
535
|
+
rules: entry.rules.map((index) => rules[index])
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Writes the entries of `ids` (one scan's files), so deleted files drop out.
|
|
541
|
+
* Most atoms are shared by many files, and a theme by every module that
|
|
542
|
+
* applies it: each is written once, in a table the entries index, which
|
|
543
|
+
* keeps the file a fraction of their size.
|
|
544
|
+
*/
|
|
545
|
+
async write(file, root, ids) {
|
|
546
|
+
const atoms = interner();
|
|
547
|
+
const rules = interner();
|
|
548
|
+
const entries = {};
|
|
549
|
+
for (const id of ids) {
|
|
550
|
+
const entry = this.#entries.get(id);
|
|
551
|
+
if (!entry) continue;
|
|
552
|
+
const deps = {};
|
|
553
|
+
for (const [dep, hash] of Object.entries(entry.deps)) deps[relative(root, dep)] = hash;
|
|
554
|
+
const resolutions = mapResolutions(entry.resolutions, (p) => relative(root, p));
|
|
555
|
+
entries[relative(root, id)] = {
|
|
556
|
+
...entry,
|
|
557
|
+
deps,
|
|
558
|
+
resolutions,
|
|
559
|
+
atoms: entry.atoms.map(atoms.intern),
|
|
560
|
+
rules: entry.rules.map(rules.intern)
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
564
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
565
|
+
const data = {
|
|
566
|
+
format: 14,
|
|
567
|
+
atoms: atoms.table,
|
|
568
|
+
rules: rules.table,
|
|
569
|
+
entries
|
|
570
|
+
};
|
|
571
|
+
await writeFile(temp, JSON.stringify(data));
|
|
572
|
+
await rename(temp, file);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
/**
|
|
576
|
+
* A table of values and their indices. By content, not by class name or id:
|
|
577
|
+
* an entry left from another config can hold an atom whose class name is
|
|
578
|
+
* the same and whose value is not.
|
|
579
|
+
*/
|
|
580
|
+
function interner() {
|
|
581
|
+
const table = [];
|
|
582
|
+
const indices = /* @__PURE__ */ new Map();
|
|
583
|
+
const intern = (value) => {
|
|
584
|
+
const json = JSON.stringify(value);
|
|
585
|
+
let index = indices.get(json);
|
|
586
|
+
if (index === void 0) indices.set(json, index = table.push(value) - 1);
|
|
587
|
+
return index;
|
|
588
|
+
};
|
|
589
|
+
return {
|
|
590
|
+
table,
|
|
591
|
+
intern
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
/** A dependency's current hash, or undefined when it cannot be read. */
|
|
595
|
+
function readHash(file) {
|
|
596
|
+
try {
|
|
597
|
+
return hashCode(readFileSync(file, "utf8"));
|
|
598
|
+
} catch {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const relative = (root, id) => path.relative(root, id).split(path.sep).join("/");
|
|
603
|
+
const absolute = (root, rel) => path.resolve(root, rel).split(path.sep).join("/");
|
|
604
|
+
/** Rewrites the paths of `importer\0source` → file; the specifier and "" stay. */
|
|
605
|
+
function mapResolutions(resolutions, map) {
|
|
606
|
+
const out = {};
|
|
607
|
+
for (const [request, file] of Object.entries(resolutions)) {
|
|
608
|
+
const at = request.indexOf("\0");
|
|
609
|
+
out[`${map(request.slice(0, at))}${request.slice(at)}`] = file && map(file);
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
function isRecord(value) {
|
|
614
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
615
|
+
}
|
|
616
|
+
const isIndices = (value, length) => Array.isArray(value) && value.every((index) => Number.isInteger(index) && index >= 0 && index < length);
|
|
617
|
+
const isTokenRefs = (value) => Array.isArray(value) && value.every((ref) => isRecord(ref) && typeof ref.scale === "string" && typeof ref.path === "string");
|
|
618
|
+
function isStored(value, atoms, rules) {
|
|
619
|
+
return isRecord(value) && typeof value.key === "string" && isRecord(value.deps) && Object.values(value.deps).every((hash) => typeof hash === "string") && isRecord(value.resolutions) && Object.entries(value.resolutions).every(([request, file]) => request.includes("\0") && typeof file === "string") && isIndices(value.atoms, atoms) && isIndices(value.rules, rules) && isTokenRefs(value.tokens) && typeof value.arbitrary === "number" && isStrings(value.classes) && isOutput(value.output);
|
|
620
|
+
}
|
|
621
|
+
function isOutput(value) {
|
|
622
|
+
if (!isRecord(value)) return false;
|
|
623
|
+
const { code, map, warnings } = value;
|
|
624
|
+
return (code === null || typeof code === "string") && (map === null || isRecord(map) && isStrings(map.names) && typeof map.mappings === "string") && isStrings(warnings);
|
|
625
|
+
}
|
|
626
|
+
const isStrings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
627
|
+
function isAtom(value) {
|
|
628
|
+
return isRecord(value) && typeof value.className === "string" && typeof value.key === "string" && typeof value.tier === "number" && Array.isArray(value.chain) && typeof value.property === "string" && typeof value.value === "string" && Array.isArray(value.tokens);
|
|
629
|
+
}
|
|
630
|
+
function isRule(value) {
|
|
631
|
+
return isRecord(value) && typeof value.id === "string" && (value.kind === "keyframes" || value.kind === "theme" || value.kind === "base" && Array.isArray(value.atoms) && value.atoms.every(isAtom)) && typeof value.css === "string" && isTokenRefs(value.tokens);
|
|
632
|
+
}
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/vite/critical.ts
|
|
635
|
+
/**
|
|
636
|
+
* Critical CSS for prerendered pages (PLAN D16, §5.10). Each stylesheet of a
|
|
637
|
+
* page that holds vicss's CSS becomes an inline `<style>` in its place,
|
|
638
|
+
* holding what the page's markup can use, and the stylesheet itself loads
|
|
639
|
+
* from the end of `<body>` (its link stays, disabled, where a framework
|
|
640
|
+
* hydrating the document expects it). A parser-created stylesheet in the
|
|
641
|
+
* body does not block the rendering of what precedes it, so the page paints
|
|
642
|
+
* with the inline CSS and the full sheet applies when it arrives, with no
|
|
643
|
+
* inline script: an `onload` handler would need `'unsafe-inline'` under a
|
|
644
|
+
* strict CSP.
|
|
645
|
+
*
|
|
646
|
+
* It works on built files only, the CSS Vite wrote (minified, its `url()`s
|
|
647
|
+
* resolved) and the page's HTML, so the build hook and `vicss critical`
|
|
648
|
+
* behave alike and need neither the config nor a scan. Class names are
|
|
649
|
+
* deterministic, so a page's atoms are the rules whose selectors need only
|
|
650
|
+
* classes its markup has. The inline CSS is the file with rules removed, in
|
|
651
|
+
* the same layers and order, so the full sheet's copies, arriving later,
|
|
652
|
+
* change no computed style (report 06 §3.2).
|
|
653
|
+
*/
|
|
654
|
+
const DEFAULT_BUDGET = 14e3;
|
|
655
|
+
const DEFAULT_HASHES_FILE = "vicss-critical.json";
|
|
656
|
+
/** The plugin's `critical` option, checked: what the build hook needs, or undefined when off. */
|
|
657
|
+
function criticalSettings(option) {
|
|
658
|
+
if (option === void 0 || option === false) return void 0;
|
|
659
|
+
const fail = (what) => {
|
|
660
|
+
throw new Error(`vicss: critical ${what}; use "prerendered" or { budget, hashes }`);
|
|
661
|
+
};
|
|
662
|
+
if (option === "prerendered") return {
|
|
663
|
+
budget: DEFAULT_BUDGET,
|
|
664
|
+
hashes: DEFAULT_HASHES_FILE
|
|
665
|
+
};
|
|
666
|
+
if (typeof option !== "object" || option === null || Array.isArray(option)) return fail(`must be "prerendered" or an object, not ${JSON.stringify(option)}`);
|
|
667
|
+
const { budget, hashes, ...rest } = option;
|
|
668
|
+
const unknown = Object.keys(rest);
|
|
669
|
+
if (unknown.length > 0) fail(`has no option ${unknown.join(", ")}`);
|
|
670
|
+
return {
|
|
671
|
+
budget: checkBudget(budget, fail),
|
|
672
|
+
hashes: checkHashes(hashes, fail)
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function checkBudget(budget, fail) {
|
|
676
|
+
if (budget === void 0) return DEFAULT_BUDGET;
|
|
677
|
+
if (typeof budget !== "number" || !Number.isFinite(budget) || budget <= 0) return fail(`budget must be a positive number of gzip bytes, not ${typeof budget === "number" ? String(budget) : JSON.stringify(budget)}`);
|
|
678
|
+
return budget;
|
|
679
|
+
}
|
|
680
|
+
function checkHashes(hashes, fail) {
|
|
681
|
+
if (hashes === void 0) return DEFAULT_HASHES_FILE;
|
|
682
|
+
if (hashes === false) return false;
|
|
683
|
+
if (typeof hashes !== "string") return fail(`hashes must be a path relative to the output directory, or false`);
|
|
684
|
+
const problem = hashesProblem(hashes);
|
|
685
|
+
if (problem !== void 0) fail(`hashes ${problem}`);
|
|
686
|
+
return path.posix.normalize(hashes.replaceAll("\\", "/"));
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* What is wrong with `file` as the hashes file, as the plugin's option and
|
|
690
|
+
* `vicss critical --hashes` say it: it must stay inside the output, which
|
|
691
|
+
* is deployed with the pages it describes.
|
|
692
|
+
*/
|
|
693
|
+
function hashesProblem(file) {
|
|
694
|
+
if (file === "" || path.isAbsolute(file)) return "must be a path relative to the output directory";
|
|
695
|
+
const normal = path.posix.normalize(file.replaceAll("\\", "/"));
|
|
696
|
+
if (normal === ".." || normal.startsWith("../")) return "must stay inside the output directory";
|
|
697
|
+
if (normal === "." || normal.endsWith("/")) return "must name a file inside the output directory";
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Inlines the critical CSS of every page under `dir`: the HTML a framework
|
|
701
|
+
* writes after `vite build` (Astro, SvelteKit, Vike, React Router
|
|
702
|
+
* prerendering), which no build hook sees. Pages are rewritten in place,
|
|
703
|
+
* a page processed before is left alone, and the hashes file is written.
|
|
704
|
+
*/
|
|
705
|
+
async function critical(dir, options = {}) {
|
|
706
|
+
const fail = (what) => {
|
|
707
|
+
throw new Error(`vicss: critical() ${what}`);
|
|
708
|
+
};
|
|
709
|
+
const budget = checkBudget(options.budget, fail);
|
|
710
|
+
const hashes = checkHashes(options.hashes, fail);
|
|
711
|
+
const root = path.resolve(dir);
|
|
712
|
+
if (!existsSync(root)) fail(`found no directory ${root}`);
|
|
713
|
+
const files = await listOutput(root);
|
|
714
|
+
const css = files.filter((file) => file.endsWith(".css"));
|
|
715
|
+
const sources = /* @__PURE__ */ new Map();
|
|
716
|
+
await Promise.all(css.map(async (file) => sources.set(file, await readFile(path.join(root, file), "utf8"))));
|
|
717
|
+
const sheets = new SheetCache((file) => sources.get(file));
|
|
718
|
+
const pages = [];
|
|
719
|
+
const html = files.filter((file) => file.endsWith(".html"));
|
|
720
|
+
for (const page of html) {
|
|
721
|
+
if (options.filter && !options.filter(page)) continue;
|
|
722
|
+
const result = criticalPage({
|
|
723
|
+
page,
|
|
724
|
+
html: await readFile(path.join(root, page), "utf8"),
|
|
725
|
+
files: css,
|
|
726
|
+
sheets
|
|
727
|
+
});
|
|
728
|
+
pages.push(result);
|
|
729
|
+
if (result.html !== void 0 && !options.dryRun) await writeFile(path.join(root, page), result.html);
|
|
730
|
+
}
|
|
731
|
+
let hashesFile;
|
|
732
|
+
if (hashes !== false && !options.dryRun && pages.some((page) => page.html !== void 0)) {
|
|
733
|
+
const file = path.join(root, hashes);
|
|
734
|
+
const earlier = await readFile(file, "utf8").then(parseHashesFile, () => void 0);
|
|
735
|
+
const kept = Object.entries(earlier?.pages ?? {}).filter(([page]) => html.includes(page));
|
|
736
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
737
|
+
await writeFile(file, hashesJson(pages, budget, Object.fromEntries(kept)));
|
|
738
|
+
hashesFile = hashes;
|
|
739
|
+
}
|
|
740
|
+
return report(pages, budget, hashesFile);
|
|
741
|
+
}
|
|
742
|
+
/** Every file under `root`, relative to it, sorted; `node_modules` and dot directories skipped. */
|
|
743
|
+
async function listOutput(root) {
|
|
744
|
+
return (await readdir(root, {
|
|
745
|
+
recursive: true,
|
|
746
|
+
withFileTypes: true
|
|
747
|
+
})).filter((entry) => entry.isFile()).map((entry) => path.relative(root, path.join(entry.parentPath, entry.name)).replaceAll("\\", "/")).filter((file) => !file.split("/").some((part) => part === "node_modules" || part.startsWith("."))).sort();
|
|
748
|
+
}
|
|
749
|
+
function parseHashesFile(text) {
|
|
750
|
+
try {
|
|
751
|
+
const value = JSON.parse(text);
|
|
752
|
+
return value.version === 1 && typeof value.pages === "object" ? value : void 0;
|
|
753
|
+
} catch {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* `{ version, budget, pages: { "index.html": { hashes, bytes, gzip } } }`,
|
|
759
|
+
* pages sorted; `earlier` holds the entries of pages an earlier run processed.
|
|
760
|
+
*/
|
|
761
|
+
function hashesJson(pages, budget, earlier = {}) {
|
|
762
|
+
const entries = { ...earlier };
|
|
763
|
+
for (const page of pages) if (page.html !== void 0) entries[page.page] = {
|
|
764
|
+
hashes: [...page.hashes],
|
|
765
|
+
bytes: page.bytes,
|
|
766
|
+
gzip: page.gzip
|
|
767
|
+
};
|
|
768
|
+
const file = {
|
|
769
|
+
version: 1,
|
|
770
|
+
budget,
|
|
771
|
+
pages: Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))
|
|
772
|
+
};
|
|
773
|
+
return `${JSON.stringify(file, null, 2)}\n`;
|
|
774
|
+
}
|
|
775
|
+
const kB = (bytes) => `${(bytes / 1e3).toFixed(2)} kB`;
|
|
776
|
+
const PART_NAMES = {
|
|
777
|
+
layers: "layer statements",
|
|
778
|
+
tokens: "tokens",
|
|
779
|
+
global: "global CSS and reset",
|
|
780
|
+
atoms: "atoms",
|
|
781
|
+
fonts: "@font-face",
|
|
782
|
+
keyframes: "keyframes",
|
|
783
|
+
other: "CSS bundled with vicss's"
|
|
784
|
+
};
|
|
785
|
+
function report(pages, budget, hashesFile) {
|
|
786
|
+
const inlined = pages.filter((page) => page.html !== void 0);
|
|
787
|
+
const warnings = [];
|
|
788
|
+
for (const page of pages) {
|
|
789
|
+
if (page.skipped !== void 0 && page.warning) warnings.push(`${page.page}: left render-blocking: ${page.skipped}`);
|
|
790
|
+
if (page.html !== void 0 && page.gzip > budget) {
|
|
791
|
+
const parts = Object.entries(page.parts).filter(([, bytes]) => bytes > 0).sort((a, b) => b[1] - a[1]).map(([part, bytes]) => `${PART_NAMES[part]} ${kB(bytes)}`);
|
|
792
|
+
warnings.push(`${page.page}: its critical CSS is ${kB(page.gzip)} gzip, over the ${kB(budget)} budget (raw: ${parts.join(", ")}). It is inlined all the same: the page still paints without waiting for a stylesheet, but its HTML takes more than the first round trip.`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const sizes = inlined.map((page) => page.gzip).sort((a, b) => a - b);
|
|
796
|
+
const range = sizes.length === 0 ? "" : sizes.length === 1 ? `, ${kB(sizes[0])} gzip` : `, ${kB(sizes[0])}–${kB(sizes.at(-1))} gzip (median ${kB(sizes[Math.floor(sizes.length / 2)])})`;
|
|
797
|
+
const over = inlined.filter((page) => page.gzip > budget).length;
|
|
798
|
+
const left = pages.length - inlined.length;
|
|
799
|
+
const summary = `vicss: critical CSS inlined in ${inlined.length} of ${pages.length} pages${range}` + (over > 0 ? `; ${over} over the ${kB(budget)} budget` : "") + (left > 0 ? `; ${left} left as they were` : "") + (hashesFile ? `; CSP hashes in ${hashesFile}` : "");
|
|
800
|
+
return {
|
|
801
|
+
pages,
|
|
802
|
+
budget,
|
|
803
|
+
...hashesFile ? { hashesFile } : {},
|
|
804
|
+
summary,
|
|
805
|
+
warnings
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const EMPTY_PARTS = {
|
|
809
|
+
layers: 0,
|
|
810
|
+
tokens: 0,
|
|
811
|
+
global: 0,
|
|
812
|
+
atoms: 0,
|
|
813
|
+
fonts: 0,
|
|
814
|
+
keyframes: 0,
|
|
815
|
+
other: 0
|
|
816
|
+
};
|
|
817
|
+
/** One page: its critical CSS, or why it keeps its stylesheets as they are. */
|
|
818
|
+
function criticalPage(input) {
|
|
819
|
+
const { page, html } = input;
|
|
820
|
+
const left = (skipped, warning = false) => ({
|
|
821
|
+
page,
|
|
822
|
+
skipped,
|
|
823
|
+
...warning ? { warning } : {},
|
|
824
|
+
hashes: [],
|
|
825
|
+
bytes: 0,
|
|
826
|
+
gzip: 0,
|
|
827
|
+
parts: { ...EMPTY_PARTS },
|
|
828
|
+
deferred: []
|
|
829
|
+
});
|
|
830
|
+
const scan = scanHtml(html);
|
|
831
|
+
if (scan.processed) return left("it has critical CSS already");
|
|
832
|
+
const elements = scan.css.map((element) => {
|
|
833
|
+
if (element.kind === "style") return {
|
|
834
|
+
element,
|
|
835
|
+
file: void 0,
|
|
836
|
+
sheet: void 0
|
|
837
|
+
};
|
|
838
|
+
const file = element.href === void 0 ? void 0 : resolveHref(element.href, page, input.files);
|
|
839
|
+
return {
|
|
840
|
+
element,
|
|
841
|
+
file,
|
|
842
|
+
sheet: file === void 0 ? void 0 : input.sheets.get(file)
|
|
843
|
+
};
|
|
844
|
+
});
|
|
845
|
+
const deferred = elements.filter((entry) => entry.sheet?.vicss === true);
|
|
846
|
+
if (deferred.length === 0) {
|
|
847
|
+
const unread = elements.filter((entry) => entry.element.kind === "link" && !entry.sheet);
|
|
848
|
+
if (scan.css.some((element) => element.kind === "style" && /@layer\s+vicss\.[\w.-]+\s*\{/.test(element.text ?? ""))) return left("its vicss CSS is inline already");
|
|
849
|
+
return unread.length === 0 ? left("it links no stylesheet holding vicss's CSS") : left(`it links no stylesheet holding vicss's CSS among those found in the output (not found: ${unread.map((entry) => entry.element.href).join(", ")})`, true);
|
|
850
|
+
}
|
|
851
|
+
for (const entry of deferred) {
|
|
852
|
+
const media = entry.element.media?.trim().toLowerCase();
|
|
853
|
+
if (media !== void 0 && media !== "" && media !== "all") return left(`${entry.file} has media="${entry.element.media}": inline, its rules would apply to every medium. Link it without a media attribute`, true);
|
|
854
|
+
if (entry.sheet.anonymous) return left(`${entry.file} has an anonymous @layer block: its copy in the full sheet, arriving after the inline one, would be a layer of its own after every other. Give the layer a name`, true);
|
|
855
|
+
if (entry.sheet.ordered) {
|
|
856
|
+
const index = elements.indexOf(entry);
|
|
857
|
+
const later = elements.slice(index + 1).find((other) => !deferred.includes(other));
|
|
858
|
+
if (later) {
|
|
859
|
+
const what = later.element.kind === "style" ? "a <style> element" : `the stylesheet ${later.element.href}`;
|
|
860
|
+
return left(`${entry.file} holds CSS besides vicss's, and ${what} comes after it: moved to the end of <body>, its rules would win over that one's where they lost before. Import that CSS before vicss's sheet, or keep it out of the sheet's chunk`, true);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const roots = [...scan.styles];
|
|
865
|
+
for (const entry of elements) {
|
|
866
|
+
if (deferred.includes(entry)) continue;
|
|
867
|
+
if (entry.element.kind === "style") roots.push(entry.element.text ?? "");
|
|
868
|
+
else if (entry.file !== void 0) {
|
|
869
|
+
const text = input.sheets.text(entry.file);
|
|
870
|
+
if (text !== void 0) roots.push(text);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const plan = planCritical(deferred.map((entry) => entry.sheet), scan.classes, roots);
|
|
874
|
+
if (input.requireClasses && plan.classRules === 0) return left("no vicss class in its markup: a client-rendered page or a template a server renders into, whose sheet must stay in <head>");
|
|
875
|
+
const parts = { ...EMPTY_PARTS };
|
|
876
|
+
const declared = /* @__PURE__ */ new Set();
|
|
877
|
+
const styles = deferred.map((entry) => {
|
|
878
|
+
return escapeStyle(rebaseUrls(printSheet(entry.sheet, plan, declared, parts), entry.element.href));
|
|
879
|
+
});
|
|
880
|
+
const hashes = styles.map((css) => `sha256-${createHash("sha256").update(css, "utf8").digest("base64")}`);
|
|
881
|
+
const all = styles.join("");
|
|
882
|
+
const edits = [];
|
|
883
|
+
deferred.forEach((entry, i) => {
|
|
884
|
+
const { start, end } = entry.element;
|
|
885
|
+
const charset = scan.charset;
|
|
886
|
+
const between = (from, to) => scan.css.some((element) => element.start >= from && element.start < to);
|
|
887
|
+
const style = `<style ${MARKER}>${styles[i]}</style>`;
|
|
888
|
+
const inert = entry.element.tag.replace(/^<link/i, "$& disabled");
|
|
889
|
+
if (charset && charset.start >= end && !between(end, charset.start)) {
|
|
890
|
+
edits.push({
|
|
891
|
+
start: charset.end,
|
|
892
|
+
end: charset.end,
|
|
893
|
+
text: style
|
|
894
|
+
});
|
|
895
|
+
edits.push({
|
|
896
|
+
start,
|
|
897
|
+
end,
|
|
898
|
+
text: inert
|
|
899
|
+
});
|
|
900
|
+
} else edits.push({
|
|
901
|
+
start,
|
|
902
|
+
end,
|
|
903
|
+
text: style + inert
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
const end = scan.bodyEnd ?? scan.htmlEnd ?? html.length;
|
|
907
|
+
const tail = Math.min(scan.trailingScript ?? end, end);
|
|
908
|
+
edits.push({
|
|
909
|
+
start: tail,
|
|
910
|
+
end: tail,
|
|
911
|
+
text: `${deferred.map((entry) => entry.element.tag).join("\n")}\n`
|
|
912
|
+
});
|
|
913
|
+
for (const policy of scan.policies) {
|
|
914
|
+
const allowed = allowStyles(policy.content, hashes);
|
|
915
|
+
if (allowed === void 0) continue;
|
|
916
|
+
const value = allowed.replaceAll("&", "&").replaceAll("\"", """);
|
|
917
|
+
const tag = html.slice(policy.start, policy.end).replace(/(\scontent\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s"'>]+)/i, (_, name) => {
|
|
918
|
+
return `${name}"${value}"`;
|
|
919
|
+
});
|
|
920
|
+
edits.push({
|
|
921
|
+
start: policy.start,
|
|
922
|
+
end: policy.end,
|
|
923
|
+
text: tag
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
edits.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
927
|
+
let out = "";
|
|
928
|
+
let at = 0;
|
|
929
|
+
for (const edit of edits) {
|
|
930
|
+
out += html.slice(at, edit.start) + edit.text;
|
|
931
|
+
at = edit.end;
|
|
932
|
+
}
|
|
933
|
+
out += html.slice(at);
|
|
934
|
+
return {
|
|
935
|
+
page,
|
|
936
|
+
html: out,
|
|
937
|
+
hashes,
|
|
938
|
+
bytes: Buffer.byteLength(all),
|
|
939
|
+
gzip: gzipSync(all).length,
|
|
940
|
+
parts,
|
|
941
|
+
deferred: deferred.map((entry) => entry.file)
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* `policy` with the inline styles' hashes allowed for `<style>` elements, or
|
|
946
|
+
* undefined when it allows them already. They answer to `style-src-elem`,
|
|
947
|
+
* else `style-src`, else `default-src`; this one gets the hashes (a
|
|
948
|
+
* `style-src` copied from `default-src`). `'unsafe-inline'` allows them
|
|
949
|
+
* unless a hash or nonce is listed, and `'none'` is left as it is: it
|
|
950
|
+
* blocks the stylesheets too.
|
|
951
|
+
*/
|
|
952
|
+
function allowStyles(policy, hashes) {
|
|
953
|
+
const directives = policy.split(";").map((directive) => directive.trim().split(/[\t\n\f\r ]+/)).filter(([name]) => name !== "");
|
|
954
|
+
const names = directives.map(([name]) => name.toLowerCase());
|
|
955
|
+
const index = [
|
|
956
|
+
"style-src-elem",
|
|
957
|
+
"style-src",
|
|
958
|
+
"default-src"
|
|
959
|
+
].map((name) => names.indexOf(name)).find((i) => i !== -1);
|
|
960
|
+
if (index === void 0) return void 0;
|
|
961
|
+
const sources = directives[index].slice(1);
|
|
962
|
+
const lower = sources.map((source) => source.toLowerCase());
|
|
963
|
+
if (lower.includes("'none'")) return void 0;
|
|
964
|
+
const keyed = lower.some((source) => /^'(?:sha(?:256|384|512)-|nonce-)/.test(source));
|
|
965
|
+
if (lower.includes("'unsafe-inline'") && !keyed) return void 0;
|
|
966
|
+
const added = hashes.map((hash) => `'${hash}'`).filter((hash) => !sources.includes(hash));
|
|
967
|
+
if (added.length === 0) return void 0;
|
|
968
|
+
if (names[index] === "default-src") directives.push([
|
|
969
|
+
"style-src",
|
|
970
|
+
...sources,
|
|
971
|
+
...added
|
|
972
|
+
]);
|
|
973
|
+
else directives[index].push(...added);
|
|
974
|
+
return directives.map((directive) => directive.join(" ")).join("; ");
|
|
975
|
+
}
|
|
976
|
+
/** Marks the inline styles, so a page is never processed twice. */
|
|
977
|
+
const MARKER = "data-vicss-critical";
|
|
978
|
+
/**
|
|
979
|
+
* The output file a stylesheet's `href` names. A relative one resolves from
|
|
980
|
+
* the page; an absolute path or URL (a `base`, a CDN) ends with the file's
|
|
981
|
+
* path: the longest that matches wins.
|
|
982
|
+
*/
|
|
983
|
+
function resolveHref(href, page, files) {
|
|
984
|
+
let target = href.trim().replace(/[?#].*$/s, "");
|
|
985
|
+
try {
|
|
986
|
+
target = decodeURI(target);
|
|
987
|
+
} catch {}
|
|
988
|
+
if (target === "") return void 0;
|
|
989
|
+
if (!/^(?:[a-z][a-z\d+.-]*:|\/)/i.test(target)) {
|
|
990
|
+
const file = path.posix.normalize(path.posix.join(path.posix.dirname(page), target));
|
|
991
|
+
return files.includes(file) ? file : void 0;
|
|
992
|
+
}
|
|
993
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(target) && !/^https?:/i.test(target)) return void 0;
|
|
994
|
+
const pathname = target.replace(/^[a-z][a-z\d+.-]*:\/\/[^/]*/i, "").replace(/^\/\/[^/]*/, "");
|
|
995
|
+
let best;
|
|
996
|
+
for (const file of files) if (pathname === `/${file}` || pathname.endsWith(`/${file}`)) {
|
|
997
|
+
if (!best || file.length > best.length) best = file;
|
|
998
|
+
}
|
|
999
|
+
return best;
|
|
1000
|
+
}
|
|
1001
|
+
/** Elements whose content is text, not markup. */
|
|
1002
|
+
const RAW_TEXT = /* @__PURE__ */ new Set([
|
|
1003
|
+
"script",
|
|
1004
|
+
"style",
|
|
1005
|
+
"textarea",
|
|
1006
|
+
"title",
|
|
1007
|
+
"xmp",
|
|
1008
|
+
"iframe",
|
|
1009
|
+
"noembed",
|
|
1010
|
+
"noframes",
|
|
1011
|
+
"noscript",
|
|
1012
|
+
"plaintext"
|
|
1013
|
+
]);
|
|
1014
|
+
const TAG_NAME = /[a-zA-Z][^\s/>]*/y;
|
|
1015
|
+
/**
|
|
1016
|
+
* A tokenizer, not a parser: tags, their attributes, comments and raw-text
|
|
1017
|
+
* elements are all critical CSS needs from a page, and it never builds a tree.
|
|
1018
|
+
*/
|
|
1019
|
+
function scanHtml(html) {
|
|
1020
|
+
const css = [];
|
|
1021
|
+
const classes = /* @__PURE__ */ new Set();
|
|
1022
|
+
const styles = [];
|
|
1023
|
+
let charset;
|
|
1024
|
+
let trailingScript;
|
|
1025
|
+
const policies = [];
|
|
1026
|
+
let bodyEnd;
|
|
1027
|
+
let htmlEnd;
|
|
1028
|
+
let processed = false;
|
|
1029
|
+
let template = 0;
|
|
1030
|
+
let i = 0;
|
|
1031
|
+
while (i < html.length) {
|
|
1032
|
+
const lt = html.indexOf("<", i);
|
|
1033
|
+
if (lt === -1) break;
|
|
1034
|
+
if (html.startsWith("<!--", lt)) {
|
|
1035
|
+
const end = html.indexOf("-->", lt + 4);
|
|
1036
|
+
i = end === -1 ? html.length : end + 3;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const next = html[lt + 1];
|
|
1040
|
+
if (next === "!" || next === "?") {
|
|
1041
|
+
const end = html.indexOf(">", lt + 2);
|
|
1042
|
+
i = end === -1 ? html.length : end + 1;
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if (next === "/") {
|
|
1046
|
+
TAG_NAME.lastIndex = lt + 2;
|
|
1047
|
+
const name = TAG_NAME.exec(html)?.[0].toLowerCase();
|
|
1048
|
+
const end = html.indexOf(">", lt + 2);
|
|
1049
|
+
if (name === "template") template = Math.max(0, template - 1);
|
|
1050
|
+
else if (name === "body") bodyEnd = lt;
|
|
1051
|
+
else if (name === "html") htmlEnd = lt;
|
|
1052
|
+
i = end === -1 ? html.length : end + 1;
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
TAG_NAME.lastIndex = lt + 1;
|
|
1056
|
+
const match = TAG_NAME.exec(html);
|
|
1057
|
+
if (!match) {
|
|
1058
|
+
i = lt + 1;
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
const name = match[0].toLowerCase();
|
|
1062
|
+
const { attrs, end } = readAttributes(html, lt + 1 + match[0].length);
|
|
1063
|
+
for (const token of (attrs.get("class") ?? "").split(/[\t\n\f\r ]+/)) if (token !== "") classes.add(token);
|
|
1064
|
+
const style = attrs.get("style");
|
|
1065
|
+
if (style !== void 0) styles.push(style);
|
|
1066
|
+
if (template === 0) {
|
|
1067
|
+
if (name !== "script") trailingScript = void 0;
|
|
1068
|
+
else if (blocksParser(attrs)) trailingScript ??= lt;
|
|
1069
|
+
}
|
|
1070
|
+
if (name === "template") template++;
|
|
1071
|
+
else if (name === "meta") {
|
|
1072
|
+
const equiv = attrs.get("http-equiv")?.trim().toLowerCase();
|
|
1073
|
+
if (!charset && (attrs.has("charset") || equiv === "content-type")) charset = {
|
|
1074
|
+
start: lt,
|
|
1075
|
+
end
|
|
1076
|
+
};
|
|
1077
|
+
if (equiv === "content-security-policy" && attrs.has("content") && template === 0) policies.push({
|
|
1078
|
+
start: lt,
|
|
1079
|
+
end,
|
|
1080
|
+
content: attrs.get("content")
|
|
1081
|
+
});
|
|
1082
|
+
} else if (name === "link" && template === 0) {
|
|
1083
|
+
const rel = (attrs.get("rel") ?? "").toLowerCase().split(/\s+/);
|
|
1084
|
+
if (rel.includes("stylesheet") && !rel.includes("alternate") && !attrs.has("disabled")) css.push({
|
|
1085
|
+
kind: "link",
|
|
1086
|
+
start: lt,
|
|
1087
|
+
end,
|
|
1088
|
+
tag: html.slice(lt, end),
|
|
1089
|
+
...attrs.has("href") ? { href: attrs.get("href") } : {},
|
|
1090
|
+
...attrs.has("media") ? { media: attrs.get("media") } : {}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
if (RAW_TEXT.has(name) && html[end - 2] !== "/") {
|
|
1094
|
+
const close = new RegExp(`</${name}(?=[\\s/>])`, "gi");
|
|
1095
|
+
close.lastIndex = end;
|
|
1096
|
+
const found = name === "plaintext" ? null : close.exec(html);
|
|
1097
|
+
const contentEnd = found ? found.index : html.length;
|
|
1098
|
+
const after = found ? html.indexOf(">", contentEnd) : -1;
|
|
1099
|
+
const elementEnd = after === -1 ? html.length : after + 1;
|
|
1100
|
+
if (name === "noscript") {
|
|
1101
|
+
const inner = scanHtml(html.slice(end, contentEnd));
|
|
1102
|
+
for (const name of inner.classes) classes.add(name);
|
|
1103
|
+
styles.push(...inner.styles);
|
|
1104
|
+
} else if (name === "style") {
|
|
1105
|
+
if (attrs.has(MARKER)) processed = true;
|
|
1106
|
+
if (template === 0) css.push({
|
|
1107
|
+
kind: "style",
|
|
1108
|
+
start: lt,
|
|
1109
|
+
end: elementEnd,
|
|
1110
|
+
text: html.slice(end, contentEnd),
|
|
1111
|
+
tag: html.slice(lt, end)
|
|
1112
|
+
});
|
|
1113
|
+
else styles.push(html.slice(end, contentEnd));
|
|
1114
|
+
}
|
|
1115
|
+
i = elementEnd;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
i = end;
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
css,
|
|
1122
|
+
classes,
|
|
1123
|
+
styles,
|
|
1124
|
+
...charset ? { charset } : {},
|
|
1125
|
+
...trailingScript === void 0 ? {} : { trailingScript },
|
|
1126
|
+
policies,
|
|
1127
|
+
...bodyEnd === void 0 ? {} : { bodyEnd },
|
|
1128
|
+
...htmlEnd === void 0 ? {} : { htmlEnd },
|
|
1129
|
+
processed
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
/** JavaScript MIME types, which the HTML spec matches whole, parameters and all. */
|
|
1133
|
+
const SCRIPT_TYPE = /^(?:(?:text|application)\/(?:x-)?(?:java|ecma)script|text\/(?:javascript1\.[0-5]|jscript|livescript))$/;
|
|
1134
|
+
/**
|
|
1135
|
+
* A classic script the parser stops for: inline, or external and neither
|
|
1136
|
+
* `async` nor `defer`. Module and deferred scripts wait for every
|
|
1137
|
+
* stylesheet before them wherever they are; async ones wait for none.
|
|
1138
|
+
*/
|
|
1139
|
+
function blocksParser(attrs) {
|
|
1140
|
+
const type = attrs.get("type")?.trim().toLowerCase();
|
|
1141
|
+
if (type !== void 0 && type !== "" && !SCRIPT_TYPE.test(type)) return false;
|
|
1142
|
+
return !attrs.has("src") || !(attrs.has("async") || attrs.has("defer"));
|
|
1143
|
+
}
|
|
1144
|
+
/** A start tag's attributes from after its name; names lowercased, values decoded, the first of each kept. */
|
|
1145
|
+
function readAttributes(html, from) {
|
|
1146
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
1147
|
+
let i = from;
|
|
1148
|
+
for (;;) {
|
|
1149
|
+
while (i < html.length && /[\s/]/.test(html[i])) i++;
|
|
1150
|
+
if (i >= html.length) return {
|
|
1151
|
+
attrs,
|
|
1152
|
+
end: html.length
|
|
1153
|
+
};
|
|
1154
|
+
if (html[i] === ">") return {
|
|
1155
|
+
attrs,
|
|
1156
|
+
end: i + 1
|
|
1157
|
+
};
|
|
1158
|
+
let j = i;
|
|
1159
|
+
while (j < html.length && !/[\s/>=]/.test(html[j])) j++;
|
|
1160
|
+
if (j === i) j++;
|
|
1161
|
+
const name = html.slice(i, j).toLowerCase();
|
|
1162
|
+
i = j;
|
|
1163
|
+
while (i < html.length && /\s/.test(html[i])) i++;
|
|
1164
|
+
let value = "";
|
|
1165
|
+
if (html[i] === "=") {
|
|
1166
|
+
i++;
|
|
1167
|
+
while (i < html.length && /\s/.test(html[i])) i++;
|
|
1168
|
+
const quote = html[i];
|
|
1169
|
+
if (quote === "\"" || quote === "'") {
|
|
1170
|
+
const close = html.indexOf(quote, i + 1);
|
|
1171
|
+
const end = close === -1 ? html.length : close;
|
|
1172
|
+
value = html.slice(i + 1, end);
|
|
1173
|
+
i = end + 1;
|
|
1174
|
+
} else {
|
|
1175
|
+
let k = i;
|
|
1176
|
+
while (k < html.length && !/[\s>]/.test(html[k])) k++;
|
|
1177
|
+
value = html.slice(i, k);
|
|
1178
|
+
i = k;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (!attrs.has(name)) attrs.set(name, decodeEntities(value));
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const NAMED = {
|
|
1185
|
+
amp: "&",
|
|
1186
|
+
lt: "<",
|
|
1187
|
+
gt: ">",
|
|
1188
|
+
quot: "\"",
|
|
1189
|
+
apos: "'",
|
|
1190
|
+
nbsp: "\xA0"
|
|
1191
|
+
};
|
|
1192
|
+
function decodeEntities(text) {
|
|
1193
|
+
if (!text.includes("&")) return text;
|
|
1194
|
+
return text.replace(/&(#x[\da-f]+|#\d+|[a-z]+);?/gi, (entity, body) => {
|
|
1195
|
+
if (body[0] === "#") {
|
|
1196
|
+
const code = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : Number(body.slice(1));
|
|
1197
|
+
return code > 0 && code <= 1114111 ? String.fromCodePoint(code) : entity;
|
|
1198
|
+
}
|
|
1199
|
+
return NAMED[body.toLowerCase()] ?? entity;
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
/** At-rules whose blocks hold rules. */
|
|
1203
|
+
const GROUPS = /* @__PURE__ */ new Set([
|
|
1204
|
+
"media",
|
|
1205
|
+
"supports",
|
|
1206
|
+
"container",
|
|
1207
|
+
"layer",
|
|
1208
|
+
"scope",
|
|
1209
|
+
"starting-style",
|
|
1210
|
+
"document",
|
|
1211
|
+
"-moz-document"
|
|
1212
|
+
]);
|
|
1213
|
+
/** At-rules whose order between stylesheets never matters in practice. */
|
|
1214
|
+
const UNORDERED = /* @__PURE__ */ new Set([
|
|
1215
|
+
"layer",
|
|
1216
|
+
"property",
|
|
1217
|
+
"font-face",
|
|
1218
|
+
"keyframes",
|
|
1219
|
+
"-webkit-keyframes",
|
|
1220
|
+
"charset"
|
|
1221
|
+
]);
|
|
1222
|
+
/**
|
|
1223
|
+
* Index of the first of `stops` at nesting depth 0 from `from`, or `to`.
|
|
1224
|
+
* Strings, comments, escapes and unquoted `url()`s never stop it.
|
|
1225
|
+
*/
|
|
1226
|
+
function find(text, from, to, stops) {
|
|
1227
|
+
const stack = [];
|
|
1228
|
+
for (let i = from; i < to; i++) {
|
|
1229
|
+
const c = text[i];
|
|
1230
|
+
if (c === "\\") {
|
|
1231
|
+
i++;
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
1235
|
+
const end = text.indexOf("*/", i + 2);
|
|
1236
|
+
i = end === -1 || end >= to ? to : end + 1;
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
if (c === "\"" || c === "'") {
|
|
1240
|
+
i = stringEnd(text, i, to) - 1;
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if ((c === "u" || c === "U") && /^url\(/i.test(text.slice(i, i + 4)) && !/[\w-]/.test(text[i - 1] ?? "")) {
|
|
1244
|
+
const end = urlEnd(text, i + 4, to);
|
|
1245
|
+
if (end !== -1) {
|
|
1246
|
+
i = end;
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (stack.length === 0 && stops.includes(c)) return i;
|
|
1251
|
+
if (c === "(") stack.push(")");
|
|
1252
|
+
else if (c === "[") stack.push("]");
|
|
1253
|
+
else if (c === "{") stack.push("}");
|
|
1254
|
+
else if ((c === ")" || c === "]" || c === "}") && stack.at(-1) === c) stack.pop();
|
|
1255
|
+
}
|
|
1256
|
+
return to;
|
|
1257
|
+
}
|
|
1258
|
+
/** After the string opening at `at`. */
|
|
1259
|
+
function stringEnd(text, at, to) {
|
|
1260
|
+
const quote = text[at];
|
|
1261
|
+
for (let i = at + 1; i < to; i++) if (text[i] === "\\") i++;
|
|
1262
|
+
else if (text[i] === quote || text[i] === "\n") return i + 1;
|
|
1263
|
+
return to;
|
|
1264
|
+
}
|
|
1265
|
+
/** The `)` of an unquoted `url(` whose content starts at `from`; -1 for a quoted one. */
|
|
1266
|
+
function urlEnd(text, from, to) {
|
|
1267
|
+
let i = from;
|
|
1268
|
+
while (i < to && /\s/.test(text[i])) i++;
|
|
1269
|
+
if (text[i] === "\"" || text[i] === "'") return -1;
|
|
1270
|
+
for (; i < to; i++) if (text[i] === "\\") i++;
|
|
1271
|
+
else if (text[i] === ")") return i;
|
|
1272
|
+
return to;
|
|
1273
|
+
}
|
|
1274
|
+
function skipSpace(text, from, to) {
|
|
1275
|
+
let i = from;
|
|
1276
|
+
for (;;) {
|
|
1277
|
+
while (i < to && /\s/.test(text[i])) i++;
|
|
1278
|
+
if (text.startsWith("/*", i)) {
|
|
1279
|
+
const end = text.indexOf("*/", i + 2);
|
|
1280
|
+
i = end === -1 ? to : end + 2;
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
return i;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
function parseCss(text, from = 0, to = text.length) {
|
|
1287
|
+
const nodes = [];
|
|
1288
|
+
let i = from;
|
|
1289
|
+
for (;;) {
|
|
1290
|
+
i = skipSpace(text, i, to);
|
|
1291
|
+
if (i >= to) return nodes;
|
|
1292
|
+
const start = i;
|
|
1293
|
+
if (text[i] === "@") {
|
|
1294
|
+
const name = /^@(-?[\w-]*)/.exec(text.slice(i, i + 64))[1].toLowerCase();
|
|
1295
|
+
const stop = find(text, i + 1 + name.length, to, "{;}");
|
|
1296
|
+
const prelude = text.slice(i + 1 + name.length, stop).trim();
|
|
1297
|
+
if (stop >= to || text[stop] !== "{") {
|
|
1298
|
+
const end = stop >= to ? to : stop + 1;
|
|
1299
|
+
nodes.push({
|
|
1300
|
+
type: name,
|
|
1301
|
+
start,
|
|
1302
|
+
end,
|
|
1303
|
+
prelude
|
|
1304
|
+
});
|
|
1305
|
+
i = end;
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
const close = find(text, stop + 1, to, "}");
|
|
1309
|
+
nodes.push({
|
|
1310
|
+
type: name,
|
|
1311
|
+
start,
|
|
1312
|
+
end: Math.min(close + 1, to),
|
|
1313
|
+
prelude,
|
|
1314
|
+
bodyStart: stop + 1,
|
|
1315
|
+
bodyEnd: close,
|
|
1316
|
+
...GROUPS.has(name) ? { children: parseCss(text, stop + 1, close) } : {}
|
|
1317
|
+
});
|
|
1318
|
+
i = close + 1;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const open = find(text, i, to, "{;}");
|
|
1322
|
+
if (open >= to || text[open] !== "{") {
|
|
1323
|
+
const end = open >= to ? to : open + 1;
|
|
1324
|
+
nodes.push({
|
|
1325
|
+
type: "raw",
|
|
1326
|
+
start,
|
|
1327
|
+
end,
|
|
1328
|
+
prelude: text.slice(start, open).trim()
|
|
1329
|
+
});
|
|
1330
|
+
i = end;
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
const close = find(text, open + 1, to, "}");
|
|
1334
|
+
nodes.push({
|
|
1335
|
+
type: "rule",
|
|
1336
|
+
start,
|
|
1337
|
+
end: Math.min(close + 1, to),
|
|
1338
|
+
prelude: text.slice(start, open).trim(),
|
|
1339
|
+
bodyStart: open + 1,
|
|
1340
|
+
bodyEnd: close
|
|
1341
|
+
});
|
|
1342
|
+
i = close + 1;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
/** Parts of `text` between separators at depth 0, each with its offset. */
|
|
1346
|
+
function splitTop(text, separator) {
|
|
1347
|
+
const parts = [];
|
|
1348
|
+
let start = 0;
|
|
1349
|
+
for (;;) {
|
|
1350
|
+
const at = find(text, start, text.length, separator);
|
|
1351
|
+
parts.push({
|
|
1352
|
+
text: text.slice(start, at),
|
|
1353
|
+
start
|
|
1354
|
+
});
|
|
1355
|
+
if (at >= text.length) return parts;
|
|
1356
|
+
start = at + 1;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
/** A CSS identifier from `from`, escapes resolved, and where it ends. */
|
|
1360
|
+
function readIdent(text, from) {
|
|
1361
|
+
let name = "";
|
|
1362
|
+
let i = from;
|
|
1363
|
+
while (i < text.length) {
|
|
1364
|
+
const c = text[i];
|
|
1365
|
+
if (c === "\\") {
|
|
1366
|
+
const hex = /^[\da-fA-F]{1,6}/.exec(text.slice(i + 1, i + 7));
|
|
1367
|
+
if (hex) {
|
|
1368
|
+
const code = parseInt(hex[0], 16);
|
|
1369
|
+
name += code === 0 || code > 1114111 ? "�" : String.fromCodePoint(code);
|
|
1370
|
+
i += 1 + hex[0].length;
|
|
1371
|
+
if (/\s/.test(text[i] ?? "")) i += text.startsWith("\r\n", i) ? 2 : 1;
|
|
1372
|
+
} else if (i + 1 < text.length && text[i + 1] !== "\n") {
|
|
1373
|
+
name += text[i + 1];
|
|
1374
|
+
i += 2;
|
|
1375
|
+
} else break;
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (/[\w-]/.test(c) || c.charCodeAt(0) >= 128) {
|
|
1379
|
+
name += c;
|
|
1380
|
+
i++;
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
break;
|
|
1384
|
+
}
|
|
1385
|
+
return [name, i];
|
|
1386
|
+
}
|
|
1387
|
+
/** Pseudo-classes whose selector argument must match in the page for theirs to. */
|
|
1388
|
+
const REQUIRING = /* @__PURE__ */ new Set([
|
|
1389
|
+
"is",
|
|
1390
|
+
"where",
|
|
1391
|
+
"matches",
|
|
1392
|
+
"-webkit-any",
|
|
1393
|
+
"-moz-any",
|
|
1394
|
+
"has"
|
|
1395
|
+
]);
|
|
1396
|
+
/**
|
|
1397
|
+
* The classes some element of the page must have for `selector` to match
|
|
1398
|
+
* there: those of its compounds, and those every alternative of an `:is()`,
|
|
1399
|
+
* `:where()` or `:has()` requires. Never those under `:not()` and other
|
|
1400
|
+
* functional pseudo-classes, whose arguments may match by being absent.
|
|
1401
|
+
*
|
|
1402
|
+
* In vicss's layers (`vicss`), a class under `:where()` that vicss did not
|
|
1403
|
+
* name comes from the config, a condition's or a mode's selector (`.dark`
|
|
1404
|
+
* of `darkMode: { selector: ".dark" }`): a script in `<head>` sets it before
|
|
1405
|
+
* the first paint, from a stored preference the build cannot see. Only the
|
|
1406
|
+
* classes vicss names itself, markers and themes (`relates`), are in the
|
|
1407
|
+
* markup. Readable ones say so (`vx-…`); a build's hashes only its namer
|
|
1408
|
+
* knows (PLAN D31), so without it (`vicss critical`) a hashed theme's or
|
|
1409
|
+
* marker's rules count as the page's: more bytes, never a missing style.
|
|
1410
|
+
*/
|
|
1411
|
+
function requiredClasses(selector, vicss = false, relates = isRelating) {
|
|
1412
|
+
const out = [];
|
|
1413
|
+
const end = selector.length;
|
|
1414
|
+
let i = 0;
|
|
1415
|
+
while (i < end) {
|
|
1416
|
+
const c = selector[i];
|
|
1417
|
+
if (c === "\\") i += 2;
|
|
1418
|
+
else if (c === "\"" || c === "'") i = stringEnd(selector, i, end);
|
|
1419
|
+
else if (c === "[") i = find(selector, i + 1, end, "]") + 1;
|
|
1420
|
+
else if (c === ".") {
|
|
1421
|
+
const [name, next] = readIdent(selector, i + 1);
|
|
1422
|
+
if (name !== "") out.push(name);
|
|
1423
|
+
i = Math.max(next, i + 1);
|
|
1424
|
+
} else if (c === ":") {
|
|
1425
|
+
const [name, next] = readIdent(selector, selector[i + 1] === ":" ? i + 2 : i + 1);
|
|
1426
|
+
if (selector[next] !== "(") {
|
|
1427
|
+
i = Math.max(next, i + 1);
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const close = find(selector, next + 1, end, ")");
|
|
1431
|
+
const pseudo = name.toLowerCase();
|
|
1432
|
+
if (REQUIRING.has(pseudo)) {
|
|
1433
|
+
const [first = [], ...rest] = splitTop(selector.slice(next + 1, close), ",").map((part) => requiredClasses(part.text, vicss, relates));
|
|
1434
|
+
const all = first.filter((name) => rest.every((other) => other.includes(name)));
|
|
1435
|
+
out.push(...vicss && pseudo === "where" ? all.filter(relates) : all);
|
|
1436
|
+
}
|
|
1437
|
+
i = close + 1;
|
|
1438
|
+
} else i++;
|
|
1439
|
+
}
|
|
1440
|
+
return out;
|
|
1441
|
+
}
|
|
1442
|
+
/** Whether `selector` is one `:where()` and nothing else. */
|
|
1443
|
+
function isLoneWhere(selector) {
|
|
1444
|
+
const text = selector.trim();
|
|
1445
|
+
return /^:where\(/i.test(text) && find(text, 7, text.length, ")") === text.length - 1;
|
|
1446
|
+
}
|
|
1447
|
+
const IDENT = String.raw`(?:[\w-]|\\[\s\S]|[^\x00-\x7f])+`;
|
|
1448
|
+
/** Custom properties a value reads, by `var()` or a container style query. */
|
|
1449
|
+
const REFERENCE = new RegExp(String.raw`(?:var|style)\(\s*(--${IDENT})`, "g");
|
|
1450
|
+
function references(text, into) {
|
|
1451
|
+
for (const match of text.matchAll(REFERENCE)) into.add(match[1]);
|
|
1452
|
+
}
|
|
1453
|
+
/** A custom property declaration and its value, up to the end of the declaration. */
|
|
1454
|
+
const DECLARATION$1 = new RegExp(String.raw`(--${IDENT})\s*:([^;}]*)`, "g");
|
|
1455
|
+
const REGISTRATION = new RegExp(String.raw`@property\s+(--${IDENT})`, "gi");
|
|
1456
|
+
const VAR = new RegExp(String.raw`^var\(\s*(--${IDENT})\s*([,)])`, "i");
|
|
1457
|
+
/**
|
|
1458
|
+
* Custom properties `text` sets to a value, as a declaration anywhere in it
|
|
1459
|
+
* (a container query's `style(--x: y)` too: more is safe), but for those it
|
|
1460
|
+
* sets to `initial` only.
|
|
1461
|
+
*/
|
|
1462
|
+
function setters(text, into) {
|
|
1463
|
+
for (const match of text.matchAll(DECLARATION$1)) if (match[2].trim().toLowerCase() !== "initial") into.add(match[1]);
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* The custom properties `value` reads with no fallback, outside any
|
|
1467
|
+
* `var()`'s fallback: when one holds nothing, so does the value (invalid
|
|
1468
|
+
* at computed-value time).
|
|
1469
|
+
*/
|
|
1470
|
+
function strictReads(value) {
|
|
1471
|
+
const out = [];
|
|
1472
|
+
let depth = 0;
|
|
1473
|
+
const fallbacks = [];
|
|
1474
|
+
for (let i = 0; i < value.length; i++) {
|
|
1475
|
+
const c = value[i];
|
|
1476
|
+
if (c === "\\") i++;
|
|
1477
|
+
else if (c === "\"" || c === "'") i = stringEnd(value, i, value.length) - 1;
|
|
1478
|
+
else if (c === "(") depth++;
|
|
1479
|
+
else if (c === ")") {
|
|
1480
|
+
if (fallbacks.at(-1) === depth) fallbacks.pop();
|
|
1481
|
+
depth--;
|
|
1482
|
+
} else if ((c === "v" || c === "V") && !/[\w-]/.test(value[i - 1] ?? "")) {
|
|
1483
|
+
const match = VAR.exec(value.slice(i));
|
|
1484
|
+
if (!match) continue;
|
|
1485
|
+
i += match[0].length - 1;
|
|
1486
|
+
if (match[2] === ",") fallbacks.push(++depth);
|
|
1487
|
+
else if (fallbacks.length === 0) out.push(match[1]);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return out;
|
|
1491
|
+
}
|
|
1492
|
+
/** Output stylesheets by file, read and parsed once for every page. */
|
|
1493
|
+
var SheetCache = class {
|
|
1494
|
+
#sheets = /* @__PURE__ */ new Map();
|
|
1495
|
+
#read;
|
|
1496
|
+
#relates;
|
|
1497
|
+
/**
|
|
1498
|
+
* `relates` tells a theme's or a marker's class from a config's
|
|
1499
|
+
* (`requiredClasses`): a build's namer knows its hashed ones.
|
|
1500
|
+
*/
|
|
1501
|
+
constructor(read, relates = isRelating) {
|
|
1502
|
+
this.#read = read;
|
|
1503
|
+
this.#relates = relates;
|
|
1504
|
+
}
|
|
1505
|
+
text(file) {
|
|
1506
|
+
return this.get(file)?.text;
|
|
1507
|
+
}
|
|
1508
|
+
get(file) {
|
|
1509
|
+
if (this.#sheets.has(file)) return this.#sheets.get(file);
|
|
1510
|
+
const raw = this.#read(file);
|
|
1511
|
+
const sheet = raw === void 0 ? void 0 : analyse(file, raw.replace(/^/, ""), this.#relates);
|
|
1512
|
+
this.#sheets.set(file, sheet);
|
|
1513
|
+
return sheet;
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
const isVicssLayer = (layer) => layer !== void 0 && (layer === "vicss" || layer.startsWith("vicss."));
|
|
1517
|
+
/** The full name of a layer block's layer, inside `outer`; undefined for an anonymous one. */
|
|
1518
|
+
function layerOf(node, outer) {
|
|
1519
|
+
const name = node.prelude.trim();
|
|
1520
|
+
if (name === "") return void 0;
|
|
1521
|
+
return outer === void 0 ? name : `${outer}.${name}`;
|
|
1522
|
+
}
|
|
1523
|
+
function analyse(file, text, relates) {
|
|
1524
|
+
const nodes = parseCss(text);
|
|
1525
|
+
const rules = /* @__PURE__ */ new Map();
|
|
1526
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
1527
|
+
let vicss = false;
|
|
1528
|
+
let ordered = false;
|
|
1529
|
+
let anonymous = false;
|
|
1530
|
+
const visit = (list, layer, top) => {
|
|
1531
|
+
for (const node of list) {
|
|
1532
|
+
if (node.type === "rule") {
|
|
1533
|
+
const own = layer?.startsWith("vicss.a.") === true;
|
|
1534
|
+
const selectors = splitTop(node.prelude, ",").map((part) => ({
|
|
1535
|
+
text: part.text.trim(),
|
|
1536
|
+
required: own && isLoneWhere(part.text) ? requiredClasses(part.text.trim().slice(7, -1), true, relates) : requiredClasses(part.text, isVicssLayer(layer), relates)
|
|
1537
|
+
}));
|
|
1538
|
+
rules.set(node, {
|
|
1539
|
+
selectors,
|
|
1540
|
+
prefixed: selectors.some((selector) => /(?:^|[^\\]):{1,2}-/.test(selector.text))
|
|
1541
|
+
});
|
|
1542
|
+
if (layer === "vicss.tokens") {
|
|
1543
|
+
const body = text.slice(node.bodyStart, node.bodyEnd);
|
|
1544
|
+
declarations.set(node, splitTop(body, ";").filter((part) => part.text.trim() !== "").map((part) => {
|
|
1545
|
+
const property = /^\s*(--[^:\s]+)\s*:/.exec(part.text)?.[1];
|
|
1546
|
+
return property === void 0 ? { text: part.text } : {
|
|
1547
|
+
text: part.text,
|
|
1548
|
+
property
|
|
1549
|
+
};
|
|
1550
|
+
}));
|
|
1551
|
+
}
|
|
1552
|
+
if (isVicssLayer(layer)) vicss = true;
|
|
1553
|
+
else ordered = true;
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
if (node.type === "layer" && node.children) {
|
|
1557
|
+
const inner = layerOf(node, layer);
|
|
1558
|
+
if (inner === void 0) anonymous = true;
|
|
1559
|
+
visit(node.children, inner, top);
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
if (node.children) {
|
|
1563
|
+
visit(node.children, layer, false);
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
if (!(top && UNORDERED.has(node.type)) && !isVicssLayer(layer)) ordered = true;
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
visit(nodes, void 0, true);
|
|
1570
|
+
const custom = new Set([...text.matchAll(DECLARATION$1)].map((match) => match[1]));
|
|
1571
|
+
return {
|
|
1572
|
+
file,
|
|
1573
|
+
text,
|
|
1574
|
+
nodes,
|
|
1575
|
+
vicss,
|
|
1576
|
+
ordered,
|
|
1577
|
+
anonymous,
|
|
1578
|
+
rules,
|
|
1579
|
+
declarations,
|
|
1580
|
+
custom
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* What the inline CSS holds, over the files a page defers: the rules whose
|
|
1585
|
+
* selectors need only classes the page has; then, until nothing changes,
|
|
1586
|
+
* the custom properties those rules, the page's other CSS and its style
|
|
1587
|
+
* attributes read (and the ones those read), and the keyframes they name.
|
|
1588
|
+
*/
|
|
1589
|
+
function planCritical(sheets, classes, roots) {
|
|
1590
|
+
const kept = /* @__PURE__ */ new Map();
|
|
1591
|
+
const properties = /* @__PURE__ */ new Set();
|
|
1592
|
+
const corpus = [...roots];
|
|
1593
|
+
const tokenRules = [];
|
|
1594
|
+
const keyframes = [];
|
|
1595
|
+
let classRules = 0;
|
|
1596
|
+
const outside = /* @__PURE__ */ new Set();
|
|
1597
|
+
const registered = /* @__PURE__ */ new Set();
|
|
1598
|
+
for (const root of roots) {
|
|
1599
|
+
references(root, properties);
|
|
1600
|
+
setters(root, outside);
|
|
1601
|
+
for (const match of root.matchAll(REGISTRATION)) registered.add(match[1]);
|
|
1602
|
+
}
|
|
1603
|
+
const visit = (sheet, list, layer) => {
|
|
1604
|
+
for (const node of list) {
|
|
1605
|
+
const source = sheet.text.slice(node.start, node.end);
|
|
1606
|
+
if (node.type === "rule") {
|
|
1607
|
+
const info = sheet.rules.get(node);
|
|
1608
|
+
const selectors = info.selectors.filter((s) => s.required.every((c) => classes.has(c)));
|
|
1609
|
+
if (selectors.length === 0) continue;
|
|
1610
|
+
kept.set(node, selectors.map((s) => s.text));
|
|
1611
|
+
const global = layer === "vicss.global" || layer === "vicss.reset";
|
|
1612
|
+
if (isVicssLayer(layer) && !global && info.selectors.some((s) => s.required.length > 0)) classRules++;
|
|
1613
|
+
if (layer === "vicss.tokens") {
|
|
1614
|
+
tokenRules.push({
|
|
1615
|
+
sheet,
|
|
1616
|
+
node
|
|
1617
|
+
});
|
|
1618
|
+
const read = node.prelude + sheet.declarations.get(node).filter((d) => d.property === void 0).map((d) => d.text).join(";");
|
|
1619
|
+
references(read, properties);
|
|
1620
|
+
corpus.push(read);
|
|
1621
|
+
} else {
|
|
1622
|
+
references(source, properties);
|
|
1623
|
+
setters(source, outside);
|
|
1624
|
+
corpus.push(source);
|
|
1625
|
+
}
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
if (node.type === "layer" && node.children) {
|
|
1629
|
+
visit(sheet, node.children, layerOf(node, layer));
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
if (node.children) {
|
|
1633
|
+
references(node.prelude, properties);
|
|
1634
|
+
visit(sheet, node.children, layer);
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1637
|
+
if (node.type === "keyframes" || node.type === "-webkit-keyframes") {
|
|
1638
|
+
const name = node.prelude.replace(/^["']|["']$/g, "");
|
|
1639
|
+
keyframes.push({
|
|
1640
|
+
sheet,
|
|
1641
|
+
node,
|
|
1642
|
+
name: new RegExp(`(?<![\\w-])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`)
|
|
1643
|
+
});
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
if (node.type === "property") {
|
|
1647
|
+
registered.add(node.prelude.trim());
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
references(source, properties);
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
for (const sheet of sheets) visit(sheet, sheet.nodes, void 0);
|
|
1654
|
+
const invalid = invalidProperties(sheets, tokenRules, outside, registered);
|
|
1655
|
+
const needed = /* @__PURE__ */ new Set();
|
|
1656
|
+
const done = /* @__PURE__ */ new Set();
|
|
1657
|
+
for (let changed = true; changed;) {
|
|
1658
|
+
changed = false;
|
|
1659
|
+
for (const { sheet, node } of tokenRules) for (const declaration of sheet.declarations.get(node)) {
|
|
1660
|
+
const key = `${sheet.file}\0${node.start}\0${declaration.text}`;
|
|
1661
|
+
if (declaration.property === void 0 || done.has(key)) continue;
|
|
1662
|
+
if (!properties.has(declaration.property) || invalid.has(declaration.property)) continue;
|
|
1663
|
+
done.add(key);
|
|
1664
|
+
changed = true;
|
|
1665
|
+
references(declaration.text, properties);
|
|
1666
|
+
corpus.push(declaration.text);
|
|
1667
|
+
}
|
|
1668
|
+
for (const { sheet, node, name } of keyframes) {
|
|
1669
|
+
if (needed.has(node) || !corpus.some((text) => name.test(text))) continue;
|
|
1670
|
+
needed.add(node);
|
|
1671
|
+
changed = true;
|
|
1672
|
+
references(sheet.text.slice(node.start, node.end), properties);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
for (const name of invalid) properties.delete(name);
|
|
1676
|
+
return {
|
|
1677
|
+
kept,
|
|
1678
|
+
properties,
|
|
1679
|
+
keyframes: needed,
|
|
1680
|
+
classRules
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Custom properties that hold no value on any element of the page at its
|
|
1685
|
+
* first paint, so that leaving out their declarations changes nothing:
|
|
1686
|
+
* every declaration of one is in `vicss.tokens`, and is `initial` or reads,
|
|
1687
|
+
* with no fallback, one of the sheets' own custom properties that nothing
|
|
1688
|
+
* the page keeps sets (transitively), and none is registered, which would
|
|
1689
|
+
* give it a value. A palette's candidates
|
|
1690
|
+
* (`--color-palette-600_red: var(--palette_red) var(--color-red-600)`) are
|
|
1691
|
+
* such properties on a page where no element selects the palette, since
|
|
1692
|
+
* only the palette's atom sets its flag: the page inlines the tokens of its
|
|
1693
|
+
* own palettes. Custom properties the sheets never declare are assumed set,
|
|
1694
|
+
* by a script for one.
|
|
1695
|
+
*/
|
|
1696
|
+
function invalidProperties(sheets, tokenRules, outside, registered) {
|
|
1697
|
+
const own = (name) => sheets.some((sheet) => sheet.custom.has(name));
|
|
1698
|
+
const values = /* @__PURE__ */ new Map();
|
|
1699
|
+
for (const { sheet, node } of tokenRules) for (const { text, property } of sheet.declarations.get(node)) {
|
|
1700
|
+
if (property === void 0 || outside.has(property) || registered.has(property)) continue;
|
|
1701
|
+
const value = text.slice(text.indexOf(":") + 1).trim();
|
|
1702
|
+
const initial = value.toLowerCase() === "initial";
|
|
1703
|
+
const reads = initial ? [] : strictReads(value).filter(own);
|
|
1704
|
+
let list = values.get(property);
|
|
1705
|
+
if (!list) values.set(property, list = []);
|
|
1706
|
+
list.push({
|
|
1707
|
+
initial,
|
|
1708
|
+
reads
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
const set = /* @__PURE__ */ new Set([...outside, ...registered]);
|
|
1712
|
+
for (const [name, list] of values) if (list.some((value) => !value.initial)) set.add(name);
|
|
1713
|
+
const invalid = /* @__PURE__ */ new Set();
|
|
1714
|
+
for (let changed = true; changed;) {
|
|
1715
|
+
changed = false;
|
|
1716
|
+
for (const [name, list] of values) {
|
|
1717
|
+
if (invalid.has(name)) continue;
|
|
1718
|
+
if (!list.every((value) => value.initial || value.reads.some((read) => !set.has(read)))) continue;
|
|
1719
|
+
invalid.add(name);
|
|
1720
|
+
set.delete(name);
|
|
1721
|
+
changed = true;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return invalid;
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* One file's inline CSS: its text with what the page cannot use removed.
|
|
1728
|
+
* A layer block whose rules all went still declares its layer, as a
|
|
1729
|
+
* statement, unless the inline CSS declared it already: Lightning CSS
|
|
1730
|
+
* folds vicss's layer statement into the blocks that follow it, and a
|
|
1731
|
+
* layer first declared by the full sheet would land last in the order.
|
|
1732
|
+
*/
|
|
1733
|
+
function printSheet(sheet, plan, declared, parts) {
|
|
1734
|
+
const { text } = sheet;
|
|
1735
|
+
const print = (node, layer, conditional) => {
|
|
1736
|
+
const source = text.slice(node.start, node.end);
|
|
1737
|
+
switch (node.type) {
|
|
1738
|
+
case "rule": {
|
|
1739
|
+
const selectors = plan.kept.get(node);
|
|
1740
|
+
if (!selectors) return "";
|
|
1741
|
+
const info = sheet.rules.get(node);
|
|
1742
|
+
const prelude = selectors.length === info.selectors.length || info.prefixed ? node.prelude : selectors.join(",");
|
|
1743
|
+
let body = text.slice(node.bodyStart, node.bodyEnd);
|
|
1744
|
+
if (layer === "vicss.tokens") {
|
|
1745
|
+
const declarations = sheet.declarations.get(node).filter((d) => d.property === void 0 || plan.properties.has(d.property));
|
|
1746
|
+
if (declarations.length === 0) return "";
|
|
1747
|
+
body = declarations.map((d) => d.text).join(";");
|
|
1748
|
+
}
|
|
1749
|
+
return prelude === node.prelude && body === text.slice(node.bodyStart, node.bodyEnd) ? source : `${prelude}{${body}}`;
|
|
1750
|
+
}
|
|
1751
|
+
case "layer": {
|
|
1752
|
+
if (!node.children) {
|
|
1753
|
+
if (!conditional) for (const name of node.prelude.split(",")) declared.add(layer === void 0 ? name.trim() : `${layer}.${name.trim()}`);
|
|
1754
|
+
return source;
|
|
1755
|
+
}
|
|
1756
|
+
const inner = layerOf(node, layer);
|
|
1757
|
+
const known = inner !== void 0 && declared.has(inner);
|
|
1758
|
+
if (inner !== void 0 && !conditional) declared.add(inner);
|
|
1759
|
+
const children = node.children.map((child) => print(child, inner, conditional)).join("");
|
|
1760
|
+
if (children !== "") return `${text.slice(node.start, node.bodyStart - 1)}{${children}}`;
|
|
1761
|
+
return inner === void 0 || known ? "" : `@layer ${node.prelude.trim()};`;
|
|
1762
|
+
}
|
|
1763
|
+
case "keyframes":
|
|
1764
|
+
case "-webkit-keyframes": return plan.keyframes.has(node) ? source : "";
|
|
1765
|
+
case "property": return plan.properties.has(node.prelude.trim()) ? source : "";
|
|
1766
|
+
default: {
|
|
1767
|
+
if (!node.children) return source;
|
|
1768
|
+
const children = node.children.map((child) => print(child, layer, true)).join("");
|
|
1769
|
+
return children === "" ? "" : `${text.slice(node.start, node.bodyStart - 1)}{${children}}`;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
let out = "";
|
|
1774
|
+
for (const node of sheet.nodes) {
|
|
1775
|
+
const printed = print(node, void 0, false);
|
|
1776
|
+
if (printed === "") continue;
|
|
1777
|
+
parts[partOf(node)] += Buffer.byteLength(printed);
|
|
1778
|
+
out += printed;
|
|
1779
|
+
}
|
|
1780
|
+
return out;
|
|
1781
|
+
}
|
|
1782
|
+
/** What a top-level node's bytes are for. */
|
|
1783
|
+
function partOf(node) {
|
|
1784
|
+
if (node.type === "layer" && !node.children) return "layers";
|
|
1785
|
+
if (node.type === "property") return "tokens";
|
|
1786
|
+
if (node.type === "font-face") return "fonts";
|
|
1787
|
+
if (node.type === "keyframes" || node.type === "-webkit-keyframes") return "keyframes";
|
|
1788
|
+
const layer = firstLayer(node, void 0);
|
|
1789
|
+
if (layer === "vicss.tokens") return "tokens";
|
|
1790
|
+
if (layer === "vicss.global" || layer === "vicss.reset") return "global";
|
|
1791
|
+
if (layer?.startsWith("vicss.a.")) return "atoms";
|
|
1792
|
+
return isVicssLayer(layer) ? "global" : "other";
|
|
1793
|
+
}
|
|
1794
|
+
function firstLayer(node, outer) {
|
|
1795
|
+
if (node.type === "layer" && node.children) return layerOf(node, outer) ?? outer;
|
|
1796
|
+
for (const child of node.children ?? []) {
|
|
1797
|
+
const layer = firstLayer(child, outer);
|
|
1798
|
+
if (layer !== void 0) return layer;
|
|
1799
|
+
}
|
|
1800
|
+
return outer;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Relative `url()`s and `@import`s resolve from the stylesheet's URL;
|
|
1804
|
+
* inline, they would resolve from the page's (a `base: "./"` build writes
|
|
1805
|
+
* them). They are rebased on the stylesheet's URL as the page's link names
|
|
1806
|
+
* it: relative to the page when the link is, else absolute, the same on
|
|
1807
|
+
* every path that serves the page (a 404 page, an SPA's fallback) and on
|
|
1808
|
+
* the stylesheet's host (a CDN).
|
|
1809
|
+
*/
|
|
1810
|
+
function rebaseUrls(css, href) {
|
|
1811
|
+
const sheet = href.trim().replace(/[?#].*$/s, "");
|
|
1812
|
+
const from = path.posix.dirname(sheet);
|
|
1813
|
+
const host = /^(?:[a-z][a-z\d+.-]*:)?\/\/[^/]*/i.exec(sheet)?.[0];
|
|
1814
|
+
if (host === void 0 && (from === "." || from === "")) return css;
|
|
1815
|
+
const rebase = (url) => {
|
|
1816
|
+
if (url === "" || /^(?:[a-z][a-z\d+.-]*:|\/|#)/i.test(url)) return url;
|
|
1817
|
+
const suffix = /[?#].*$/s.exec(url)?.[0] ?? "";
|
|
1818
|
+
const target = url.slice(0, url.length - suffix.length);
|
|
1819
|
+
if (host === void 0) return path.posix.join(from, target) + suffix;
|
|
1820
|
+
return host + path.posix.join(from.slice(host.length) || "/", target) + suffix;
|
|
1821
|
+
};
|
|
1822
|
+
return css.replace(/(\burl\(\s*)(?:"([^"]*)"|'([^']*)'|([^)\s"']*))(\s*\))/gi, (_, open, double, single, bare, close) => {
|
|
1823
|
+
if (double !== void 0) return `${open}"${rebase(double)}"${close}`;
|
|
1824
|
+
if (single !== void 0) return `${open}'${rebase(single)}'${close}`;
|
|
1825
|
+
return `${open}${rebase(bare ?? "")}${close}`;
|
|
1826
|
+
}).replace(/(@import\s+)(["'])([^"']*)\2/gi, (_, at, quote, url) => {
|
|
1827
|
+
return `${at}${quote}${rebase(url)}${quote}`;
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* `</style` would end the element early; `\/` is `/` to CSS, in strings and
|
|
1832
|
+
* `url()`s alike. The HTML parser turns CR LF and CR into LF before the
|
|
1833
|
+
* browser hashes the style for its CSP, and so does this.
|
|
1834
|
+
*/
|
|
1835
|
+
function escapeStyle(css) {
|
|
1836
|
+
return css.replace(/<\/(style)/gi, "<\\/$1").replace(/\r\n?/g, "\n");
|
|
1837
|
+
}
|
|
1838
|
+
//#endregion
|
|
1839
|
+
//#region src/vite/libraries.ts
|
|
1840
|
+
/**
|
|
1841
|
+
* Component libraries an app installs (PLAN §5.13): the root `package.json`'s
|
|
1842
|
+
* dependencies whose own `package.json` has a `"vicss"` field naming their
|
|
1843
|
+
* manifest, and the libraries those depend on in turn. Their atoms join the
|
|
1844
|
+
* app's sheet, printed with the app's tokens and conditions; no file under
|
|
1845
|
+
* `node_modules` is scanned or compiled.
|
|
1846
|
+
*/
|
|
1847
|
+
/**
|
|
1848
|
+
* The libraries installed for the app: the root `package.json`'s
|
|
1849
|
+
* dependencies with a `vicss` field, then the dependencies and peers of
|
|
1850
|
+
* each library that have one (a forms library on a design system's
|
|
1851
|
+
* components), resolved from that library's directory, and so on. A
|
|
1852
|
+
* library's classes include those of the libraries it renders, which ship
|
|
1853
|
+
* no CSS without their manifests. Each installed copy once, by real path.
|
|
1854
|
+
*/
|
|
1855
|
+
function findLibraries(root) {
|
|
1856
|
+
const pkg = readJson(path.join(root, "package.json"));
|
|
1857
|
+
if (!pkg) return [];
|
|
1858
|
+
const found = /* @__PURE__ */ new Map();
|
|
1859
|
+
const sorted = (names) => [...new Set(names)].sort(compareStrings);
|
|
1860
|
+
let level = sorted([...keys(pkg.dependencies), ...keys(pkg.devDependencies)]).map((name) => ({
|
|
1861
|
+
name,
|
|
1862
|
+
from: root
|
|
1863
|
+
}));
|
|
1864
|
+
while (level.length > 0) {
|
|
1865
|
+
const next = [];
|
|
1866
|
+
for (const { name, from } of level) {
|
|
1867
|
+
const pkgFile = packageJsonOf(name, from);
|
|
1868
|
+
const own = pkgFile && readJson(pkgFile);
|
|
1869
|
+
if (!own || typeof own.vicss !== "string") continue;
|
|
1870
|
+
const dir = normalizePath(path.dirname(pkgFile));
|
|
1871
|
+
const real = normalizePath(realpath(dir));
|
|
1872
|
+
if (found.has(real)) continue;
|
|
1873
|
+
found.set(real, {
|
|
1874
|
+
name,
|
|
1875
|
+
packageFile: `${real}/package.json`,
|
|
1876
|
+
manifest: normalizePath(path.resolve(real, own.vicss)),
|
|
1877
|
+
dirs: [.../* @__PURE__ */ new Set([dir, real])],
|
|
1878
|
+
field: own.vicss
|
|
1879
|
+
});
|
|
1880
|
+
for (const dep of sorted([...keys(own.dependencies), ...keys(own.peerDependencies)])) next.push({
|
|
1881
|
+
name: dep,
|
|
1882
|
+
from: real
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
level = next;
|
|
1886
|
+
}
|
|
1887
|
+
return [...found.values()];
|
|
1888
|
+
}
|
|
1889
|
+
/** The installed libraries with a manifest, each checked against `system`. */
|
|
1890
|
+
function loadLibraries(root, system) {
|
|
1891
|
+
return findLibraries(root).map(({ name, packageFile, manifest: file, dirs, field }) => {
|
|
1892
|
+
let data;
|
|
1893
|
+
try {
|
|
1894
|
+
data = JSON.parse(readFileSync(file, "utf8"));
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
throw new VicssError("BAD_MANIFEST", `${name}'s vicss manifest ${field} cannot be read: ${error instanceof Error ? error.message : String(error)}`, { hint: "Build the library (its `vicss` field names the manifest its build writes)." });
|
|
1897
|
+
}
|
|
1898
|
+
const manifest = parseManifest(data, name);
|
|
1899
|
+
const styles = applyManifest(manifest, system, name, { privateAccess: dirs.some((dir) => hasPrivateAccess(system, root, path.posix.join(dir, field))) });
|
|
1900
|
+
const renames = customPropertyRenames(manifest, system);
|
|
1901
|
+
return {
|
|
1902
|
+
name,
|
|
1903
|
+
files: [packageFile, file],
|
|
1904
|
+
dirs,
|
|
1905
|
+
...styles,
|
|
1906
|
+
renames
|
|
1907
|
+
};
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* A library module's code with the custom properties its JS names renamed
|
|
1912
|
+
* to the app's (`renames`), and the map of the edit; undefined when it
|
|
1913
|
+
* names none. Only the names the library's manifest lists: a string the
|
|
1914
|
+
* library wrote for another purpose stays as it is.
|
|
1915
|
+
*/
|
|
1916
|
+
function renameCustomProperties(code, id, renames) {
|
|
1917
|
+
const exact = [...renames.exact.keys()];
|
|
1918
|
+
const prefixes = [...renames.prefixes.keys()];
|
|
1919
|
+
if (exact.length === 0 && prefixes.length === 0) return void 0;
|
|
1920
|
+
const pattern = new RegExp([...exact.sort((a, b) => b.length - a.length).map(escapeRegExp), ...prefixes.map((prefix) => `${escapeRegExp(prefix)}(?=["'\`$])`)].join("|"), "g");
|
|
1921
|
+
const edits = new SourceEdits(code);
|
|
1922
|
+
for (const match of code.matchAll(pattern)) {
|
|
1923
|
+
const to = renames.exact.get(match[0]) ?? renames.prefixes.get(match[0]);
|
|
1924
|
+
edits.overwrite(match.index, match.index + match[0].length, to);
|
|
1925
|
+
}
|
|
1926
|
+
if (!edits.hasChanged()) return void 0;
|
|
1927
|
+
return {
|
|
1928
|
+
code: edits.toString(),
|
|
1929
|
+
map: edits.generateMap({
|
|
1930
|
+
source: id,
|
|
1931
|
+
file: id,
|
|
1932
|
+
hires: true
|
|
1933
|
+
})
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* The name in the nearest `package.json` at or above `root`: the package a
|
|
1938
|
+
* project's modules belong to, which marker ids hash and a library's
|
|
1939
|
+
* manifest names.
|
|
1940
|
+
*/
|
|
1941
|
+
function ownPackageName(root) {
|
|
1942
|
+
for (let dir = root;; dir = path.dirname(dir)) {
|
|
1943
|
+
const pkg = readJson(path.join(dir, "package.json"));
|
|
1944
|
+
if (pkg) return typeof pkg.name === "string" ? pkg.name : "";
|
|
1945
|
+
if (path.dirname(dir) === dir) return "";
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
/** The scan's styles and the libraries', one entry per class and rule id. */
|
|
1949
|
+
function withLibraries(scan, libraries, system) {
|
|
1950
|
+
if (libraries.length === 0) return scan;
|
|
1951
|
+
const atoms = new Map(scan.atoms.map((atom) => [atom.className, atom]));
|
|
1952
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1953
|
+
for (const library of libraries) for (const atom of library.atoms) if (!atoms.has(atom.className)) {
|
|
1954
|
+
atoms.set(atom.className, atom);
|
|
1955
|
+
owners.set(atom.className, library.name);
|
|
1956
|
+
}
|
|
1957
|
+
const all = [...atoms.values()].sort((a, b) => compareStrings(a.className, b.className));
|
|
1958
|
+
const everyRule = [...scan.rules, ...libraries.flatMap((library) => library.rules)];
|
|
1959
|
+
const [conflict] = [...checkAtoms([
|
|
1960
|
+
...scan.atoms,
|
|
1961
|
+
...libraries.flatMap((library) => library.atoms),
|
|
1962
|
+
...baseAtoms(everyRule)
|
|
1963
|
+
], system), ...checkBases(everyRule)];
|
|
1964
|
+
if (conflict) {
|
|
1965
|
+
const named = [...new Set(owners.values())].join(", ");
|
|
1966
|
+
throw new VicssError(conflict.code, `${conflict.message} (between the app and ${named})`, conflict.hint === void 0 ? {} : { hint: conflict.hint });
|
|
1967
|
+
}
|
|
1968
|
+
const rules = new Map(scan.rules.map((rule) => [rule.id, rule]));
|
|
1969
|
+
for (const rule of libraries.flatMap((library) => library.rules)) if (!rules.has(rule.id)) rules.set(rule.id, rule);
|
|
1970
|
+
return {
|
|
1971
|
+
...scan,
|
|
1972
|
+
atoms: all,
|
|
1973
|
+
rules: [...rules.values()].sort((a, b) => compareStrings(a.id, b.id)),
|
|
1974
|
+
tokens: [...scan.tokens, ...libraries.flatMap((library) => library.tokens)]
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
/** `<name>/package.json` as Node resolves it from `from`, or under a `node_modules` above it. */
|
|
1978
|
+
function packageJsonOf(name, from) {
|
|
1979
|
+
try {
|
|
1980
|
+
return createRequire(path.join(from, "package.json")).resolve(`${name}/package.json`);
|
|
1981
|
+
} catch {}
|
|
1982
|
+
for (let dir = from;; dir = path.dirname(dir)) {
|
|
1983
|
+
const file = path.join(dir, "node_modules", name, "package.json");
|
|
1984
|
+
if (existsSync(file)) return file;
|
|
1985
|
+
if (path.dirname(dir) === dir) return void 0;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
/** The library `id` (without a query) is a module of, not of a package it installs. */
|
|
1989
|
+
function libraryOf(libraries, id) {
|
|
1990
|
+
return libraries.find((library) => library.dirs.some((dir) => id.startsWith(`${dir}/`) && !id.slice(dir.length).includes("/node_modules/")));
|
|
1991
|
+
}
|
|
1992
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1993
|
+
function realpath(dir) {
|
|
1994
|
+
try {
|
|
1995
|
+
return realpathSync(dir);
|
|
1996
|
+
} catch {
|
|
1997
|
+
return dir;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
function readJson(file) {
|
|
2001
|
+
try {
|
|
2002
|
+
const data = JSON.parse(readFileSync(file, "utf8"));
|
|
2003
|
+
return typeof data === "object" && data !== null && !Array.isArray(data) ? data : void 0;
|
|
2004
|
+
} catch {
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
function keys(value) {
|
|
2009
|
+
return typeof value === "object" && value !== null ? Object.keys(value) : [];
|
|
2010
|
+
}
|
|
2011
|
+
//#endregion
|
|
2012
|
+
//#region src/vite/glob.ts
|
|
2013
|
+
/**
|
|
2014
|
+
* Listing `include` without `exclude` for the pre-scan, fast. `fs.glob` and
|
|
2015
|
+
* Vite's `createFilter` (which compiles its patterns again on every call)
|
|
2016
|
+
* took a tenth of a cold scan; this compiles each pattern once to a regular
|
|
2017
|
+
* expression and walks the directories the patterns can reach. It keeps
|
|
2018
|
+
* their semantics: `include` matches like `fs.glob` (wildcards skip dot
|
|
2019
|
+
* files), `exclude` like `createFilter` (wildcards match them; a pattern is
|
|
2020
|
+
* relative to the root unless it is absolute or starts with `**`). Patterns
|
|
2021
|
+
* it does not compile (extglobs, ranges, negations) take the slow path.
|
|
2022
|
+
*/
|
|
2023
|
+
/** Absolute, forward-slash paths of the files `include` matches and `exclude` does not, sorted. */
|
|
2024
|
+
async function listFiles$1(root, include, exclude) {
|
|
2025
|
+
return listFast(root, include, exclude) ?? listWithGlob(root, include, exclude);
|
|
2026
|
+
}
|
|
2027
|
+
/** The reference: `fs.glob` for `include`, Vite's `createFilter` for `exclude`. */
|
|
2028
|
+
async function listWithGlob(root, include, exclude) {
|
|
2029
|
+
const kept = createFilter(null, exclude, { resolve: root });
|
|
2030
|
+
const files = /* @__PURE__ */ new Set();
|
|
2031
|
+
const skip = (entry) => path.basename(typeof entry === "string" ? entry : entry.name) === "node_modules";
|
|
2032
|
+
for await (const file of glob(include, {
|
|
2033
|
+
cwd: root,
|
|
2034
|
+
exclude: skip
|
|
2035
|
+
})) {
|
|
2036
|
+
const id = normalizePath(path.resolve(root, file));
|
|
2037
|
+
if (kept(id)) files.add(id);
|
|
2038
|
+
}
|
|
2039
|
+
return [...files].sort(compareStrings);
|
|
2040
|
+
}
|
|
2041
|
+
function listFast(root, include, exclude) {
|
|
2042
|
+
const base = normalizePath(root).replace(/\/$/, "");
|
|
2043
|
+
const includes = [];
|
|
2044
|
+
for (const pattern of include) {
|
|
2045
|
+
const clean = normalizePath(pattern).replace(/^\.\//, "");
|
|
2046
|
+
if (path.isAbsolute(clean)) return void 0;
|
|
2047
|
+
const dir = staticPrefix(clean);
|
|
2048
|
+
if (hasDotSegment(clean.slice(dir.length))) return void 0;
|
|
2049
|
+
const regex = globToRegExp(clean, false);
|
|
2050
|
+
if (!regex) return void 0;
|
|
2051
|
+
includes.push({
|
|
2052
|
+
dir,
|
|
2053
|
+
test: regex
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
const outside = includes.some(({ dir }) => hasDotSegment(dir));
|
|
2057
|
+
const excludes = [];
|
|
2058
|
+
for (const pattern of exclude) {
|
|
2059
|
+
const clean = normalizePath(pattern);
|
|
2060
|
+
const on = path.isAbsolute(clean) ? "absolute" : clean.startsWith("**") ? "bare" : "relative";
|
|
2061
|
+
const regex = globToRegExp(on === "relative" ? path.posix.normalize(clean) : clean, true);
|
|
2062
|
+
if (!regex) return void 0;
|
|
2063
|
+
excludes.push({
|
|
2064
|
+
regex,
|
|
2065
|
+
on
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
const dotDirs = include.some((p) => p.split("/").some((segment) => segment.startsWith(".") && !isDotSegment(segment)));
|
|
2069
|
+
const files = /* @__PURE__ */ new Set();
|
|
2070
|
+
const walked = /* @__PURE__ */ new Set();
|
|
2071
|
+
const walk = (rel) => {
|
|
2072
|
+
if (walked.has(rel)) return;
|
|
2073
|
+
walked.add(rel);
|
|
2074
|
+
let entries;
|
|
2075
|
+
try {
|
|
2076
|
+
entries = readdirSync(rel ? `${base}/${rel}` : base, { withFileTypes: true });
|
|
2077
|
+
} catch {
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
for (const entry of entries) {
|
|
2081
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
2082
|
+
if (entry.isDirectory()) {
|
|
2083
|
+
if (entry.name !== "node_modules" && (dotDirs || !entry.name.startsWith("."))) walk(child);
|
|
2084
|
+
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
2085
|
+
if (!includes.some((p) => p.test.test(child))) continue;
|
|
2086
|
+
const id = outside ? path.posix.normalize(`${base}/${child}`) : `${base}/${child}`;
|
|
2087
|
+
const relative = outside ? path.posix.relative(base, id) : child;
|
|
2088
|
+
const bare = id.replace(/^\//, "");
|
|
2089
|
+
if (!excludes.some(({ regex, on }) => regex.test(on === "absolute" ? id : on === "bare" ? bare : relative))) files.add(id);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
for (const { dir } of includes) walk(dir);
|
|
2094
|
+
return [...files].sort(compareStrings);
|
|
2095
|
+
}
|
|
2096
|
+
const isDotSegment = (segment) => segment === "." || segment === "..";
|
|
2097
|
+
const hasDotSegment = (pattern) => pattern.split("/").some(isDotSegment);
|
|
2098
|
+
/** The directories before the first segment with a wildcard: where a walk starts. */
|
|
2099
|
+
function staticPrefix(pattern) {
|
|
2100
|
+
const segments = pattern.split("/");
|
|
2101
|
+
const fixed = [];
|
|
2102
|
+
for (const segment of segments.slice(0, -1)) {
|
|
2103
|
+
if (/[*?[{\\]/.test(segment)) break;
|
|
2104
|
+
fixed.push(segment);
|
|
2105
|
+
}
|
|
2106
|
+
return fixed.join("/");
|
|
2107
|
+
}
|
|
2108
|
+
//#endregion
|
|
2109
|
+
//#region src/vite/scan.ts
|
|
2110
|
+
/**
|
|
2111
|
+
* The build pre-scan (PLAN §5.10): every file under `include` is compiled
|
|
2112
|
+
* before the bundle exists, so the one sheet holds every atom and rule the
|
|
2113
|
+
* app can use, whatever order environments and modules are built in. A
|
|
2114
|
+
* framework file (`.vue`, `.svelte`, `.astro`) is compiled by its scripts,
|
|
2115
|
+
* extracted from its source; with more than `POOL_MIN` modules to compile,
|
|
2116
|
+
* worker threads share the work (D17).
|
|
2117
|
+
*/
|
|
2118
|
+
const DEFAULT_INCLUDE = ["src/**/*.{js,jsx,ts,tsx,mjs,mts,cjs,cts,vue,svelte,astro}", "app/**/*.{js,jsx,ts,tsx,mjs,mts,cjs,cts,vue,svelte,astro}"];
|
|
2119
|
+
const DEFAULT_EXCLUDE = [
|
|
2120
|
+
"**/*.{test,spec,stories}.*",
|
|
2121
|
+
"**/node_modules/**",
|
|
2122
|
+
"**/dist/**"
|
|
2123
|
+
];
|
|
2124
|
+
/** Files compiled at once: enough to overlap reads with parsing, few enough to bound memory. */
|
|
2125
|
+
const CONCURRENCY = 16;
|
|
2126
|
+
/** The main thread's share once a worker thread is ready: it also answers their resolutions. */
|
|
2127
|
+
const CONCURRENCY_WITH_POOL = 1;
|
|
2128
|
+
const DECLARATION = /\.d\.[cm]?ts$/;
|
|
2129
|
+
/** A conflict between the atoms of two files, which carry no location. */
|
|
2130
|
+
var ScanConflict = class extends VicssError {
|
|
2131
|
+
/** The later of the files, which the build reports the error on. */
|
|
2132
|
+
id;
|
|
2133
|
+
constructor(error, id, message) {
|
|
2134
|
+
super(error.code, message, error.hint === void 0 ? {} : { hint: error.hint });
|
|
2135
|
+
this.id = id;
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
async function listFiles(root, include = DEFAULT_INCLUDE, exclude = DEFAULT_EXCLUDE) {
|
|
2139
|
+
return (await listFiles$1(root, include, exclude)).filter((id) => !DECLARATION.test(id));
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* About how many of `files` name one of `sources`, read from an even sample
|
|
2143
|
+
* of them: whether a build starts scan threads before its config, and so
|
|
2144
|
+
* its `include` and import sources, has loaded (pool.ts).
|
|
2145
|
+
*/
|
|
2146
|
+
function estimateStyled(files, sources, sample = 100) {
|
|
2147
|
+
const read = Math.min(sample, files.length);
|
|
2148
|
+
let styled = 0;
|
|
2149
|
+
for (let k = 0; k < read; k++) {
|
|
2150
|
+
const i = read === files.length ? k : Math.floor(k * GOLDEN % 1 * files.length);
|
|
2151
|
+
try {
|
|
2152
|
+
const code = readFileSync(files[i], "utf8");
|
|
2153
|
+
if (sources.some((source) => code.includes(source))) styled++;
|
|
2154
|
+
} catch {}
|
|
2155
|
+
}
|
|
2156
|
+
return read === 0 ? 0 : Math.round(styled / read * files.length);
|
|
2157
|
+
}
|
|
2158
|
+
const GOLDEN = (Math.sqrt(5) - 1) / 2;
|
|
2159
|
+
async function scan(options) {
|
|
2160
|
+
const { root, system, cache } = options;
|
|
2161
|
+
const config = system.config;
|
|
2162
|
+
const files = await listFiles(root, config.include, config.exclude);
|
|
2163
|
+
const sources = ["@menapo/vicss", ...options.importSources];
|
|
2164
|
+
const depHashes = /* @__PURE__ */ new Map();
|
|
2165
|
+
const depHash = (dep) => {
|
|
2166
|
+
if (!depHashes.has(dep)) depHashes.set(dep, readHash(dep));
|
|
2167
|
+
return depHashes.get(dep);
|
|
2168
|
+
};
|
|
2169
|
+
const results = Array.from({ length: files.length });
|
|
2170
|
+
const failures = [];
|
|
2171
|
+
const work = [];
|
|
2172
|
+
for (const [index, id] of files.entries()) try {
|
|
2173
|
+
const code = readFileSync(id, "utf8");
|
|
2174
|
+
if (!sources.some((source) => code.includes(source))) continue;
|
|
2175
|
+
const key = cache.key(code, options.configHash);
|
|
2176
|
+
const entry = await cache.lookup(id, key, depHash, options.resolve);
|
|
2177
|
+
if (entry) {
|
|
2178
|
+
if (!isSfc(id)) options.cached?.(id, code, entry);
|
|
2179
|
+
results[index] = entry;
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
const jobs = isSfc(id) ? extractScripts(id, code).map(({ code: text, ...script }) => ({
|
|
2183
|
+
id,
|
|
2184
|
+
code: text,
|
|
2185
|
+
script
|
|
2186
|
+
})) : [{
|
|
2187
|
+
id,
|
|
2188
|
+
code
|
|
2189
|
+
}];
|
|
2190
|
+
work.push({
|
|
2191
|
+
index,
|
|
2192
|
+
id,
|
|
2193
|
+
code,
|
|
2194
|
+
key,
|
|
2195
|
+
jobs,
|
|
2196
|
+
results: [],
|
|
2197
|
+
left: jobs.length
|
|
2198
|
+
});
|
|
2199
|
+
} catch (error) {
|
|
2200
|
+
failures.push({
|
|
2201
|
+
index,
|
|
2202
|
+
error
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
const complete = (item) => {
|
|
2206
|
+
const parts = item.results;
|
|
2207
|
+
const deps = {};
|
|
2208
|
+
for (const dep of parts.flatMap((part) => part.dependencies)) {
|
|
2209
|
+
const hash = depHash(dep);
|
|
2210
|
+
if (hash !== void 0) deps[dep] = hash;
|
|
2211
|
+
}
|
|
2212
|
+
const sfc = isSfc(item.id);
|
|
2213
|
+
const classes = [...new Set(parts.flatMap((part) => [...placeholdersIn(part.code), ...(part.names ?? []).map((name) => "!" + name)]))].sort(compareStrings);
|
|
2214
|
+
const entry = !sfc && parts.length === 1 ? {
|
|
2215
|
+
key: item.key,
|
|
2216
|
+
deps,
|
|
2217
|
+
resolutions: parts[0].resolutions,
|
|
2218
|
+
atoms: parts[0].atoms,
|
|
2219
|
+
tokens: parts[0].tokens,
|
|
2220
|
+
rules: parts[0].rules,
|
|
2221
|
+
arbitrary: parts[0].arbitrary,
|
|
2222
|
+
classes,
|
|
2223
|
+
output: toOutput(parts[0])
|
|
2224
|
+
} : {
|
|
2225
|
+
key: item.key,
|
|
2226
|
+
deps,
|
|
2227
|
+
resolutions: Object.assign({}, ...parts.map((part) => part.resolutions)),
|
|
2228
|
+
atoms: uniqueAtoms(parts.flatMap((part) => part.atoms)),
|
|
2229
|
+
tokens: uniqueBy(parts.flatMap((part) => part.tokens), (ref) => `${ref.scale}:${ref.path}`),
|
|
2230
|
+
rules: uniqueBy(parts.flatMap((part) => part.rules), (rule) => rule.id),
|
|
2231
|
+
arbitrary: parts.reduce((sum, part) => sum + part.arbitrary, 0),
|
|
2232
|
+
classes,
|
|
2233
|
+
output: {
|
|
2234
|
+
code: null,
|
|
2235
|
+
map: null,
|
|
2236
|
+
warnings: []
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
cache.set(item.id, entry);
|
|
2240
|
+
results[item.index] = entry;
|
|
2241
|
+
if (!sfc) item.deferred?.resolve(parts[0]);
|
|
2242
|
+
};
|
|
2243
|
+
const queue = work.flatMap((item) => item.jobs.map((job, unit) => ({
|
|
2244
|
+
item,
|
|
2245
|
+
job,
|
|
2246
|
+
unit,
|
|
2247
|
+
taken: false
|
|
2248
|
+
})));
|
|
2249
|
+
for (const item of work) if (item.left === 0) complete(item);
|
|
2250
|
+
const urgent = [];
|
|
2251
|
+
let next = 0;
|
|
2252
|
+
let stopAt = Infinity;
|
|
2253
|
+
const take = () => {
|
|
2254
|
+
for (let task = urgent.pop(); task; task = urgent.pop()) if (!task.taken && task.item.index < stopAt) return task.taken = true, task;
|
|
2255
|
+
for (let task = queue[next]; task; task = queue[++next]) {
|
|
2256
|
+
if (task.item.index >= stopAt) return void 0;
|
|
2257
|
+
if (!task.taken) return task.taken = true, task;
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
2260
|
+
const failed = (task, error) => {
|
|
2261
|
+
failures.push({
|
|
2262
|
+
index: task.item.index,
|
|
2263
|
+
error
|
|
2264
|
+
});
|
|
2265
|
+
if (!options.keepGoing) stopAt = Math.min(stopAt, task.item.index);
|
|
2266
|
+
task.item.deferred?.reject(error);
|
|
2267
|
+
};
|
|
2268
|
+
const settle = (task, result) => {
|
|
2269
|
+
task.item.results[task.unit] = result;
|
|
2270
|
+
if (--task.item.left === 0) complete(task.item);
|
|
2271
|
+
};
|
|
2272
|
+
for (const task of queue) {
|
|
2273
|
+
const { item } = task;
|
|
2274
|
+
if (isSfc(item.id)) continue;
|
|
2275
|
+
item.deferred = defer();
|
|
2276
|
+
options.queued?.(item.id, item.code, item.deferred.promise, () => {
|
|
2277
|
+
if (!task.taken) urgent.push(task);
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
options.allQueued?.();
|
|
2281
|
+
const local = (task) => options.compile(task.job.id, task.job.code, task.job.script);
|
|
2282
|
+
const pool = queue.length > 200 ? options.pool?.(queue.length) : void 0;
|
|
2283
|
+
/** Workers that built their compiler; until one has, this thread compiles at full width. */
|
|
2284
|
+
let ready = 0;
|
|
2285
|
+
let localRunners = 0;
|
|
2286
|
+
const width = () => ready > 0 ? CONCURRENCY_WITH_POOL : CONCURRENCY;
|
|
2287
|
+
const runLocal = async () => {
|
|
2288
|
+
localRunners++;
|
|
2289
|
+
try {
|
|
2290
|
+
while (!(pool && localRunners > width())) {
|
|
2291
|
+
const task = take();
|
|
2292
|
+
if (!task) return;
|
|
2293
|
+
try {
|
|
2294
|
+
settle(task, await local(task));
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
failed(task, error);
|
|
2297
|
+
}
|
|
2298
|
+
if (pool) await new Promise((resolve) => setImmediate(resolve));
|
|
2299
|
+
}
|
|
2300
|
+
} finally {
|
|
2301
|
+
localRunners--;
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
/** Local runners up to the width: no worker can take a share. */
|
|
2305
|
+
const topUp = () => Array.from({ length: Math.max(0, width() - localRunners) }, () => runLocal());
|
|
2306
|
+
const runRemote = async (worker) => {
|
|
2307
|
+
for (let task = take(); task; task = take()) try {
|
|
2308
|
+
settle(task, await worker.compile(task.job));
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
if (!(error instanceof WorkerLost)) {
|
|
2311
|
+
failed(task, error);
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
try {
|
|
2315
|
+
settle(task, await local(task));
|
|
2316
|
+
} catch (inner) {
|
|
2317
|
+
failed(task, inner);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
try {
|
|
2322
|
+
const runners = [];
|
|
2323
|
+
for (let i = 0; i < Math.min(CONCURRENCY, queue.length); i++) runners.push(runLocal());
|
|
2324
|
+
for (const worker of pool?.workers ?? []) runners.push(worker.ready.then((ok) => {
|
|
2325
|
+
if (!ok) return Promise.all(topUp());
|
|
2326
|
+
ready++;
|
|
2327
|
+
return Promise.all(Array.from({ length: worker.capacity }, () => runRemote(worker)));
|
|
2328
|
+
}));
|
|
2329
|
+
await Promise.all(runners);
|
|
2330
|
+
} finally {
|
|
2331
|
+
await pool?.close();
|
|
2332
|
+
const error = failures.sort((a, b) => a.index - b.index)[0]?.error;
|
|
2333
|
+
for (const item of work) item.deferred?.reject(error ?? /* @__PURE__ */ new Error("vicss: the scan stopped"));
|
|
2334
|
+
}
|
|
2335
|
+
failures.sort((a, b) => a.index - b.index);
|
|
2336
|
+
const [failure] = failures;
|
|
2337
|
+
if (failure && !options.keepGoing) throw failure.error;
|
|
2338
|
+
const all = results.flatMap((entry) => entry?.atoms ?? []);
|
|
2339
|
+
const conflicts = conflictsOf(all, results.flatMap((entry) => entry?.rules ?? []), system);
|
|
2340
|
+
const located = [];
|
|
2341
|
+
if (conflicts.length > 0) {
|
|
2342
|
+
const styled = files.flatMap((id, i) => results[i] ? [{
|
|
2343
|
+
id,
|
|
2344
|
+
atoms: results[i].atoms,
|
|
2345
|
+
rules: results[i].rules
|
|
2346
|
+
}] : []);
|
|
2347
|
+
for (const conflict of options.keepGoing ? conflicts : conflicts.slice(0, 1)) {
|
|
2348
|
+
const error = locate(conflict, styled, root, system);
|
|
2349
|
+
if (!options.keepGoing) throw error;
|
|
2350
|
+
located.push(error);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
const atoms = uniqueAtoms(all);
|
|
2354
|
+
const refs = /* @__PURE__ */ new Map();
|
|
2355
|
+
for (const ref of results.flatMap((entry) => entry?.tokens ?? [])) refs.set(`${ref.scale}:${ref.path}`, ref);
|
|
2356
|
+
const tokens = [...refs.entries()].sort(([a], [b]) => compareStrings(a, b)).map(([, r]) => r);
|
|
2357
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2358
|
+
for (const rule of results.flatMap((entry) => entry?.rules ?? [])) if (!byId.has(rule.id)) byId.set(rule.id, rule);
|
|
2359
|
+
return {
|
|
2360
|
+
files,
|
|
2361
|
+
atoms,
|
|
2362
|
+
tokens,
|
|
2363
|
+
rules: [...byId.values()].sort((a, b) => compareStrings(a.id, b.id)),
|
|
2364
|
+
classes: new Set(atoms.map((atom) => atom.className)),
|
|
2365
|
+
ruleIds: new Set(byId.keys()),
|
|
2366
|
+
names: new Set(results.flatMap((entry) => entry?.classes ?? [])),
|
|
2367
|
+
arbitrary: results.reduce((sum, entry) => sum + (entry?.arbitrary ?? 0), 0),
|
|
2368
|
+
failures: [...failures.map(({ index, error }) => ({
|
|
2369
|
+
id: files[index],
|
|
2370
|
+
error
|
|
2371
|
+
})), ...located.map((error) => ({
|
|
2372
|
+
id: error.id,
|
|
2373
|
+
error
|
|
2374
|
+
}))]
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
function defer() {
|
|
2378
|
+
let resolve;
|
|
2379
|
+
let reject;
|
|
2380
|
+
const promise = new Promise((res, rej) => (resolve = res, reject = rej));
|
|
2381
|
+
promise.catch(() => {});
|
|
2382
|
+
return {
|
|
2383
|
+
promise,
|
|
2384
|
+
resolve,
|
|
2385
|
+
reject
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
/** One atom per class, sorted by class (`keepAtom`: a style's palette over a runtime value's twin). */
|
|
2389
|
+
function uniqueAtoms(atoms) {
|
|
2390
|
+
const out = /* @__PURE__ */ new Map();
|
|
2391
|
+
for (const atom of atoms) keepAtom(out, atom);
|
|
2392
|
+
return [...out.values()].sort((a, b) => compareStrings(a.className, b.className));
|
|
2393
|
+
}
|
|
2394
|
+
/** One item per key, the first kept, sorted by key. */
|
|
2395
|
+
function uniqueBy(items, keyOf) {
|
|
2396
|
+
const out = /* @__PURE__ */ new Map();
|
|
2397
|
+
for (const item of items) if (!out.has(keyOf(item))) out.set(keyOf(item), item);
|
|
2398
|
+
return [...out.entries()].sort(([a], [b]) => compareStrings(a, b)).map(([, item]) => item);
|
|
2399
|
+
}
|
|
2400
|
+
/** The ordering invariant's conflicts over atoms and rules: their flattened bases' atoms too. */
|
|
2401
|
+
function conflictsOf(atoms, rules, system) {
|
|
2402
|
+
return [...checkAtoms([...atoms, ...baseAtoms(rules)], system), ...checkBases(rules)];
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Names the files behind a conflict found over every file's atoms: the first
|
|
2406
|
+
* file whose atoms, after those before it, raise it, and the first earlier
|
|
2407
|
+
* file that raises it together with that one. `checkAtoms` reports the first
|
|
2408
|
+
* conflict in atom order, so both searches are monotonic. Only the error
|
|
2409
|
+
* path pays for them.
|
|
2410
|
+
*/
|
|
2411
|
+
function locate(conflict, styled, root, system) {
|
|
2412
|
+
const raises = (files) => conflictsOf(files.flatMap((file) => file.atoms), files.flatMap((file) => file.rules), system).some((e) => e.code === conflict.code && e.message === conflict.message);
|
|
2413
|
+
const before = (n) => styled.slice(0, n);
|
|
2414
|
+
const first = (lo, hi, holds) => {
|
|
2415
|
+
while (lo < hi) {
|
|
2416
|
+
const mid = lo + hi >> 1;
|
|
2417
|
+
if (holds(mid)) hi = mid;
|
|
2418
|
+
else lo = mid + 1;
|
|
2419
|
+
}
|
|
2420
|
+
return lo;
|
|
2421
|
+
};
|
|
2422
|
+
const later = styled[first(1, styled.length, (n) => raises(before(n))) - 1];
|
|
2423
|
+
const rel = (id) => path.relative(root, id) || id;
|
|
2424
|
+
const files = [later.id];
|
|
2425
|
+
if (!raises([later])) {
|
|
2426
|
+
const at = first(1, styled.indexOf(later), (n) => raises([...before(n), later]));
|
|
2427
|
+
files.unshift(styled[at - 1].id);
|
|
2428
|
+
}
|
|
2429
|
+
const message = `${conflict.message} (${files.map(rel).join(" and ")})`;
|
|
2430
|
+
return new ScanConflict(conflict, later.id, message);
|
|
2431
|
+
}
|
|
2432
|
+
//#endregion
|
|
2433
|
+
//#region src/vite/types-file.ts
|
|
2434
|
+
/**
|
|
2435
|
+
* Where the generated `vicss.d.ts` goes and how it is written (PLAN §5.11),
|
|
2436
|
+
* shared by the plugin, which writes it whenever the config loads, and by
|
|
2437
|
+
* `vicss types` and `vicss init`. The lint snapshot (core/snapshot.ts) goes
|
|
2438
|
+
* next to it, with it: the rules find it there.
|
|
2439
|
+
*/
|
|
2440
|
+
/**
|
|
2441
|
+
* The directories a default `vicss.d.ts` goes in, the first the root has:
|
|
2442
|
+
* where a tsconfig's `include` usually looks, `app/` for Nuxt 4 and React
|
|
2443
|
+
* Router.
|
|
2444
|
+
*/
|
|
2445
|
+
const SOURCE_DIRS = ["src", "app"];
|
|
2446
|
+
/**
|
|
2447
|
+
* `output` relative to the root, else `vicss.d.ts` in the first of
|
|
2448
|
+
* `SOURCE_DIRS` the root has, else in the root.
|
|
2449
|
+
*/
|
|
2450
|
+
function typesFile(root, output) {
|
|
2451
|
+
if (output !== void 0) return normalizePath(path.resolve(root, output));
|
|
2452
|
+
const dir = SOURCE_DIRS.map((name) => path.join(root, name)).find((dir) => existsSync(dir));
|
|
2453
|
+
return normalizePath(path.join(dir ?? root, "vicss.d.ts"));
|
|
2454
|
+
}
|
|
2455
|
+
/** `vicss.lint.json` in the directory of the types file. */
|
|
2456
|
+
function snapshotFile(types) {
|
|
2457
|
+
return normalizePath(path.join(path.dirname(types), SNAPSHOT_FILE));
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* The project root as the snapshot records it (`LintSnapshot.root`):
|
|
2461
|
+
* relative to the snapshot's directory, so a committed snapshot is the same
|
|
2462
|
+
* on every machine and the rules match `privateAccess` against the root.
|
|
2463
|
+
*/
|
|
2464
|
+
function snapshotRoot(snapshot, root) {
|
|
2465
|
+
return normalizePath(path.relative(path.dirname(snapshot), root)) || ".";
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Writes `text` to `file` unless it holds it already, so an unchanged config
|
|
2469
|
+
* never touches the file (a type checker or a watcher would see an edit).
|
|
2470
|
+
* Returns whether it wrote.
|
|
2471
|
+
*/
|
|
2472
|
+
function writeIfChanged(file, text) {
|
|
2473
|
+
try {
|
|
2474
|
+
if (readFileSync(file, "utf8") === text) return false;
|
|
2475
|
+
} catch {}
|
|
2476
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
2477
|
+
writeFileSync(file, text);
|
|
2478
|
+
return true;
|
|
2479
|
+
}
|
|
2480
|
+
//#endregion
|
|
2481
|
+
export { CompileCache as C, generateTypes as D, readHash as E, createManifest as O, report as S, hashCode as T, critical as _, ScanConflict as a, hashesJson as b, scan as c, libraryOf as d, loadLibraries as f, SheetCache as g, withLibraries as h, writeIfChanged as i, codeFrame as k, escapeRegExp as l, renameCustomProperties as m, snapshotRoot as n, estimateStyled as o, ownPackageName as p, typesFile as r, listFiles as s, snapshotFile as t, findLibraries as u, criticalPage as v, compiledOf as w, hashesProblem as x, criticalSettings as y };
|