@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.
@@ -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("&", "&amp;").replaceAll("\"", "&quot;");
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 };