@menapo/vicss 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +662 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1311 -0
- package/dist/config-loader-1kWS0cRU.js +162 -0
- package/dist/config-loader-CG76KA50.js +1 -0
- package/dist/config.d.ts +821 -0
- package/dist/config.js +2690 -0
- package/dist/index.d.ts +1528 -0
- package/dist/index.js +442 -0
- package/dist/lint.d.ts +114 -0
- package/dist/lint.js +0 -0
- package/dist/pool-F3qb30Fy.js +3389 -0
- package/dist/print-B_mn7wBu.js +5127 -0
- package/dist/snapshot-DAJ8FLU4.js +149 -0
- package/dist/theme-zBcr5DHi.js +2460 -0
- package/dist/types-file-D6Iu4kzv.js +2481 -0
- package/dist/vite.d.ts +270 -0
- package/dist/vite.js +2264 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +113 -0
- package/package.json +66 -3
package/dist/vite.js
ADDED
|
@@ -0,0 +1,2264 @@
|
|
|
1
|
+
import { $ as isPaletteAtom, Ct as isArbitrary, Dt as mergeIdOf, G as minLengthOf, H as createNamer, J as placeholdersIn, K as minLengthWarning, U as hasPlaceholders, Y as renameClasses, _t as VicssError, bt as compareStrings, d as printSheet, k as layerStatement, l as printAtoms, n as chainSelectors, q as placeholder, s as outerLayers, t as baseTokens, u as printBase } from "./print-B_mn7wBu.js";
|
|
2
|
+
import { S as originalLines, _ as isSfc, d as FRAMEWORKS, f as SFC_SCRIPT_ID, h as extractScripts, i as poolSize, l as resolveFile, m as detectFramework, o as compile, p as configuredJsxSource, r as configShape, s as createParseCache, t as CompilePool, u as resolvesAlike, x as SourceEdits } from "./pool-F3qb30Fy.js";
|
|
3
|
+
import { C as createLocator, u as baseAtoms } from "./theme-zBcr5DHi.js";
|
|
4
|
+
import { C as CompileCache, D as generateTypes, E as readHash, O as createManifest, S as report, T as hashCode, _ as critical, a as ScanConflict, b as hashesJson, c as scan, d as libraryOf, f as loadLibraries, g as SheetCache, h as withLibraries, i as writeIfChanged, k as codeFrame, l as escapeRegExp, m as renameCustomProperties, n as snapshotRoot, o as estimateStyled, p as ownPackageName, r as typesFile, s as listFiles, t as snapshotFile, u as findLibraries, v as criticalPage, w as compiledOf, y as criticalSettings } from "./types-file-D6Iu4kzv.js";
|
|
5
|
+
import { r as snapshotText } from "./snapshot-DAJ8FLU4.js";
|
|
6
|
+
import { n as loadSystem, t as CONFIG_FILES } from "./config-loader-1kWS0cRU.js";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { gzipSync } from "node:zlib";
|
|
11
|
+
import { createIdResolver, isFileLoadingAllowed, normalizePath, parseSync } from "vite";
|
|
12
|
+
//#region src/core/literals.ts
|
|
13
|
+
/**
|
|
14
|
+
* Literal declarations (the plugin's `api.literals()`, email PLAN D4): each
|
|
15
|
+
* atom with its tokens resolved to the values they stand for, for a renderer
|
|
16
|
+
* that cannot read custom properties. Mail clients drop `var()` and cascade
|
|
17
|
+
* layers, so `@menapo/email` inlines these instead of linking the sheet.
|
|
18
|
+
*/
|
|
19
|
+
const VAR = /var\((--[\w-]+)\)/g;
|
|
20
|
+
const SCHEMES = /* @__PURE__ */ new Set(["dark", "light"]);
|
|
21
|
+
function tokenIndex(system) {
|
|
22
|
+
const index = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const table of system.tokens.values()) for (const token of table.values()) index.set(token.cssVar, token);
|
|
24
|
+
return index;
|
|
25
|
+
}
|
|
26
|
+
/** `value` with every token's `var()` replaced by its value in `mode` (its base value without one). */
|
|
27
|
+
function resolve(value, index, mode) {
|
|
28
|
+
return value.replace(VAR, (match, name) => {
|
|
29
|
+
const token = index.get(name);
|
|
30
|
+
if (!token) return match;
|
|
31
|
+
return resolve((mode && token.modes?.[mode]) ?? token.value, index, mode);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every class of `atoms` and of the flattened bases in `rules`, by identity,
|
|
36
|
+
* with its declarations in literal form. A flattened base is one class with
|
|
37
|
+
* many declarations; an atom is one with one.
|
|
38
|
+
*/
|
|
39
|
+
function literalsOf(atoms, rules, system) {
|
|
40
|
+
const index = tokenIndex(system);
|
|
41
|
+
const modes = [...system.modes].filter((mode) => system.conditions.get(mode)?.scope);
|
|
42
|
+
const literal = (atom) => {
|
|
43
|
+
const value = resolve(atom.value, index);
|
|
44
|
+
const switched = {};
|
|
45
|
+
let any = false;
|
|
46
|
+
for (const mode of modes) {
|
|
47
|
+
const there = resolve(atom.value, index, mode);
|
|
48
|
+
if (there !== value) {
|
|
49
|
+
switched[mode] = there;
|
|
50
|
+
any = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const conditions = atom.chain.filter((name) => !SCHEMES.has(name));
|
|
54
|
+
return {
|
|
55
|
+
property: atom.property,
|
|
56
|
+
value,
|
|
57
|
+
tier: atom.tier,
|
|
58
|
+
chain: atom.chain,
|
|
59
|
+
rules: chainSelectors(conditions, "&", system),
|
|
60
|
+
...any && { modes: switched }
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const out = /* @__PURE__ */ new Map();
|
|
64
|
+
for (const atom of atoms) if (!out.has(atom.className)) out.set(atom.className, [literal(atom)]);
|
|
65
|
+
for (const rule of rules) if (rule.kind === "base" && rule.atoms) out.set(rule.id, rule.atoms.map(literal));
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
const STRINGS = /(["'`])((?:\\.|(?!\1)[^\\\n])*)\1/g;
|
|
69
|
+
/** The classes of `known` that string literals in `code` name (dev code writes classes as they are). */
|
|
70
|
+
function classesIn(code, known) {
|
|
71
|
+
const found = /* @__PURE__ */ new Set();
|
|
72
|
+
for (const match of code.matchAll(STRINGS)) for (const word of match[2].split(/\s+/)) if (known.has(word)) found.add(word);
|
|
73
|
+
return [...found];
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/vite/css.ts
|
|
77
|
+
/**
|
|
78
|
+
* The CSS modules the plugin serves (PLAN §5.10, D13, D14) and the ids they
|
|
79
|
+
* live under. None exists on disk; `load` builds each from compiled atoms
|
|
80
|
+
* and rules.
|
|
81
|
+
* The ids end in `.css` and sit under the root, so Vite's CSS pipeline, HMR
|
|
82
|
+
* and framework CSS collection treat them as ordinary stylesheets.
|
|
83
|
+
*/
|
|
84
|
+
const BASE_SPECIFIER = "virtual:vicss/base.css";
|
|
85
|
+
const SHEET_SPECIFIER = "virtual:vicss/sheet.css";
|
|
86
|
+
const MODULE_SUFFIX = ".vicss.css";
|
|
87
|
+
/** `root` is Vite's root with forward slashes. */
|
|
88
|
+
function cssIds(root) {
|
|
89
|
+
return {
|
|
90
|
+
base: path.posix.join(root, "@vicss/base.css"),
|
|
91
|
+
sheet: path.posix.join(root, "@vicss/sheet.css"),
|
|
92
|
+
tokens: path.posix.join(root, "@vicss/tokens.css"),
|
|
93
|
+
libraries: path.posix.join(root, "@vicss/lib/")
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function splitQuery(id) {
|
|
97
|
+
const at = id.search(/[?#]/);
|
|
98
|
+
return at === -1 ? [id, ""] : [id.slice(0, at), id.slice(at)];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* `./Button.tsx.vicss.css`: what a styled module imports in dev. A framework
|
|
102
|
+
* file's script sub-module (`Card.astro?astro&type=script…`) imports its
|
|
103
|
+
* file's, which holds the styles of every module of the file.
|
|
104
|
+
*/
|
|
105
|
+
function moduleCssSpecifier(id) {
|
|
106
|
+
return `./${path.posix.basename(splitQuery(id)[0])}${MODULE_SUFFIX}`;
|
|
107
|
+
}
|
|
108
|
+
const join = (...parts) => parts.filter(Boolean).join("\n");
|
|
109
|
+
/**
|
|
110
|
+
* Dev `base.css` (PLAN §5.10, D14): everything but the atoms. Every token,
|
|
111
|
+
* so a token edit never waits for a module to use it; the layer statement
|
|
112
|
+
* over every atom compiled so far; the rules of every module compiled so far
|
|
113
|
+
* (themes after the root tokens, module keyframes after the config's); and
|
|
114
|
+
* the config's reset, fonts, keyframes and global CSS. Its `<style>` comes
|
|
115
|
+
* before any module's, and the browser orders layers by their first
|
|
116
|
+
* declaration in document order, so updating it re-orders the sub-layers of
|
|
117
|
+
* modules added later.
|
|
118
|
+
*/
|
|
119
|
+
function devBaseCss(registry, options, libraries = []) {
|
|
120
|
+
return printBase({
|
|
121
|
+
...options,
|
|
122
|
+
atoms: [
|
|
123
|
+
...registry.layerAtoms(),
|
|
124
|
+
...registry.paletteAtoms(),
|
|
125
|
+
...libraries.flatMap((library) => library.atoms)
|
|
126
|
+
],
|
|
127
|
+
rules: [...registry.rules(), ...libraries.flatMap((library) => library.rules)],
|
|
128
|
+
referenced: registry.paletteKeys(),
|
|
129
|
+
tokens: "all"
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** `virtual:vicss/lib/<n>.css`, the dev CSS of the library at index `n`. */
|
|
133
|
+
const LIBRARY_SPECIFIER = "virtual:vicss/lib/";
|
|
134
|
+
function librarySpecifier(index) {
|
|
135
|
+
return `${LIBRARY_SPECIFIER}${index}.css`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Dev CSS of one module: its atom rules and flattened recipe bases, in the
|
|
139
|
+
* sub-layers `base.css` declares; its other rules print in `base.css`.
|
|
140
|
+
*/
|
|
141
|
+
function moduleCss(styles, system, names) {
|
|
142
|
+
return printAtoms(styles.atoms, system, styles.rules, names);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The build's sheet and, in an SPA, its tokens file. Both depend only on the
|
|
146
|
+
* scan and the config, so every environment and build order prints the same
|
|
147
|
+
* bytes. Tokens are the used ones: those atoms, rules, `token()` calls,
|
|
148
|
+
* `globalCss` and config keyframes read (D13). Flattened recipe bases are
|
|
149
|
+
* code, like atoms: they print in the sheet, never in the tokens file.
|
|
150
|
+
*/
|
|
151
|
+
function buildCss(scan, options, tokensFile) {
|
|
152
|
+
const { system, layers } = options;
|
|
153
|
+
const input = {
|
|
154
|
+
...options,
|
|
155
|
+
atoms: scan.atoms,
|
|
156
|
+
rules: scan.rules,
|
|
157
|
+
tokens: "used",
|
|
158
|
+
referenced: scan.tokens
|
|
159
|
+
};
|
|
160
|
+
const tokenKeys = new Set(baseTokens(input).map((token) => `${token.scale}:${token.path}`));
|
|
161
|
+
if (!tokensFile) return {
|
|
162
|
+
sheet: printSheet(input),
|
|
163
|
+
tokens: void 0,
|
|
164
|
+
tokenKeys
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
sheet: join(layers ? outerLayers(layers) : "", layerStatement(scan.atoms, system, scan.rules), printAtoms(scan.atoms, system, scan.rules, options.names)),
|
|
168
|
+
tokens: printBase(input),
|
|
169
|
+
tokenKeys
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/vite/attributes.ts
|
|
174
|
+
/**
|
|
175
|
+
* What every compiled component and JSX module gets for `class=` and
|
|
176
|
+
* `style=` (PLAN §5.16), whether it imports vicss or not: a style it
|
|
177
|
+
* receives may come from anywhere.
|
|
178
|
+
*
|
|
179
|
+
* - Vue's `normalizeClass` and Svelte's `clsx` read an object as a map from
|
|
180
|
+
* class to condition, so `class={card}` would write the merge ids. The
|
|
181
|
+
* compiled component's calls go through `__vxClassWith`, which passes a
|
|
182
|
+
* compiled style as its class string (runtime/style.ts), and the calls
|
|
183
|
+
* that normalise a props object's `class` (spreads, a root element's
|
|
184
|
+
* attributes) through `__vxPropsWith` and `__vxEffectWith`.
|
|
185
|
+
* - In dev, a `style=` value the compiler could not write as an inline
|
|
186
|
+
* style goes through `__vxStyleCheck`, which fails on a compiled style
|
|
187
|
+
* and names the fix (runtime/dev.ts): in JSX attributes and template
|
|
188
|
+
* literals here, in Vue's, Svelte's and Astro's style setters through
|
|
189
|
+
* `__vxStyleWith`.
|
|
190
|
+
*/
|
|
191
|
+
const RUNTIME = "@menapo/vicss";
|
|
192
|
+
/** Vue's and Svelte's class calls, and in dev the style setters of all three compilers. */
|
|
193
|
+
function rewriteComponent(code, id, options) {
|
|
194
|
+
const edits = new SourceEdits(code);
|
|
195
|
+
const helpers = /* @__PURE__ */ new Set();
|
|
196
|
+
const consts = [];
|
|
197
|
+
let n = 0;
|
|
198
|
+
const src = JSON.stringify(options.src);
|
|
199
|
+
const named = /import\s*\{([^}]*)\}\s*from\s*(["'])([^"']+)\2;?/g;
|
|
200
|
+
const WRAPS = {
|
|
201
|
+
normalizeClass: (local) => `__vxClassWith(${local})`,
|
|
202
|
+
ssrRenderClass: (local) => `__vxClassWith(${local})`,
|
|
203
|
+
mergeProps: (local) => `__vxPropsWith(${local})`,
|
|
204
|
+
normalizeProps: (local) => `__vxPropsWith(${local})`,
|
|
205
|
+
ssrRenderAttrs: (local) => `__vxPropsWith(${local})`,
|
|
206
|
+
normalizeStyle: (local) => options.dev ? `__vxStyleWith(${local}, 0, ${src})` : void 0,
|
|
207
|
+
ssrRenderStyle: (local) => options.dev ? `__vxStyleWith(${local}, 0, ${src})` : void 0,
|
|
208
|
+
addAttribute: (local) => options.dev ? `__vxStyleWith(${local}, 0, ${src}, "style")` : void 0
|
|
209
|
+
};
|
|
210
|
+
const SOURCES = /^(?:vue|vue\/server-renderer|@vue\/server-renderer|astro\/(?:runtime\/server(?:\/index\.js)?|compiler-runtime))$/;
|
|
211
|
+
for (const match of code.matchAll(named)) {
|
|
212
|
+
if (!SOURCES.test(match[3])) continue;
|
|
213
|
+
const listStart = match.index + match[0].indexOf("{") + 1;
|
|
214
|
+
for (const spec of match[1].matchAll(/(\w+)(?:\s+as\s+([\w$]+))?/g)) {
|
|
215
|
+
const wrap = WRAPS[spec[1]];
|
|
216
|
+
const local = spec[2] ?? spec[1];
|
|
217
|
+
const inner = `__vxOwn${++n}`;
|
|
218
|
+
const text = wrap?.(inner);
|
|
219
|
+
if (!text) continue;
|
|
220
|
+
edits.overwrite(listStart + spec.index, listStart + spec.index + spec[0].length, `${spec[1]} as ${inner}`);
|
|
221
|
+
for (const helper of text.match(/__vx(?:ClassWith|StyleWith|PropsWith)/g)) helpers.add(helper);
|
|
222
|
+
consts.push({
|
|
223
|
+
at: match.index + match[0].length,
|
|
224
|
+
text: `const ${local} = ${text};`
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const match of code.matchAll(/import\s*\*\s*as\s+([\w$]+)\s+from\s*(["'])svelte\/internal\/(client|server)\2;?/g)) {
|
|
229
|
+
const ns = match[1];
|
|
230
|
+
const calls = {
|
|
231
|
+
clsx: "__vxClassWith",
|
|
232
|
+
attribute_effect: "__vxEffectWith",
|
|
233
|
+
attributes: "__vxPropsWith",
|
|
234
|
+
set_style: options.dev ? "__vxStyleWith" : void 0,
|
|
235
|
+
attr_style: options.dev ? "__vxStyleWith" : void 0
|
|
236
|
+
};
|
|
237
|
+
for (const [name, helper] of Object.entries(calls)) {
|
|
238
|
+
if (!helper) continue;
|
|
239
|
+
const pattern = new RegExp(`(?<![\\w$.])${ns.replace(/\$/g, "\\$")}\\.${name}(?=\\s*\\()`, "g");
|
|
240
|
+
const local = `__vx_${name}`;
|
|
241
|
+
let used = false;
|
|
242
|
+
for (const call of code.matchAll(pattern)) {
|
|
243
|
+
edits.overwrite(call.index, call.index + call[0].length, local);
|
|
244
|
+
used = true;
|
|
245
|
+
}
|
|
246
|
+
if (!used) continue;
|
|
247
|
+
helpers.add(helper);
|
|
248
|
+
const at = name === "set_style" ? `, 1, ${src}` : name === "attr_style" ? `, 0, ${src}` : "";
|
|
249
|
+
consts.push({
|
|
250
|
+
at: match.index + match[0].length,
|
|
251
|
+
text: `const ${local} = ${helper}(${ns}.${name}${at});`
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (consts.length === 0) return null;
|
|
256
|
+
for (const { at, text } of consts) edits.insert(at, `\n${text}`);
|
|
257
|
+
edits.prepend(`import { ${[...helpers].join(", ")} } from "${RUNTIME}";\n`);
|
|
258
|
+
const file = id.replace(/[?#].*$/, "");
|
|
259
|
+
return {
|
|
260
|
+
code: edits.toString(),
|
|
261
|
+
map: edits.generateMap({
|
|
262
|
+
source: file,
|
|
263
|
+
file,
|
|
264
|
+
includeContent: true,
|
|
265
|
+
hires: true
|
|
266
|
+
})
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** Values whose type is plainly not a compiled style: no check. */
|
|
270
|
+
const PLAIN = /* @__PURE__ */ new Set([
|
|
271
|
+
"ObjectExpression",
|
|
272
|
+
"Literal",
|
|
273
|
+
"TemplateLiteral",
|
|
274
|
+
"ArrowFunctionExpression",
|
|
275
|
+
"FunctionExpression"
|
|
276
|
+
]);
|
|
277
|
+
/**
|
|
278
|
+
* Dev: `style={x}` in JSX, and `style=${x}` in a template literal, where
|
|
279
|
+
* `x` is anything the compiler left: checked when it renders.
|
|
280
|
+
*/
|
|
281
|
+
function checkStyles(code, id, src) {
|
|
282
|
+
const file = id.replace(/[?#].*$/, "");
|
|
283
|
+
const lang = /\.([cm]?[jt]sx?)$/.exec(file)?.[1]?.replace(/^[cm]/, "");
|
|
284
|
+
let program;
|
|
285
|
+
try {
|
|
286
|
+
const parsed = parseSync(file, code, {
|
|
287
|
+
lang: lang ?? "js",
|
|
288
|
+
sourceType: "module"
|
|
289
|
+
});
|
|
290
|
+
if (parsed.errors.length > 0) return null;
|
|
291
|
+
program = parsed.program;
|
|
292
|
+
} catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const values = [];
|
|
296
|
+
const visit = (node) => {
|
|
297
|
+
if (!node || typeof node !== "object") return;
|
|
298
|
+
if (Array.isArray(node)) {
|
|
299
|
+
for (const item of node) visit(item);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const current = node;
|
|
303
|
+
if (typeof current.type !== "string") return;
|
|
304
|
+
if (current.type === "JSXAttribute" && current.name.type === "JSXIdentifier" && current.name.name === "style" && current.value?.type === "JSXExpressionContainer" && current.value.expression.type !== "JSXEmptyExpression" && !PLAIN.has(current.value.expression.type)) values.push(current.value.expression);
|
|
305
|
+
else if (current.type === "TemplateLiteral") current.quasis.forEach((quasi, i) => {
|
|
306
|
+
const expression = current.expressions[i];
|
|
307
|
+
if (expression && /\sstyle="?$/.test(quasi.value.raw) && !PLAIN.has(expression.type)) values.push(expression);
|
|
308
|
+
});
|
|
309
|
+
for (const key in current) if (key !== "parent" && key !== "range") visit(current[key]);
|
|
310
|
+
};
|
|
311
|
+
visit(program);
|
|
312
|
+
if (values.length === 0) return null;
|
|
313
|
+
const edits = new SourceEdits(code, { keepLines: true });
|
|
314
|
+
const locate = createLocator(code);
|
|
315
|
+
for (const value of values) {
|
|
316
|
+
const at = JSON.stringify(`${src}:${locate(value.start).line}`);
|
|
317
|
+
edits.insert(value.start, "__vxStyleCheck(");
|
|
318
|
+
edits.insert(value.end, `, ${at})`);
|
|
319
|
+
}
|
|
320
|
+
const first = program.body.find((node) => !(node.type === "ExpressionStatement" && typeof node.directive === "string"));
|
|
321
|
+
edits.insert(first?.start ?? 0, `import { __vxStyleCheck } from "${RUNTIME}"; `);
|
|
322
|
+
return {
|
|
323
|
+
code: edits.toString(),
|
|
324
|
+
map: edits.generateMap({
|
|
325
|
+
source: file,
|
|
326
|
+
file,
|
|
327
|
+
includeContent: true,
|
|
328
|
+
hires: true
|
|
329
|
+
})
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/vite/html.ts
|
|
334
|
+
/**
|
|
335
|
+
* The SPA tokens file (PLAN D13). Tokens change on a theme-only deploy and
|
|
336
|
+
* atoms do not; kept out of every chunk's `importedCss`, they rename no JS
|
|
337
|
+
* file through `augmentChunkHash` (report 06 §7.1). Only HTML that Vite
|
|
338
|
+
* builds can link it, so it exists only when the client build has an HTML
|
|
339
|
+
* entry, and only when Vite splits CSS per chunk: with one CSS file for the
|
|
340
|
+
* whole app, no JS names CSS at all.
|
|
341
|
+
*/
|
|
342
|
+
/** The CSS entry's name: Vite emits its CSS as `assets/vicss-tokens-[hash].css`. */
|
|
343
|
+
const TOKENS_CHUNK_NAME = "vicss-tokens";
|
|
344
|
+
/**
|
|
345
|
+
* Whether the client build has an HTML entry, read from the config so that
|
|
346
|
+
* every environment agrees: it decides what the sheet holds. Mirrors how
|
|
347
|
+
* Vite picks a build's input.
|
|
348
|
+
*/
|
|
349
|
+
function hasHtmlEntry(config) {
|
|
350
|
+
const client = config.environments.client;
|
|
351
|
+
if (!client || client.build.lib) return false;
|
|
352
|
+
const input = client.build.rolldownOptions?.input ?? client.input;
|
|
353
|
+
if (input === void 0) return existsSync(path.join(config.root, "index.html"));
|
|
354
|
+
return (typeof input === "string" ? [input] : Object.values(input)).some((entry) => entry.endsWith(".html"));
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* `<link rel="stylesheet">` first in `<head>`, so its layer statement comes
|
|
358
|
+
* first. Its URL is built the way Vite builds its own links in the same page
|
|
359
|
+
* (`renderBuiltUrl`, then the base, relative when the base is), and it is
|
|
360
|
+
* `crossorigin` like them, so a CDN setup serves both alike.
|
|
361
|
+
*/
|
|
362
|
+
function tokensLink(config, fileName, hostId) {
|
|
363
|
+
const link = (href) => ({
|
|
364
|
+
tag: "link",
|
|
365
|
+
attrs: {
|
|
366
|
+
rel: "stylesheet",
|
|
367
|
+
crossorigin: true,
|
|
368
|
+
href
|
|
369
|
+
},
|
|
370
|
+
injectTo: "head-prepend"
|
|
371
|
+
});
|
|
372
|
+
const rendered = config.experimental.renderBuiltUrl?.(fileName, {
|
|
373
|
+
hostId,
|
|
374
|
+
hostType: "html",
|
|
375
|
+
type: "asset",
|
|
376
|
+
ssr: false
|
|
377
|
+
});
|
|
378
|
+
if (typeof rendered === "string") return link(rendered);
|
|
379
|
+
if (!(rendered?.relative ?? (config.base === "" || config.base === "./"))) return link(`${config.base}${fileName}`);
|
|
380
|
+
const href = path.posix.join(path.posix.relative(path.posix.dirname(hostId), ""), fileName);
|
|
381
|
+
return link(href.startsWith(".") ? href : `./${href}`);
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/vite/registry.ts
|
|
385
|
+
/**
|
|
386
|
+
* What each compiled module contributes to the CSS, shared by every
|
|
387
|
+
* environment: one source compiles to the same atoms for the client, SSR and
|
|
388
|
+
* any other consumer, so keeping one copy also keeps their CSS identical.
|
|
389
|
+
*/
|
|
390
|
+
/** Whether a module contributes CSS, and so imports it. */
|
|
391
|
+
function isStyled(styles) {
|
|
392
|
+
return styles.atoms.length > 0 || styles.tokens.length > 0 || styles.rules.length > 0;
|
|
393
|
+
}
|
|
394
|
+
/** Entries counted by key, so one leaves only when the last module holding it does. */
|
|
395
|
+
var Counted = class {
|
|
396
|
+
#entries = /* @__PURE__ */ new Map();
|
|
397
|
+
/** Returns the keys that were not held before. */
|
|
398
|
+
add(values, keyOf) {
|
|
399
|
+
const added = [];
|
|
400
|
+
for (const value of values) {
|
|
401
|
+
const key = keyOf(value);
|
|
402
|
+
const entry = this.#entries.get(key);
|
|
403
|
+
if (entry) entry.count++;
|
|
404
|
+
else {
|
|
405
|
+
this.#entries.set(key, {
|
|
406
|
+
value,
|
|
407
|
+
count: 1
|
|
408
|
+
});
|
|
409
|
+
added.push(key);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return added;
|
|
413
|
+
}
|
|
414
|
+
/** Returns the keys that lost their last holder. */
|
|
415
|
+
remove(values, keyOf) {
|
|
416
|
+
const gone = [];
|
|
417
|
+
for (const value of values) {
|
|
418
|
+
const key = keyOf(value);
|
|
419
|
+
const entry = this.#entries.get(key);
|
|
420
|
+
if (!entry || --entry.count > 0) continue;
|
|
421
|
+
this.#entries.delete(key);
|
|
422
|
+
gone.push(key);
|
|
423
|
+
}
|
|
424
|
+
return gone;
|
|
425
|
+
}
|
|
426
|
+
has(key) {
|
|
427
|
+
return this.#entries.has(key);
|
|
428
|
+
}
|
|
429
|
+
values() {
|
|
430
|
+
return [...this.#entries.values()].map((entry) => entry.value);
|
|
431
|
+
}
|
|
432
|
+
clear() {
|
|
433
|
+
this.#entries.clear();
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
var Registry = class {
|
|
437
|
+
#modules = /* @__PURE__ */ new Map();
|
|
438
|
+
/**
|
|
439
|
+
* Module ids by file: a framework file can be several modules (Astro's
|
|
440
|
+
* `?astro&type=script` scripts next to its component), which share the
|
|
441
|
+
* file's one dev CSS module.
|
|
442
|
+
*/
|
|
443
|
+
#files = /* @__PURE__ */ new Map();
|
|
444
|
+
#dependants = /* @__PURE__ */ new Map();
|
|
445
|
+
/**
|
|
446
|
+
* What each compiled module that styles nothing read: `sx(row)` folds a
|
|
447
|
+
* marker's class, a module may inline a constant into a string. It must
|
|
448
|
+
* compile again when one of those files changes, as styled modules do.
|
|
449
|
+
*/
|
|
450
|
+
#reads = /* @__PURE__ */ new Map();
|
|
451
|
+
/**
|
|
452
|
+
* One atom per sub-layer (chain and tier). The layer statement depends only
|
|
453
|
+
* on which sub-layers exist, so it changes exactly when a key appears or
|
|
454
|
+
* goes, and one atom per key is enough to print it.
|
|
455
|
+
*/
|
|
456
|
+
#subLayers = new Counted();
|
|
457
|
+
/** Rules by id: a theme folded into several modules prints once. */
|
|
458
|
+
#rules = new Counted();
|
|
459
|
+
/**
|
|
460
|
+
* `colorPalette` atoms and the palette keys styles read (`palette.600`):
|
|
461
|
+
* `base.css` prints the palettes' rules from them (core/palettes.ts).
|
|
462
|
+
*/
|
|
463
|
+
#palettes = new Counted();
|
|
464
|
+
#paletteKeys = new Counted();
|
|
465
|
+
/**
|
|
466
|
+
* Flattened recipe bases by id, and one of their atoms per chain. They
|
|
467
|
+
* print in the modules' CSS, so `base.css` depends only on the chains'
|
|
468
|
+
* flattened sub-layers, as on the atoms' sub-layers: an edited base is a
|
|
469
|
+
* new class over the same chains, which leaves it alone.
|
|
470
|
+
*/
|
|
471
|
+
#bases = new Counted();
|
|
472
|
+
#flatLayers = new Counted();
|
|
473
|
+
get(id) {
|
|
474
|
+
return this.#modules.get(id);
|
|
475
|
+
}
|
|
476
|
+
has(id) {
|
|
477
|
+
return this.#modules.has(id);
|
|
478
|
+
}
|
|
479
|
+
ids() {
|
|
480
|
+
return [...this.#modules.keys()];
|
|
481
|
+
}
|
|
482
|
+
/** The styles of every module of `file` (an id without its query), merged. */
|
|
483
|
+
ofFile(file) {
|
|
484
|
+
const styles = [...this.#files.get(file) ?? []].flatMap((id) => this.#modules.get(id) ?? []);
|
|
485
|
+
if (styles.length <= 1) return styles[0];
|
|
486
|
+
return {
|
|
487
|
+
atoms: [...new Map(styles.flatMap((s) => s.atoms).map((a) => [a.className, a])).values()].sort((a, b) => compareStrings(a.className, b.className)),
|
|
488
|
+
tokens: styles.flatMap((s) => s.tokens),
|
|
489
|
+
rules: styles.flatMap((s) => s.rules),
|
|
490
|
+
dependencies: [...new Set(styles.flatMap((s) => s.dependencies))]
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Returns whether `base.css` changed: a sub-layer, and so the layer
|
|
495
|
+
* statement, or a rule appeared or went.
|
|
496
|
+
*/
|
|
497
|
+
set(id, styles) {
|
|
498
|
+
const removed = this.#remove(id);
|
|
499
|
+
this.#modules.set(id, styles);
|
|
500
|
+
const file = fileOf(id);
|
|
501
|
+
let ids = this.#files.get(file);
|
|
502
|
+
if (!ids) this.#files.set(file, ids = /* @__PURE__ */ new Set());
|
|
503
|
+
ids.add(id);
|
|
504
|
+
this.#depend(id, styles.dependencies);
|
|
505
|
+
this.#bases.add(styles.rules.filter(isBase), ruleId);
|
|
506
|
+
const added = [
|
|
507
|
+
...this.#subLayers.add(styles.atoms, subLayer).map((key) => `l:${key}`),
|
|
508
|
+
...this.#rules.add(styles.rules.filter(prints), ruleId).map((key) => `r:${key}`),
|
|
509
|
+
...this.#flatLayers.add(flatAtoms(styles.rules), flatLayer).map((key) => `f:${key}`),
|
|
510
|
+
...this.#palettes.add(styles.atoms.filter(isPaletteAtom), className).map((key) => `p:${key}`),
|
|
511
|
+
...this.#paletteKeys.add(paletteRefs(styles), refId).map((key) => `k:${key}`)
|
|
512
|
+
];
|
|
513
|
+
return removed.some((key) => !this.#holds(key)) || added.some((key) => !removed.includes(key));
|
|
514
|
+
}
|
|
515
|
+
/** Returns whether `base.css` changed. */
|
|
516
|
+
delete(id) {
|
|
517
|
+
return this.#remove(id).length > 0;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* A compiled module that styles nothing: not registered, but a module
|
|
521
|
+
* that read `dependencies`, so `dependantsOf` lists it. Returns whether
|
|
522
|
+
* `base.css` changed (it styled something before).
|
|
523
|
+
*/
|
|
524
|
+
unstyled(id, dependencies) {
|
|
525
|
+
const changed = this.delete(id);
|
|
526
|
+
this.#depend(id, dependencies);
|
|
527
|
+
return changed;
|
|
528
|
+
}
|
|
529
|
+
clear() {
|
|
530
|
+
this.#modules.clear();
|
|
531
|
+
this.#files.clear();
|
|
532
|
+
this.#dependants.clear();
|
|
533
|
+
this.#reads.clear();
|
|
534
|
+
this.#subLayers.clear();
|
|
535
|
+
this.#rules.clear();
|
|
536
|
+
this.#palettes.clear();
|
|
537
|
+
this.#paletteKeys.clear();
|
|
538
|
+
this.#bases.clear();
|
|
539
|
+
this.#flatLayers.clear();
|
|
540
|
+
}
|
|
541
|
+
/** Modules, styled or not, that read constants from `file`. */
|
|
542
|
+
dependantsOf(file) {
|
|
543
|
+
return [...this.#dependants.get(file) ?? []];
|
|
544
|
+
}
|
|
545
|
+
/** One atom per sub-layer in use: what the layer statement is printed from. */
|
|
546
|
+
layerAtoms() {
|
|
547
|
+
return this.#subLayers.values();
|
|
548
|
+
}
|
|
549
|
+
/** Every registered module's rules, one per id: flattened bases declare their sub-layers. */
|
|
550
|
+
rules() {
|
|
551
|
+
return [...this.#rules.values(), ...this.#bases.values()];
|
|
552
|
+
}
|
|
553
|
+
/** Every `colorPalette` atom of the registered modules. */
|
|
554
|
+
paletteAtoms() {
|
|
555
|
+
return this.#palettes.values();
|
|
556
|
+
}
|
|
557
|
+
/** The palette keys the registered modules read, as token references. */
|
|
558
|
+
paletteKeys() {
|
|
559
|
+
return this.#paletteKeys.values();
|
|
560
|
+
}
|
|
561
|
+
/** Records that `id` read `dependencies`, replacing what it read before. */
|
|
562
|
+
#depend(id, dependencies) {
|
|
563
|
+
for (const dep of this.#reads.get(id) ?? []) {
|
|
564
|
+
const set = this.#dependants.get(dep);
|
|
565
|
+
set?.delete(id);
|
|
566
|
+
if (set?.size === 0) this.#dependants.delete(dep);
|
|
567
|
+
}
|
|
568
|
+
if (dependencies.length === 0) this.#reads.delete(id);
|
|
569
|
+
else this.#reads.set(id, dependencies);
|
|
570
|
+
for (const dep of dependencies) {
|
|
571
|
+
let set = this.#dependants.get(dep);
|
|
572
|
+
if (!set) this.#dependants.set(dep, set = /* @__PURE__ */ new Set());
|
|
573
|
+
set.add(id);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
#holds(key) {
|
|
577
|
+
const id = key.slice(2);
|
|
578
|
+
switch (key[0]) {
|
|
579
|
+
case "l": return this.#subLayers.has(id);
|
|
580
|
+
case "r": return this.#rules.has(id);
|
|
581
|
+
case "f": return this.#flatLayers.has(id);
|
|
582
|
+
case "p": return this.#palettes.has(id);
|
|
583
|
+
default: return this.#paletteKeys.has(id);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Returns the sub-layers (`l:`), rules (`r:`), flattened sub-layers (`f:`),
|
|
588
|
+
* palette atoms (`p:`) and palette keys (`k:`) that lost their last holder.
|
|
589
|
+
*/
|
|
590
|
+
#remove(id) {
|
|
591
|
+
const old = this.#modules.get(id);
|
|
592
|
+
if (!old) return [];
|
|
593
|
+
this.#modules.delete(id);
|
|
594
|
+
const ids = this.#files.get(fileOf(id));
|
|
595
|
+
ids?.delete(id);
|
|
596
|
+
if (ids?.size === 0) this.#files.delete(fileOf(id));
|
|
597
|
+
this.#depend(id, []);
|
|
598
|
+
this.#bases.remove(old.rules.filter(isBase), ruleId);
|
|
599
|
+
return [
|
|
600
|
+
...this.#subLayers.remove(old.atoms, subLayer).map((key) => `l:${key}`),
|
|
601
|
+
...this.#rules.remove(old.rules.filter(prints), ruleId).map((key) => `r:${key}`),
|
|
602
|
+
...this.#flatLayers.remove(flatAtoms(old.rules), flatLayer).map((key) => `f:${key}`),
|
|
603
|
+
...this.#palettes.remove(old.atoms.filter(isPaletteAtom), className).map((key) => `p:${key}`),
|
|
604
|
+
...this.#paletteKeys.remove(paletteRefs(old), refId).map((key) => `k:${key}`)
|
|
605
|
+
];
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
/** The palette keys a module's styles read: colour tokens of the `palette` group. */
|
|
609
|
+
function paletteRefs(styles) {
|
|
610
|
+
const refs = [
|
|
611
|
+
...styles.atoms.flatMap((atom) => atom.tokens),
|
|
612
|
+
...styles.rules.flatMap((rule) => rule.tokens),
|
|
613
|
+
...styles.tokens
|
|
614
|
+
];
|
|
615
|
+
const unique = /* @__PURE__ */ new Map();
|
|
616
|
+
for (const ref of refs) if (ref.scale === "colors" && (ref.path === "palette" || ref.path.startsWith("palette."))) unique.set(ref.path, ref);
|
|
617
|
+
return [...unique.values()];
|
|
618
|
+
}
|
|
619
|
+
const subLayer = (atom) => `${atom.chain.join(" ")}|${atom.tier}`;
|
|
620
|
+
const className = (atom) => atom.className;
|
|
621
|
+
const refId = (ref) => ref.path;
|
|
622
|
+
const flatLayer = (atom) => atom.chain.join(" ");
|
|
623
|
+
const isBase = (rule) => rule.kind === "base";
|
|
624
|
+
/** What `base.css` prints: themes and keyframes. */
|
|
625
|
+
const prints = (rule) => !isBase(rule);
|
|
626
|
+
const flatAtoms = (rules) => rules.flatMap((rule) => isBase(rule) ? rule.atoms ?? [] : []);
|
|
627
|
+
/** A module id without its query: the file its dev CSS module is named after. */
|
|
628
|
+
const fileOf = (id) => id.replace(/[?#].*$/, "");
|
|
629
|
+
const ruleId = (rule) => rule.id;
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/vite/plugin.ts
|
|
632
|
+
/**
|
|
633
|
+
* The Vite plugin (PLAN §5.10, M2). One `pre` transform compiles styled
|
|
634
|
+
* modules on their source; dev serves one CSS module per styled module plus
|
|
635
|
+
* a shared `base.css` (D14); a build pre-scans `include` and serves one sheet
|
|
636
|
+
* (D5), plus a tokens file in SPAs (D13).
|
|
637
|
+
*/
|
|
638
|
+
const RUNTIME_PACKAGE = "@menapo/vicss";
|
|
639
|
+
/** What a library build writes into its `outDir` (PLAN §5.13). */
|
|
640
|
+
const MANIFEST_FILE = "vicss.json";
|
|
641
|
+
const LIBRARY_SHEET_FILE = "vicss.css";
|
|
642
|
+
const SCRIPT_ID = /\.[cm]?[jt]sx?$/;
|
|
643
|
+
/** The calls `vicss:attributes` rewrites: a module with none is left alone. */
|
|
644
|
+
const COMPONENT_CALLS = [
|
|
645
|
+
"normalizeClass",
|
|
646
|
+
"ssrRenderClass",
|
|
647
|
+
"mergeProps",
|
|
648
|
+
"normalizeProps",
|
|
649
|
+
"ssrRenderAttrs",
|
|
650
|
+
"clsx",
|
|
651
|
+
"attribute_effect",
|
|
652
|
+
"attributes",
|
|
653
|
+
"normalizeStyle",
|
|
654
|
+
"ssrRenderStyle",
|
|
655
|
+
"set_style",
|
|
656
|
+
"attr_style",
|
|
657
|
+
"addAttribute"
|
|
658
|
+
];
|
|
659
|
+
/** Declaration files: never styled, and the generated `vicss.d.ts` names the runtime. */
|
|
660
|
+
const DECLARATION_ID = /\.d\.[cm]?ts$/;
|
|
661
|
+
/**
|
|
662
|
+
* PLAN §6: a warning above this share of hashed atoms (values outside the
|
|
663
|
+
* scales, and long or unusual literals), as a design system erodes.
|
|
664
|
+
*/
|
|
665
|
+
const HASHED_BUDGET = .1;
|
|
666
|
+
const CSS_ID = /(?:^virtual:vicss\/(?:base|sheet|lib\/\d+)\.css|\/@vicss\/(?:base|sheet|tokens|lib\/\d+)\.css|\.vicss\.css)(?:[?#]|$)/;
|
|
667
|
+
/** Modules compiled at once outside a transform (HMR, a config edit). */
|
|
668
|
+
const BATCH = 16;
|
|
669
|
+
function vicss(options = {}) {
|
|
670
|
+
let state;
|
|
671
|
+
let server;
|
|
672
|
+
const registry = new Registry();
|
|
673
|
+
const cache = new CompileCache();
|
|
674
|
+
/** Dev: ids the transform rewrote, invalidated when the config changes. */
|
|
675
|
+
const transformed = /* @__PURE__ */ new Set();
|
|
676
|
+
/**
|
|
677
|
+
* Dev: what each framework file's scripts style, as the pre-scan reads
|
|
678
|
+
* them, by file and the hash of its source.
|
|
679
|
+
*/
|
|
680
|
+
const scriptStyles = /* @__PURE__ */ new Map();
|
|
681
|
+
/**
|
|
682
|
+
* Build: one scan per config, shared by every environment of one build.
|
|
683
|
+
* A build is the configs one `vite build` or builder resolves, all before
|
|
684
|
+
* any environment starts; a config resolved after that starts a new one.
|
|
685
|
+
*/
|
|
686
|
+
const scans = /* @__PURE__ */ new Map();
|
|
687
|
+
let session = newSession();
|
|
688
|
+
/** The CSS each scan's result prints with the libraries, its atom count and its classes. */
|
|
689
|
+
const sheets = /* @__PURE__ */ new WeakMap();
|
|
690
|
+
/** Each scan's classes in literal form, for `api.literals()`. */
|
|
691
|
+
const literalTables = /* @__PURE__ */ new WeakMap();
|
|
692
|
+
/** A scan whose sheet failed to print (a library's atoms conflict with the app's). */
|
|
693
|
+
const sheetErrors = /* @__PURE__ */ new WeakMap();
|
|
694
|
+
/** Scan failures a hook has reported (`sheetFor`). */
|
|
695
|
+
const reportedFailures = /* @__PURE__ */ new WeakSet();
|
|
696
|
+
const reported = /* @__PURE__ */ new WeakSet();
|
|
697
|
+
/**
|
|
698
|
+
* What the scan (build) or an HMR pass (dev) compiled, by id, so the
|
|
699
|
+
* transform of an unchanged module reuses it instead of compiling every
|
|
700
|
+
* module twice. Build entries go when the build ends; dev entries when
|
|
701
|
+
* their transform takes them.
|
|
702
|
+
*/
|
|
703
|
+
const kept = /* @__PURE__ */ new Map();
|
|
704
|
+
let diskCache;
|
|
705
|
+
/** Build: the CSS files of the chunk that holds the sheet, per environment. */
|
|
706
|
+
const sheetFiles = /* @__PURE__ */ new WeakMap();
|
|
707
|
+
/**
|
|
708
|
+
* Build: resolutions per environment, by importer directory, shared by
|
|
709
|
+
* the scan and the transforms: most modules import the same few modules.
|
|
710
|
+
*/
|
|
711
|
+
const buildResolutions = /* @__PURE__ */ new WeakMap();
|
|
712
|
+
/** The SPA tokens file, per client build, with the config that links it. */
|
|
713
|
+
const tokenAssets = /* @__PURE__ */ new Map();
|
|
714
|
+
/**
|
|
715
|
+
* Dev: resolutions per environment, kept until a file is added or removed.
|
|
716
|
+
* While the watcher handles a change, every async call (Vite's native
|
|
717
|
+
* resolver included) waits behind it for ~50 ms; the requests of an
|
|
718
|
+
* edited module were all resolved when it was first served.
|
|
719
|
+
*/
|
|
720
|
+
const resolutions = /* @__PURE__ */ new Map();
|
|
721
|
+
/** One recompile per file change, shared by the environments' `hotUpdate` calls. */
|
|
722
|
+
const changes = /* @__PURE__ */ new Map();
|
|
723
|
+
/**
|
|
724
|
+
* Dev and build watch mode: files a config load read before it failed (a
|
|
725
|
+
* token file it could not parse), watched too so fixing one reloads the
|
|
726
|
+
* config.
|
|
727
|
+
*/
|
|
728
|
+
let failedReads = /* @__PURE__ */ new Set();
|
|
729
|
+
/**
|
|
730
|
+
* Build watch mode: a file the config read changed, so the next build
|
|
731
|
+
* loads the config again. A rebuild runs no `configResolved`.
|
|
732
|
+
*/
|
|
733
|
+
let configChanged = false;
|
|
734
|
+
/** Build: `scan.json` writes in progress, which the build's end waits for. */
|
|
735
|
+
const cacheWrites = [];
|
|
736
|
+
/**
|
|
737
|
+
* Build: scan threads started while the config loads (pool.ts), for the
|
|
738
|
+
* scan to take; closed if it does not.
|
|
739
|
+
*/
|
|
740
|
+
let spawned;
|
|
741
|
+
/** Settles when the current dev server closes. */
|
|
742
|
+
let closing = new Promise(() => {});
|
|
743
|
+
/** Dev: settles once the modules registered before a config edit compiled again. */
|
|
744
|
+
let rebuilt = Promise.resolve();
|
|
745
|
+
const sources = [RUNTIME_PACKAGE];
|
|
746
|
+
const ready = () => {
|
|
747
|
+
if (!state) throw new Error("vicss: the plugin was used before Vite resolved its config");
|
|
748
|
+
return state;
|
|
749
|
+
};
|
|
750
|
+
const layers = options.layers;
|
|
751
|
+
/**
|
|
752
|
+
* The config with the installed libraries it must cover (an app's), their
|
|
753
|
+
* manifests watched like the config's files. Throws VicssError when a
|
|
754
|
+
* library uses a token or condition the config lacks.
|
|
755
|
+
*/
|
|
756
|
+
function checkLibraries(s, loaded) {
|
|
757
|
+
const libraries = s.library ? [] : loadLibraries(s.root, loaded.system);
|
|
758
|
+
const files = libraries.flatMap((library) => library.files);
|
|
759
|
+
return {
|
|
760
|
+
...loaded,
|
|
761
|
+
watch: [.../* @__PURE__ */ new Set([...loaded.watch, ...files])],
|
|
762
|
+
configFiles: loaded.watch,
|
|
763
|
+
libraries
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function setSystem(s, loaded) {
|
|
767
|
+
s.loaded = loaded;
|
|
768
|
+
s.compile = {
|
|
769
|
+
system: loaded.system,
|
|
770
|
+
importSources: loaded.system.config.importSources ?? [],
|
|
771
|
+
parsed: createParseCache(),
|
|
772
|
+
sourceMap: s.dev || Object.values(s.config.environments).some((env) => env.build.sourcemap),
|
|
773
|
+
dev: s.devSource,
|
|
774
|
+
placeholders: !s.dev,
|
|
775
|
+
keepLines: s.dev,
|
|
776
|
+
root: s.root,
|
|
777
|
+
package: s.pkg,
|
|
778
|
+
frameworks: s.frameworks
|
|
779
|
+
};
|
|
780
|
+
const warned = /* @__PURE__ */ new Set();
|
|
781
|
+
const options = loaded.system.classNames;
|
|
782
|
+
s.print = {
|
|
783
|
+
system: loaded.system,
|
|
784
|
+
layers,
|
|
785
|
+
warn(message) {
|
|
786
|
+
if (warned.has(message)) return;
|
|
787
|
+
warned.add(message);
|
|
788
|
+
s.config.logger.warn(`vicss: ${message}`);
|
|
789
|
+
},
|
|
790
|
+
...s.dev && { names: createNamer([], {
|
|
791
|
+
prefix: options.prefix,
|
|
792
|
+
minLength: minLengthOf(options, false)
|
|
793
|
+
}, loaded.libraries.map((library) => ({
|
|
794
|
+
library: library.name,
|
|
795
|
+
names: library.classes
|
|
796
|
+
})), true) }
|
|
797
|
+
};
|
|
798
|
+
const short = minLengthWarning(options);
|
|
799
|
+
if (short) s.config.logger.warn(`vicss: ${short}`);
|
|
800
|
+
setConfigHash(s);
|
|
801
|
+
sources.splice(0, sources.length, .../* @__PURE__ */ new Set([RUNTIME_PACKAGE, ...s.compile.importSources]));
|
|
802
|
+
const dirs = loaded.libraries.flatMap((library) => library.dirs);
|
|
803
|
+
libraryDirs.splice(0, libraryDirs.length, ...dirs.length > 0 ? dirs.map((dir) => new RegExp(`^${escapeRegExp(dir)}/`)) : [NOTHING]);
|
|
804
|
+
writeTypes(s);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* The generated `vicss.d.ts`, for the config just loaded (PLAN §5.11),
|
|
808
|
+
* with the framework of the project's plain modules, so `sx()`'s result
|
|
809
|
+
* type follows the shape the compiler gives them (§5.12); and next to it
|
|
810
|
+
* the lint snapshot, so the editor's lint rules see the config the server
|
|
811
|
+
* just loaded.
|
|
812
|
+
*/
|
|
813
|
+
function writeTypes(s) {
|
|
814
|
+
if (options.types === false) return;
|
|
815
|
+
const types = typesFile(s.root, options.types?.output);
|
|
816
|
+
const outputs = [[types, () => generateTypes(s.loaded.system, { framework: s.frameworks.scripts })], [snapshotFile(types), () => snapshotText(s.loaded.system, { root: snapshotRoot(snapshotFile(types), s.root) })]];
|
|
817
|
+
for (const [file, text] of outputs) try {
|
|
818
|
+
if (writeIfChanged(file, text())) s.config.logger.info(`vicss: wrote ${path.relative(s.root, file) || file}`);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
s.config.logger.warn(`vicss: could not write ${file}: ${String(error)}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* The part of a cache key that stands for the config. The system's hash
|
|
825
|
+
* covers a function only by its source, so the files that define the
|
|
826
|
+
* config count too (a constant a utility closes over), and so do the
|
|
827
|
+
* token files it read, though their data is in the hash already. A
|
|
828
|
+
* function from an installed package (a preset's utility) is defined in a
|
|
829
|
+
* file Vite leaves to Node, which no list names: the lockfile stands for
|
|
830
|
+
* every such file, as for Vite's dependency optimizer, so an upgrade
|
|
831
|
+
* scans afresh. When no module on disk defines the config and a function
|
|
832
|
+
* could hide a change, `scan.json` is not used: token files hold no
|
|
833
|
+
* functions. The libraries' manifests do not count: no module's compile
|
|
834
|
+
* reads them, and the sheet prints them after the scan.
|
|
835
|
+
*/
|
|
836
|
+
function setConfigHash(s) {
|
|
837
|
+
const defining = typeof options.config === "object" ? [s.config.configFile, ...s.config.configFileDependencies].flatMap((f) => f ? [normalizePath(f)] : []) : s.loaded.configFiles;
|
|
838
|
+
const functions = hasFunction(s.loaded.system.config);
|
|
839
|
+
const contents = [.../* @__PURE__ */ new Set([
|
|
840
|
+
...defining,
|
|
841
|
+
...s.loaded.configFiles,
|
|
842
|
+
...functions ? lockfiles(s.root) : []
|
|
843
|
+
])].map((file) => `${file}\0${readHash(file) ?? ""}`).join("\0");
|
|
844
|
+
const { scripts, jsxImportSource } = s.frameworks;
|
|
845
|
+
s.configHash = hashCode(`${s.loaded.system.hash}\0${s.compile.sourceMap}\0${s.compile.placeholders}\0${scripts}\0${jsxImportSource ?? ""}\0${s.pkg}\0${contents}`);
|
|
846
|
+
s.diskCache = defining.length > 0 || !functions;
|
|
847
|
+
}
|
|
848
|
+
async function reloadConfig(dev) {
|
|
849
|
+
const s = ready();
|
|
850
|
+
let loaded;
|
|
851
|
+
const reads = /* @__PURE__ */ new Set();
|
|
852
|
+
try {
|
|
853
|
+
loaded = checkLibraries(s, await loadSystem(s.root, options.config, (m) => s.config.logger.info(m), reads));
|
|
854
|
+
failedReads = /* @__PURE__ */ new Set();
|
|
855
|
+
} catch (error) {
|
|
856
|
+
failedReads = new Set([...reads].map(normalizePath));
|
|
857
|
+
dev.watcher.add([...failedReads]);
|
|
858
|
+
const failure = loadFailure(error, s.root, options.config);
|
|
859
|
+
const err = failure instanceof Error ? failure : new Error(String(failure));
|
|
860
|
+
const message = error instanceof VicssError ? err.message : `vicss: ${err.message}`;
|
|
861
|
+
s.config.logger.error(message, {
|
|
862
|
+
error: err,
|
|
863
|
+
timestamp: true
|
|
864
|
+
});
|
|
865
|
+
dev.environments.client?.hot.send({
|
|
866
|
+
type: "error",
|
|
867
|
+
err: {
|
|
868
|
+
message,
|
|
869
|
+
stack: err.stack ?? "",
|
|
870
|
+
plugin: "vicss"
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const before = sources.join("\n");
|
|
876
|
+
const styled = registry.ids();
|
|
877
|
+
const stale = [
|
|
878
|
+
...[...transformed].map(fileOf),
|
|
879
|
+
...styled.map((id) => fileOf(id) + MODULE_SUFFIX),
|
|
880
|
+
s.ids.base,
|
|
881
|
+
...s.loaded.libraries.map((_, i) => `${s.ids.libraries}${i}.css`)
|
|
882
|
+
];
|
|
883
|
+
setSystem(s, loaded);
|
|
884
|
+
registry.clear();
|
|
885
|
+
cache.clear();
|
|
886
|
+
kept.clear();
|
|
887
|
+
scans.clear();
|
|
888
|
+
transformed.clear();
|
|
889
|
+
scriptStyles.clear();
|
|
890
|
+
resolutions.clear();
|
|
891
|
+
dev.watcher.add([...loaded.watch]);
|
|
892
|
+
const client = dev.environments.client;
|
|
893
|
+
const resolve = memoize(resolutions, "client", envResolver(s, client));
|
|
894
|
+
const recompiled = inBatches(styled, async (id) => registry.has(id) ? void 0 : recompile(id, void 0, resolve));
|
|
895
|
+
rebuilt = recompiled;
|
|
896
|
+
const everything = sources.join("\n") !== before;
|
|
897
|
+
for (const env of Object.values(dev.environments)) {
|
|
898
|
+
const graph = env.moduleGraph;
|
|
899
|
+
if (everything) graph.invalidateAll();
|
|
900
|
+
else for (const file of stale) for (const mod of graph.getModulesByFile(file) ?? []) graph.invalidateModule(mod);
|
|
901
|
+
env.hot.send({
|
|
902
|
+
type: "full-reload",
|
|
903
|
+
path: "*"
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
await recompiled;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Build watch mode: the config again, before the rebuild that a change to
|
|
910
|
+
* one of its files started. A config that fails stays marked, so every
|
|
911
|
+
* rebuild reports it until it loads.
|
|
912
|
+
*/
|
|
913
|
+
async function reloadBuildConfig(ctx, s) {
|
|
914
|
+
const reads = /* @__PURE__ */ new Set();
|
|
915
|
+
let loaded;
|
|
916
|
+
try {
|
|
917
|
+
loaded = checkLibraries(s, await loadSystem(s.root, options.config, (m) => s.config.logger.info(m), reads));
|
|
918
|
+
} catch (error) {
|
|
919
|
+
failedReads = new Set([...reads].map(normalizePath));
|
|
920
|
+
for (const file of [...s.loaded.watch, ...failedReads]) ctx.addWatchFile(file);
|
|
921
|
+
fail(ctx, error, {});
|
|
922
|
+
}
|
|
923
|
+
configChanged = false;
|
|
924
|
+
failedReads = /* @__PURE__ */ new Set();
|
|
925
|
+
setSystem(s, loaded);
|
|
926
|
+
cache.clear();
|
|
927
|
+
diskCache = void 0;
|
|
928
|
+
scans.clear();
|
|
929
|
+
kept.clear();
|
|
930
|
+
}
|
|
931
|
+
/** A hook context's resolver, through the dev server's memo. */
|
|
932
|
+
function devResolver(ctx) {
|
|
933
|
+
return memoize(resolutions, ctx.environment.name, contextResolver(ready(), ctx));
|
|
934
|
+
}
|
|
935
|
+
/** A hook context's resolver, through its build environment's memo. */
|
|
936
|
+
function buildResolver(ctx) {
|
|
937
|
+
let resolved = buildResolutions.get(ctx.environment);
|
|
938
|
+
if (!resolved) buildResolutions.set(ctx.environment, resolved = /* @__PURE__ */ new Map());
|
|
939
|
+
return memoize(resolved, "", contextResolver(ready(), ctx));
|
|
940
|
+
}
|
|
941
|
+
let refresh;
|
|
942
|
+
/**
|
|
943
|
+
* A module compiled after `base.css` was served brought new sub-layers.
|
|
944
|
+
* Stale copies go at once, so the next request prints the new statement;
|
|
945
|
+
* the browser's copy is replaced on the next tick, after a burst of
|
|
946
|
+
* transforms (a lazy route's modules) has settled.
|
|
947
|
+
*/
|
|
948
|
+
function baseChanged() {
|
|
949
|
+
if (!server) return;
|
|
950
|
+
const base = ready().ids.base;
|
|
951
|
+
for (const env of Object.values(server.environments)) for (const mod of env.moduleGraph.getModulesByFile(base) ?? []) env.moduleGraph.invalidateModule(mod);
|
|
952
|
+
refresh ??= setTimeout(() => {
|
|
953
|
+
refresh = void 0;
|
|
954
|
+
const client = server?.environments.client;
|
|
955
|
+
for (const mod of client?.moduleGraph.getModulesByFile(base) ?? []) client.reloadModule(mod);
|
|
956
|
+
}, 30).unref();
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* A transform compiled other atoms than the registry held, after the
|
|
960
|
+
* module's CSS was served from them: two saves a few ms apart that the
|
|
961
|
+
* watcher reported once, or an HMR compile of a half-written file. The
|
|
962
|
+
* served copies go, and the browser's is replaced.
|
|
963
|
+
*/
|
|
964
|
+
function cssChanged(id) {
|
|
965
|
+
for (const env of Object.values(server?.environments ?? {})) for (const mod of env.moduleGraph.getModulesByFile(fileOf(id) + ".vicss.css") ?? []) {
|
|
966
|
+
if (!mod.transformResult) continue;
|
|
967
|
+
env.moduleGraph.invalidateModule(mod);
|
|
968
|
+
if (env === server?.environments.client) env.reloadModule(mod);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/** Returns whether `base.css` changed. */
|
|
972
|
+
function register(id, styles) {
|
|
973
|
+
return isStyled(styles) ? registry.set(id, styles) : registry.unstyled(id, styles.dependencies);
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Compiles `id` again outside its transform, from `code` or the file, and
|
|
977
|
+
* keeps the result for that transform. Returns whether the layer statement
|
|
978
|
+
* changed, or undefined when the module does not compile: its own
|
|
979
|
+
* transform runs again and reports why. The file is read synchronously:
|
|
980
|
+
* the first async read after a change waits behind the watcher for
|
|
981
|
+
* 15–85 ms (report 06 §6.2).
|
|
982
|
+
*/
|
|
983
|
+
async function recompile(id, code, resolve) {
|
|
984
|
+
if (isSfc(id)) return void 0;
|
|
985
|
+
try {
|
|
986
|
+
const source = code ?? readFileSync(id, "utf8");
|
|
987
|
+
const result = compile(ready().compile, id, source, resolve);
|
|
988
|
+
kept.set(id, {
|
|
989
|
+
code: source,
|
|
990
|
+
result
|
|
991
|
+
});
|
|
992
|
+
return register(id, await result);
|
|
993
|
+
} catch {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
async function applyChange(file, type, read, resolve) {
|
|
998
|
+
const styled = [];
|
|
999
|
+
let base = false;
|
|
1000
|
+
if (SCRIPT_ID.test(file) && !file.includes("/node_modules/")) {
|
|
1001
|
+
if (type === "delete") {
|
|
1002
|
+
kept.delete(file);
|
|
1003
|
+
base = registry.delete(file);
|
|
1004
|
+
} else {
|
|
1005
|
+
const code = await read();
|
|
1006
|
+
if (registry.has(file) || sources.some((source) => code.includes(source))) {
|
|
1007
|
+
const changed = await recompile(file, code, resolve);
|
|
1008
|
+
if (changed !== void 0) {
|
|
1009
|
+
base ||= changed;
|
|
1010
|
+
styled.push(file);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const dependants = registry.dependantsOf(file).filter((id) => id !== file);
|
|
1016
|
+
const changed = await inBatches(dependants, (id) => recompile(id, void 0, resolve));
|
|
1017
|
+
for (const [i, id] of dependants.entries()) {
|
|
1018
|
+
const layersChanged = changed[i];
|
|
1019
|
+
if (layersChanged === void 0) continue;
|
|
1020
|
+
base ||= layersChanged;
|
|
1021
|
+
styled.push(id);
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
styled,
|
|
1025
|
+
dependants,
|
|
1026
|
+
base
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
/** The build's scan, started by the first environment that needs it. */
|
|
1030
|
+
function scanned(ctx) {
|
|
1031
|
+
return startScan(ctx).result;
|
|
1032
|
+
}
|
|
1033
|
+
function startScan(ctx) {
|
|
1034
|
+
const s = ready();
|
|
1035
|
+
const key = s.configHash;
|
|
1036
|
+
const running = scans.get(key);
|
|
1037
|
+
if (running) return running;
|
|
1038
|
+
let allQueued;
|
|
1039
|
+
const queued = new Promise((resolve) => allQueued = resolve);
|
|
1040
|
+
const cacheFile = s.diskCache && path.join(s.config.cacheDir, "vicss", "scan.json");
|
|
1041
|
+
const resolve = buildResolver(ctx);
|
|
1042
|
+
const resolveId = (source, importer) => resolveFile(resolve, source, importer);
|
|
1043
|
+
const setup = workerSetup(s);
|
|
1044
|
+
const promise = (async () => {
|
|
1045
|
+
if (cacheFile) await (diskCache ??= cache.read(cacheFile, s.root));
|
|
1046
|
+
const result = await scan({
|
|
1047
|
+
root: s.root,
|
|
1048
|
+
system: s.loaded.system,
|
|
1049
|
+
configHash: key,
|
|
1050
|
+
importSources: s.compile.importSources,
|
|
1051
|
+
cache,
|
|
1052
|
+
resolve: resolveId,
|
|
1053
|
+
compile: (id, code, script) => compile(s.compile, id, code, resolve, script),
|
|
1054
|
+
queued(id, code, result, prioritize) {
|
|
1055
|
+
kept.set(id, {
|
|
1056
|
+
code: hashCode(code),
|
|
1057
|
+
result,
|
|
1058
|
+
prioritize
|
|
1059
|
+
});
|
|
1060
|
+
},
|
|
1061
|
+
allQueued,
|
|
1062
|
+
cached(id, code, entry) {
|
|
1063
|
+
kept.set(id, {
|
|
1064
|
+
code: hashCode(code),
|
|
1065
|
+
result: Promise.resolve(compiledOf(entry, id, code))
|
|
1066
|
+
});
|
|
1067
|
+
},
|
|
1068
|
+
pool: setup ? (jobs) => {
|
|
1069
|
+
const pool = spawned ?? CompilePool.spawn(poolSize(jobs));
|
|
1070
|
+
spawned = void 0;
|
|
1071
|
+
return pool?.use({
|
|
1072
|
+
setup,
|
|
1073
|
+
resolve: resolveId,
|
|
1074
|
+
warn: (message) => s.config.logger.warn(`vicss: ${message}`)
|
|
1075
|
+
});
|
|
1076
|
+
} : void 0
|
|
1077
|
+
});
|
|
1078
|
+
if (cacheFile) {
|
|
1079
|
+
const write = async () => {
|
|
1080
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1081
|
+
try {
|
|
1082
|
+
await cache.write(cacheFile, s.root, result.files);
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
s.config.logger.warn(`vicss: could not write ${cacheFile}: ${String(error)}`);
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
cacheWrites.push(write());
|
|
1088
|
+
}
|
|
1089
|
+
return result;
|
|
1090
|
+
})();
|
|
1091
|
+
promise.then(allQueued, allQueued);
|
|
1092
|
+
const unused = () => {
|
|
1093
|
+
spawned?.close();
|
|
1094
|
+
spawned = void 0;
|
|
1095
|
+
};
|
|
1096
|
+
promise.then(unused, unused);
|
|
1097
|
+
const run = {
|
|
1098
|
+
queued,
|
|
1099
|
+
result: promise
|
|
1100
|
+
};
|
|
1101
|
+
scans.set(key, run);
|
|
1102
|
+
return run;
|
|
1103
|
+
}
|
|
1104
|
+
function sheetOf(result) {
|
|
1105
|
+
return printed(result).css;
|
|
1106
|
+
}
|
|
1107
|
+
function printed(result) {
|
|
1108
|
+
let sheet = sheets.get(result);
|
|
1109
|
+
if (!sheet) {
|
|
1110
|
+
const s = ready();
|
|
1111
|
+
const failed = sheetErrors.get(result);
|
|
1112
|
+
if (failed) throw failed.error;
|
|
1113
|
+
try {
|
|
1114
|
+
const all = withLibraries(result, s.loaded.libraries, s.loaded.system);
|
|
1115
|
+
const options = s.loaded.system.classNames;
|
|
1116
|
+
const names = createNamer(identitiesOf(all, result.names), {
|
|
1117
|
+
prefix: options.prefix,
|
|
1118
|
+
minLength: minLengthOf(options, s.library)
|
|
1119
|
+
}, s.loaded.libraries.map((library) => ({
|
|
1120
|
+
library: library.name,
|
|
1121
|
+
names: library.classes
|
|
1122
|
+
})), readableBuild(options));
|
|
1123
|
+
sheet = {
|
|
1124
|
+
css: buildCss(all, {
|
|
1125
|
+
...s.print,
|
|
1126
|
+
names
|
|
1127
|
+
}, s.tokensFile),
|
|
1128
|
+
atoms: all.atoms.length,
|
|
1129
|
+
bases: all.rules.filter((rule) => rule.kind === "base").length,
|
|
1130
|
+
names,
|
|
1131
|
+
identities: identitiesOf(result, result.names).filter((identity) => !identity.startsWith("!"))
|
|
1132
|
+
};
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
sheetErrors.set(result, { error });
|
|
1135
|
+
throw error;
|
|
1136
|
+
}
|
|
1137
|
+
sheets.set(result, sheet);
|
|
1138
|
+
}
|
|
1139
|
+
return sheet;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* The build's scan and the CSS it prints, or their failure as this hook's
|
|
1143
|
+
* error. Every hook that waits for a failed scan sees the same error: the
|
|
1144
|
+
* first reports it, with its code frame, and the others get `undefined`
|
|
1145
|
+
* and go on quietly, so the build fails once.
|
|
1146
|
+
*/
|
|
1147
|
+
async function sheetFor(ctx) {
|
|
1148
|
+
try {
|
|
1149
|
+
const scan = await scanned(ctx);
|
|
1150
|
+
const { css, names } = printed(scan);
|
|
1151
|
+
return {
|
|
1152
|
+
scan,
|
|
1153
|
+
css,
|
|
1154
|
+
names
|
|
1155
|
+
};
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
if (typeof error === "object" && error !== null) {
|
|
1158
|
+
if (reportedFailures.has(error)) return void 0;
|
|
1159
|
+
reportedFailures.add(error);
|
|
1160
|
+
}
|
|
1161
|
+
fail(ctx, error, { id: error instanceof ScanConflict ? error.id : void 0 });
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* The build's one summary line (spec P12), with the sizes of the files
|
|
1166
|
+
* written, or else of the sheet vicss printed.
|
|
1167
|
+
*/
|
|
1168
|
+
function summarize(result, files) {
|
|
1169
|
+
if (reported.has(result)) return;
|
|
1170
|
+
reported.add(result);
|
|
1171
|
+
const kb = (n) => `${(n / 1e3).toFixed(2)} kB`;
|
|
1172
|
+
const size = (text) => `${kb(Buffer.byteLength(text))} │ gzip: ${kb(gzipSync(text).length)}`;
|
|
1173
|
+
const sizes = files ? files.map(([name, text]) => `${name} ${size(text)}`) : [`sheet ${size(sheetOf(result).sheet)} (unminified)`];
|
|
1174
|
+
const { logger } = ready().config;
|
|
1175
|
+
const own = new Set(result.atoms.map((atom) => atom.className));
|
|
1176
|
+
const every = [...result.atoms, ...baseAtoms(result.rules).filter((atom) => !own.has(atom.className))];
|
|
1177
|
+
const hashed = every.filter(isArbitrary).length;
|
|
1178
|
+
const share = every.length === 0 ? 0 : hashed / every.length;
|
|
1179
|
+
const percent = `${(share * 100).toFixed(1)} %`;
|
|
1180
|
+
const { atoms, bases } = printed(result);
|
|
1181
|
+
const flat = bases === 0 ? "" : `, ${bases} flattened base${bases === 1 ? "" : "s"}`;
|
|
1182
|
+
logger.info(`vicss: ${atoms} atoms${flat}, ${result.files.length} files scanned; ${result.arbitrary} values outside the scales, ${hashed} hashed atoms (${percent}); ${sizes.join(", ")}`);
|
|
1183
|
+
if (share > HASHED_BUDGET) logger.warn(`vicss: ${percent} of the atoms (${hashed} of ${every.length}) get hashed class names (\`~…\`): values outside the scales and long or unusual literals; the budget is ${HASHED_BUDGET * 100} %. \`vicss check\` lists them by file.`);
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Compiles a module (a script, or a framework file's compiled output),
|
|
1187
|
+
* registers its styles (dev) or checks them against the sheet (build), and
|
|
1188
|
+
* imports their CSS.
|
|
1189
|
+
*/
|
|
1190
|
+
async function transformStyled(ctx, code, id) {
|
|
1191
|
+
if (id.includes("/node_modules/") || !sources.some((source) => code.includes(source))) return null;
|
|
1192
|
+
const s = ready();
|
|
1193
|
+
if (!s.dev) await startScan(ctx).queued;
|
|
1194
|
+
const reused = kept.get(id);
|
|
1195
|
+
let result;
|
|
1196
|
+
/** Whether the scan compiled it, so its atoms, rules and tokens are the sheet's. */
|
|
1197
|
+
let fromScan = false;
|
|
1198
|
+
try {
|
|
1199
|
+
const resolve = s.dev ? devResolver(ctx) : buildResolver(ctx);
|
|
1200
|
+
if (reused && reused.code === (s.dev ? code : hashCode(code))) {
|
|
1201
|
+
if (s.dev) kept.delete(id);
|
|
1202
|
+
reused.prioritize?.();
|
|
1203
|
+
const earlier = await reused.result.catch(async (error) => {
|
|
1204
|
+
if (!await sheetFor(ctx)) return void 0;
|
|
1205
|
+
throw error;
|
|
1206
|
+
});
|
|
1207
|
+
if (!earlier) return null;
|
|
1208
|
+
const file = (source, importer) => resolveFile(resolve, source, importer);
|
|
1209
|
+
if (s.dev || await resolvesAlike(earlier.resolutions, file)) {
|
|
1210
|
+
result = earlier;
|
|
1211
|
+
fromScan = !s.dev;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
const lines = s.compile.dev && isSfc(id) ? mappedLines(ctx, id) : void 0;
|
|
1215
|
+
result ??= await compile(s.compile, id, code, resolve, void 0, lines);
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
if (isSfc(id) && error instanceof VicssError && !(error instanceof ScanConflict)) {
|
|
1218
|
+
await failInComponent(ctx, s, id, error);
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
fail(ctx, error, error instanceof ScanConflict ? { id: error.id } : {
|
|
1222
|
+
id,
|
|
1223
|
+
code
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
for (const message of result.warnings) ctx.warn(message);
|
|
1227
|
+
for (const dep of result.dependencies) ctx.addWatchFile(dep);
|
|
1228
|
+
const styled = isStyled(result);
|
|
1229
|
+
if (s.dev) {
|
|
1230
|
+
if (styled && isSfc(id)) await checkTemplate(ctx, s, id, result);
|
|
1231
|
+
const before = registry.get(id);
|
|
1232
|
+
if (register(id, result)) baseChanged();
|
|
1233
|
+
if (before && before !== result && !sameCss(before, result)) cssChanged(id);
|
|
1234
|
+
if (!result.changed) return null;
|
|
1235
|
+
transformed.add(id);
|
|
1236
|
+
} else {
|
|
1237
|
+
if (!result.changed) return null;
|
|
1238
|
+
if (styled && !fromScan) {
|
|
1239
|
+
const sheet = await sheetFor(ctx);
|
|
1240
|
+
if (!sheet) return null;
|
|
1241
|
+
checkSheet(ctx, sheet.scan, sheet.css.tokenKeys, result, id, s.root);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
let out = result.code;
|
|
1245
|
+
if (styled) out += cssImports(s, id);
|
|
1246
|
+
return {
|
|
1247
|
+
code: out,
|
|
1248
|
+
map: result.map
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* A framework file's compiled output did not compile. Its location is in
|
|
1253
|
+
* code the author never wrote (the virtual script module in a build), so
|
|
1254
|
+
* the error is reported against the component. A build's scan compiled
|
|
1255
|
+
* the file's scripts, and a failure there is the build's one error, which
|
|
1256
|
+
* the first hook to see it reports (`sheetFor`): this returns quietly
|
|
1257
|
+
* once another has. In dev the scripts compile again, as `checkTemplate`
|
|
1258
|
+
* compiles them, and their error is reported at its place in the file.
|
|
1259
|
+
* What fails only in the compiled output (a style in the template) is
|
|
1260
|
+
* located through the framework's source map, by line.
|
|
1261
|
+
*/
|
|
1262
|
+
async function failInComponent(ctx, s, id, error) {
|
|
1263
|
+
if (!s.dev && !await sheetFor(ctx)) return;
|
|
1264
|
+
const file = fileOf(id);
|
|
1265
|
+
const source = readSource(file);
|
|
1266
|
+
if (source === void 0) fail(ctx, error, { id });
|
|
1267
|
+
if (s.dev) {
|
|
1268
|
+
const resolve = devResolver(ctx);
|
|
1269
|
+
try {
|
|
1270
|
+
await Promise.all(extractScripts(file, source).map(({ code: text, ...script }) => compile(s.compile, file, text, resolve, script)));
|
|
1271
|
+
} catch (own) {
|
|
1272
|
+
fail(ctx, own, {
|
|
1273
|
+
id: file,
|
|
1274
|
+
code: source
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const line = error.loc && mappedLines(ctx, id)?.(error.loc.line, error.loc.column);
|
|
1279
|
+
if (line === void 0) fail(ctx, error, { id });
|
|
1280
|
+
const hint = error.hint === void 0 ? {} : { hint: error.hint };
|
|
1281
|
+
fail(ctx, new VicssError(error.code, error.message, {
|
|
1282
|
+
...hint,
|
|
1283
|
+
loc: {
|
|
1284
|
+
file,
|
|
1285
|
+
line,
|
|
1286
|
+
column: 0
|
|
1287
|
+
}
|
|
1288
|
+
}), {
|
|
1289
|
+
id: file,
|
|
1290
|
+
code: source
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Dev: a style written in a framework file's template works here, yet the
|
|
1295
|
+
* build's pre-scan reads only the file's scripts, and its safety net fails
|
|
1296
|
+
* the build (§5.4). It fails now, with the build's hint.
|
|
1297
|
+
*/
|
|
1298
|
+
async function checkTemplate(ctx, s, id, result) {
|
|
1299
|
+
const file = fileOf(id);
|
|
1300
|
+
let code;
|
|
1301
|
+
try {
|
|
1302
|
+
code = readFileSync(file, "utf8");
|
|
1303
|
+
} catch {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const hash = hashCode(code);
|
|
1307
|
+
let scripts = scriptStyles.get(file);
|
|
1308
|
+
if (scripts?.hash !== hash) {
|
|
1309
|
+
const resolve = devResolver(ctx);
|
|
1310
|
+
let parts;
|
|
1311
|
+
try {
|
|
1312
|
+
parts = await Promise.all(extractScripts(file, code).map(({ code: text, ...script }) => compile(s.compile, file, text, resolve, script)));
|
|
1313
|
+
} catch {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
scripts = {
|
|
1317
|
+
hash,
|
|
1318
|
+
styles: mergeStyles(parts)
|
|
1319
|
+
};
|
|
1320
|
+
scriptStyles.set(file, scripts);
|
|
1321
|
+
}
|
|
1322
|
+
const classes = new Set(scripts.styles.atoms.map((atom) => atom.className));
|
|
1323
|
+
const ids = new Set(scripts.styles.rules.map((rule) => rule.id));
|
|
1324
|
+
const rel = path.relative(s.root, file) || file;
|
|
1325
|
+
const own = result.own ?? {
|
|
1326
|
+
classes: result.atoms.map((a) => a.className),
|
|
1327
|
+
rules: result.rules.map((r) => r.id)
|
|
1328
|
+
};
|
|
1329
|
+
const atom = own.classes.find((name) => !classes.has(name));
|
|
1330
|
+
const rule = own.rules.find((ruleId) => !ids.has(ruleId));
|
|
1331
|
+
const [errorCode, what] = atom ? ["MISSING_ATOM", `the class \`${atom}\``] : rule ? ["MISSING_RULE", `the rule \`${rule}\``] : [];
|
|
1332
|
+
if (errorCode) ctx.error({
|
|
1333
|
+
code: errorCode,
|
|
1334
|
+
id,
|
|
1335
|
+
message: `vicss(${errorCode}): ${rel} uses ${what} outside its scripts: ${TEMPLATE_HINT}`
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* What a styled module imports: in dev, `base.css`, the installed
|
|
1340
|
+
* libraries' CSS and its own, in that order (a CSS module's imports run
|
|
1341
|
+
* before it, so a library's sub-layers would come before the statement
|
|
1342
|
+
* if `base.css` imported them); the sheet in an app build; nothing in a
|
|
1343
|
+
* library build, whose consumers print its manifest or link `vicss.css`.
|
|
1344
|
+
*/
|
|
1345
|
+
function cssImports(s, id) {
|
|
1346
|
+
if (s.dev) return `\n${[
|
|
1347
|
+
BASE_SPECIFIER,
|
|
1348
|
+
...s.loaded.libraries.map((_, i) => librarySpecifier(i)),
|
|
1349
|
+
moduleCssSpecifier(id)
|
|
1350
|
+
].map((specifier) => `import "${specifier}";`).join("\n")}\n`;
|
|
1351
|
+
return s.library ? "" : `\nimport "${SHEET_SPECIFIER}";\n`;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* What a module of an installed library imports (PLAN §5.13): its classes
|
|
1355
|
+
* are the sheet's, so wherever the app renders its components, alone or
|
|
1356
|
+
* with no styles of its own, the sheet comes with them. In dev, `base.css`
|
|
1357
|
+
* and the libraries' CSS.
|
|
1358
|
+
*/
|
|
1359
|
+
function libraryImports(s, library) {
|
|
1360
|
+
if (!s.dev) return `\nimport "${SHEET_SPECIFIER}";\n`;
|
|
1361
|
+
let code = `\n${[BASE_SPECIFIER, ...s.loaded.libraries.map((_, i) => librarySpecifier(i))].map((specifier) => `import "${specifier}";`).join("\n")}\n`;
|
|
1362
|
+
const bases = (library?.rules ?? []).filter((rule) => rule.kind === "base");
|
|
1363
|
+
if (bases.length > 0) {
|
|
1364
|
+
const shipped = (identity) => library.classes[identity] ?? identity;
|
|
1365
|
+
const atoms = Object.fromEntries(bases.map((rule) => [shipped(rule.id), Object.fromEntries((rule.atoms ?? []).map((atom) => [mergeIdOf(atom.className), shipped(atom.className)]))]));
|
|
1366
|
+
code += `import { __vxFlatBases } from "${RUNTIME_PACKAGE}";\nObject.assign(__vxFlatBases, ${JSON.stringify(atoms)});\n`;
|
|
1367
|
+
}
|
|
1368
|
+
return code;
|
|
1369
|
+
}
|
|
1370
|
+
const transformFilter = {
|
|
1371
|
+
id: SCRIPT_ID,
|
|
1372
|
+
code: sources
|
|
1373
|
+
};
|
|
1374
|
+
const frameworkFilter = {
|
|
1375
|
+
id: SFC_SCRIPT_ID,
|
|
1376
|
+
code: sources
|
|
1377
|
+
};
|
|
1378
|
+
/** Build: the installed libraries' directories, filled once they load. */
|
|
1379
|
+
const libraryDirs = [];
|
|
1380
|
+
const libraryFilter = { id: libraryDirs };
|
|
1381
|
+
const main = {
|
|
1382
|
+
name: "vicss",
|
|
1383
|
+
enforce: "pre",
|
|
1384
|
+
sharedDuringBuild: true,
|
|
1385
|
+
api: {
|
|
1386
|
+
options,
|
|
1387
|
+
/**
|
|
1388
|
+
* The app's whole stylesheet as one string, for a renderer outside the
|
|
1389
|
+
* browser (`@menapo/og` hands it to takumi, og PLAN D3): in a build,
|
|
1390
|
+
* what the scan prints, so it needs the hook's context; in dev, the
|
|
1391
|
+
* base and every compiled module's atoms, as the dev server serves them.
|
|
1392
|
+
*/
|
|
1393
|
+
async stylesheet(ctx) {
|
|
1394
|
+
const s = ready();
|
|
1395
|
+
if (s.dev) return [
|
|
1396
|
+
devBaseCss(registry, s.print, s.loaded.libraries),
|
|
1397
|
+
...s.loaded.libraries.map((library) => moduleCss(library, s.loaded.system, s.print.names)),
|
|
1398
|
+
...registry.ids().map((id) => moduleCss(registry.get(id), s.loaded.system, s.print.names))
|
|
1399
|
+
].join("\n");
|
|
1400
|
+
if (!ctx) throw new Error("vicss: api.stylesheet() needs a plugin hook's context in a build.");
|
|
1401
|
+
const printed = await sheetFor(ctx);
|
|
1402
|
+
return [printed?.css.tokens ?? "", printed?.css.sheet ?? ""].join("\n");
|
|
1403
|
+
},
|
|
1404
|
+
/**
|
|
1405
|
+
* The literal declarations (core/literals.ts) of the classes `code`, a
|
|
1406
|
+
* module as this plugin's transform left it, names: for a renderer
|
|
1407
|
+
* that cannot read custom properties (`@menapo/email` inlines them,
|
|
1408
|
+
* email PLAN D4). Keyed as the code writes each class, so a key written
|
|
1409
|
+
* back into the module is named with the module's own classes: a build
|
|
1410
|
+
* writes placeholders that `renderChunk` names, dev the class itself.
|
|
1411
|
+
* A build needs a hook's context, as `stylesheet()` does.
|
|
1412
|
+
*/
|
|
1413
|
+
async literals(code, ctx) {
|
|
1414
|
+
const s = ready();
|
|
1415
|
+
const out = {};
|
|
1416
|
+
if (s.dev) {
|
|
1417
|
+
const table = literalsOf([...s.loaded.libraries.flatMap((library) => library.atoms), ...registry.ids().flatMap((id) => registry.get(id).atoms)], [...s.loaded.libraries.flatMap((library) => library.rules), ...registry.rules()], s.loaded.system);
|
|
1418
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1419
|
+
for (const [identity, list] of table) byName.set(s.print.names.name(identity), list);
|
|
1420
|
+
for (const name of classesIn(code, new Set(byName.keys()))) out[name] = byName.get(name);
|
|
1421
|
+
return out;
|
|
1422
|
+
}
|
|
1423
|
+
if (!ctx) throw new Error("vicss: api.literals() needs a plugin hook's context in a build.");
|
|
1424
|
+
const sheet = await sheetFor(ctx);
|
|
1425
|
+
if (!sheet) return out;
|
|
1426
|
+
let table = literalTables.get(sheet.scan);
|
|
1427
|
+
if (!table) {
|
|
1428
|
+
const all = withLibraries(sheet.scan, s.loaded.libraries, s.loaded.system);
|
|
1429
|
+
table = literalsOf(all.atoms, all.rules, s.loaded.system);
|
|
1430
|
+
literalTables.set(sheet.scan, table);
|
|
1431
|
+
}
|
|
1432
|
+
for (const identity of placeholdersIn(code)) {
|
|
1433
|
+
const list = table.get(identity);
|
|
1434
|
+
if (list) out[placeholder(identity)] = list;
|
|
1435
|
+
}
|
|
1436
|
+
return out;
|
|
1437
|
+
}
|
|
1438
|
+
},
|
|
1439
|
+
async configResolved(config) {
|
|
1440
|
+
if (state && !state.dev && config.command === "build" && !session.started) {
|
|
1441
|
+
session.configs++;
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
if (layers) outerLayers(layers);
|
|
1445
|
+
const dev = config.command === "serve";
|
|
1446
|
+
if (dev && config.experimental.bundledDev) config.logger.warn("vicss: experimental.bundledDev is not supported yet; CSS updates may arrive late or not at all");
|
|
1447
|
+
const html = hasHtmlEntry(config);
|
|
1448
|
+
const split = config.environments.client?.build.cssCodeSplit !== false;
|
|
1449
|
+
if (!dev && options.tokensFile === true && !html) config.logger.warn("vicss: tokensFile: true needs an HTML entry in the client build to link the file; the tokens stay at the start of the sheet");
|
|
1450
|
+
else if (!dev && options.tokensFile === true && !split) config.logger.warn("vicss: tokensFile: true needs build.cssCodeSplit; with one CSS file for the app, the tokens stay at the start of the sheet");
|
|
1451
|
+
const root = normalizePath(config.root);
|
|
1452
|
+
spawned?.close();
|
|
1453
|
+
spawned = void 0;
|
|
1454
|
+
if (!dev && !existsSync(path.join(config.cacheDir, "vicss", "scan.json"))) {
|
|
1455
|
+
const files = await listFiles(root);
|
|
1456
|
+
const estimate = files.length > 200 ? estimateStyled(files, [RUNTIME_PACKAGE]) : 0;
|
|
1457
|
+
if (estimate > 200) spawned = CompilePool.spawn(poolSize(estimate));
|
|
1458
|
+
}
|
|
1459
|
+
const critical = criticalSettings(options.critical);
|
|
1460
|
+
if (critical && !dev && config.build?.lib) config.logger.warn("vicss: critical needs pages, and a library build has none; it does nothing here");
|
|
1461
|
+
else if (critical && !dev && !html) config.logger.info("vicss: critical: this build emits no HTML page; once your framework has written its pages, run `vicss critical <outDir>`");
|
|
1462
|
+
if (options.framework !== void 0 && !FRAMEWORKS.includes(options.framework)) throw new Error(`vicss: framework must be one of ${FRAMEWORKS.join(", ")}; got ${JSON.stringify(options.framework)}`);
|
|
1463
|
+
const s = {
|
|
1464
|
+
config,
|
|
1465
|
+
root,
|
|
1466
|
+
ids: cssIds(root),
|
|
1467
|
+
dev,
|
|
1468
|
+
devSource: dev && !isTestRunner(config),
|
|
1469
|
+
loaded: void 0,
|
|
1470
|
+
compile: void 0,
|
|
1471
|
+
print: void 0,
|
|
1472
|
+
tokensFile: options.tokensFile !== false && html && split,
|
|
1473
|
+
library: !dev && Boolean(config.build?.lib),
|
|
1474
|
+
configHash: "",
|
|
1475
|
+
diskCache: false,
|
|
1476
|
+
frameworks: {
|
|
1477
|
+
scripts: options.framework ?? detectFramework(root) ?? "vanilla",
|
|
1478
|
+
jsxImportSource: configuredJsxSource(config, root)
|
|
1479
|
+
},
|
|
1480
|
+
pkg: ownPackageName(root)
|
|
1481
|
+
};
|
|
1482
|
+
let loaded;
|
|
1483
|
+
try {
|
|
1484
|
+
loaded = checkLibraries(s, await loadSystem(root, options.config, (m) => config.logger.info(m)));
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
throw loadFailure(error, root, options.config);
|
|
1487
|
+
}
|
|
1488
|
+
setSystem(s, loaded);
|
|
1489
|
+
for (const filter of [transformFilter, frameworkFilter]) if (s.dev) delete filter.code;
|
|
1490
|
+
else filter.code = sources;
|
|
1491
|
+
if (s.dev) delete libraryFilter.id;
|
|
1492
|
+
else libraryFilter.id = libraryDirs;
|
|
1493
|
+
if (state) {
|
|
1494
|
+
registry.clear();
|
|
1495
|
+
cache.clear();
|
|
1496
|
+
diskCache = void 0;
|
|
1497
|
+
scans.clear();
|
|
1498
|
+
kept.clear();
|
|
1499
|
+
transformed.clear();
|
|
1500
|
+
scriptStyles.clear();
|
|
1501
|
+
changes.clear();
|
|
1502
|
+
resolutions.clear();
|
|
1503
|
+
}
|
|
1504
|
+
session = newSession();
|
|
1505
|
+
state = s;
|
|
1506
|
+
},
|
|
1507
|
+
configureServer(dev) {
|
|
1508
|
+
server = dev;
|
|
1509
|
+
const s = ready();
|
|
1510
|
+
let release;
|
|
1511
|
+
closing = new Promise((resolve) => release = resolve);
|
|
1512
|
+
const client = dev.environments.client;
|
|
1513
|
+
if (client) {
|
|
1514
|
+
const close = client.close.bind(client);
|
|
1515
|
+
client.close = () => {
|
|
1516
|
+
release();
|
|
1517
|
+
return close();
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
const forget = () => resolutions.clear();
|
|
1521
|
+
for (const event of [
|
|
1522
|
+
"add",
|
|
1523
|
+
"unlink",
|
|
1524
|
+
"addDir",
|
|
1525
|
+
"unlinkDir"
|
|
1526
|
+
]) dev.watcher.on(event, forget);
|
|
1527
|
+
dev.watcher.on("change", (file) => {
|
|
1528
|
+
if (/(?:^|[/\\])(?:package|[jt]sconfig[^/\\]*)\.json$/.test(file)) forget();
|
|
1529
|
+
});
|
|
1530
|
+
const candidates = options.config ? [] : CONFIG_FILES.map((name) => normalizePath(path.join(s.root, name)));
|
|
1531
|
+
dev.watcher.add([...s.loaded.watch, ...candidates]);
|
|
1532
|
+
let reloading = Promise.resolve();
|
|
1533
|
+
let queued = false;
|
|
1534
|
+
const onChange = (file) => {
|
|
1535
|
+
const id = normalizePath(file);
|
|
1536
|
+
const watched = ready().loaded.watch.includes(id) || candidates.includes(id) || failedReads.has(id);
|
|
1537
|
+
if (queued || !watched) return;
|
|
1538
|
+
queued = true;
|
|
1539
|
+
reloading = reloading.then(() => {
|
|
1540
|
+
queued = false;
|
|
1541
|
+
return reloadConfig(dev).catch((error) => {
|
|
1542
|
+
ready().config.logger.error(`vicss: ${String(error)}`, { timestamp: true });
|
|
1543
|
+
});
|
|
1544
|
+
});
|
|
1545
|
+
};
|
|
1546
|
+
dev.watcher.on("change", onChange);
|
|
1547
|
+
dev.watcher.on("add", onChange);
|
|
1548
|
+
dev.watcher.on("unlink", onChange);
|
|
1549
|
+
},
|
|
1550
|
+
async buildStart() {
|
|
1551
|
+
const s = ready();
|
|
1552
|
+
if (s.dev) return;
|
|
1553
|
+
session.started = true;
|
|
1554
|
+
if (this.meta.watchMode) {
|
|
1555
|
+
if (configChanged) await reloadBuildConfig(this, s);
|
|
1556
|
+
for (const file of [...s.loaded.watch, ...failedReads]) this.addWatchFile(file);
|
|
1557
|
+
}
|
|
1558
|
+
buildResolutions.delete(this.environment);
|
|
1559
|
+
const run = startScan(this);
|
|
1560
|
+
if (this.meta.watchMode) {
|
|
1561
|
+
await run.result.catch(() => {});
|
|
1562
|
+
const sheet = await sheetFor(this);
|
|
1563
|
+
for (const file of sheet?.scan.files ?? []) this.addWatchFile(file);
|
|
1564
|
+
}
|
|
1565
|
+
if (s.tokensFile && this.environment.config.consumer === "client") {
|
|
1566
|
+
this.emitFile({
|
|
1567
|
+
type: "chunk",
|
|
1568
|
+
id: s.ids.tokens,
|
|
1569
|
+
name: TOKENS_CHUNK_NAME
|
|
1570
|
+
});
|
|
1571
|
+
tokenAssets.set(this.environment, { config: this.environment.config });
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
watchChange(id) {
|
|
1575
|
+
scans.clear();
|
|
1576
|
+
kept.clear();
|
|
1577
|
+
const file = normalizePath(id);
|
|
1578
|
+
if (state && !state.dev && (state.loaded.watch.includes(file) || failedReads.has(file))) configChanged = true;
|
|
1579
|
+
},
|
|
1580
|
+
resolveId: {
|
|
1581
|
+
filter: { id: CSS_ID },
|
|
1582
|
+
handler(source, importer) {
|
|
1583
|
+
const s = ready();
|
|
1584
|
+
const [file, query] = splitQuery(source);
|
|
1585
|
+
if (file === "virtual:vicss/base.css" || file === "/@vicss/base.css" || file === s.ids.base) return s.dev ? s.ids.base + query : null;
|
|
1586
|
+
if (file === "virtual:vicss/sheet.css" || file === "/@vicss/sheet.css" || file === s.ids.sheet) return s.dev ? null : s.ids.sheet + query;
|
|
1587
|
+
if (file === "/@vicss/tokens.css" || file === s.ids.tokens) return s.dev || !s.tokensFile ? null : s.ids.tokens + query;
|
|
1588
|
+
const library = /(?:^virtual:vicss\/lib\/|\/@vicss\/lib\/)(\d+)\.css$/.exec(file);
|
|
1589
|
+
if (library) return s.dev ? `${s.ids.libraries}${library[1]}.css${query}` : null;
|
|
1590
|
+
if (!s.dev || !file.endsWith(".vicss.css")) return null;
|
|
1591
|
+
return moduleCssId(s.root, file, importer) + query;
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1594
|
+
load: {
|
|
1595
|
+
filter: { id: CSS_ID },
|
|
1596
|
+
async handler(id) {
|
|
1597
|
+
const s = ready();
|
|
1598
|
+
const [file] = splitQuery(id);
|
|
1599
|
+
if (file === s.ids.sheet && !s.dev) return (await sheetFor(this))?.css.sheet ?? "";
|
|
1600
|
+
if (file === s.ids.tokens && !s.dev) return (await sheetFor(this))?.css.tokens ?? "";
|
|
1601
|
+
if (!s.dev) return null;
|
|
1602
|
+
if (file.startsWith(s.ids.libraries)) {
|
|
1603
|
+
const library = s.loaded.libraries[Number(file.slice(s.ids.libraries.length, -4))];
|
|
1604
|
+
return library ? moduleCss(library, s.loaded.system, s.print.names) : "";
|
|
1605
|
+
}
|
|
1606
|
+
if (file === s.ids.base) {
|
|
1607
|
+
const env = this.environment;
|
|
1608
|
+
if (env.mode === "dev" && env.config.consumer === "client") await Promise.race([env.waitForRequestsIdle(id), closing]);
|
|
1609
|
+
await rebuilt;
|
|
1610
|
+
try {
|
|
1611
|
+
return devBaseCss(registry, s.print, s.loaded.libraries);
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
return fail(this, error, { id });
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
if (!file.endsWith(".vicss.css")) return null;
|
|
1617
|
+
const source = file.slice(0, -10);
|
|
1618
|
+
let styles = registry.ofFile(source);
|
|
1619
|
+
if (!styles) {
|
|
1620
|
+
if (!(SCRIPT_ID.test(source) || isSfc(source)) || source.includes("/node_modules/") || !isFileLoadingAllowed(s.config, source)) return "";
|
|
1621
|
+
let code;
|
|
1622
|
+
try {
|
|
1623
|
+
code = await readFile(source, "utf8");
|
|
1624
|
+
} catch {
|
|
1625
|
+
return "";
|
|
1626
|
+
}
|
|
1627
|
+
let result;
|
|
1628
|
+
try {
|
|
1629
|
+
const resolve = devResolver(this);
|
|
1630
|
+
result = isSfc(source) ? mergeStyles(await Promise.all(extractScripts(source, code).map(({ code: script, ...options }) => compile(s.compile, source, script, resolve, options)))) : await compile(s.compile, source, code, resolve);
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
fail(this, error, {
|
|
1633
|
+
id: source,
|
|
1634
|
+
code
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
for (const dep of result.dependencies) this.addWatchFile(dep);
|
|
1638
|
+
if (register(source, result)) baseChanged();
|
|
1639
|
+
styles = result;
|
|
1640
|
+
}
|
|
1641
|
+
this.addWatchFile(source);
|
|
1642
|
+
return moduleCss(styles, s.loaded.system, s.print.names);
|
|
1643
|
+
}
|
|
1644
|
+
},
|
|
1645
|
+
transform: {
|
|
1646
|
+
order: "pre",
|
|
1647
|
+
filter: transformFilter,
|
|
1648
|
+
handler(code, id) {
|
|
1649
|
+
if (isSfc(id)) return null;
|
|
1650
|
+
return transformStyled(this, code, id);
|
|
1651
|
+
}
|
|
1652
|
+
},
|
|
1653
|
+
async hotUpdate(ctx) {
|
|
1654
|
+
const s = ready();
|
|
1655
|
+
const file = normalizePath(ctx.file);
|
|
1656
|
+
if (!s.dev || s.loaded.watch.includes(file) || DECLARATION_ID.test(file)) return;
|
|
1657
|
+
const key = `${ctx.timestamp}:${file}`;
|
|
1658
|
+
let change = changes.get(key);
|
|
1659
|
+
if (!change) {
|
|
1660
|
+
for (const [k, c] of changes) if (c.at < ctx.timestamp - 1e4) changes.delete(k);
|
|
1661
|
+
const read = () => readFileSync(file, "utf8") || ctx.read();
|
|
1662
|
+
const resolve = memoize(resolutions, this.environment.name, envResolver(s, this.environment));
|
|
1663
|
+
const done = applyChange(file, ctx.type, read, resolve);
|
|
1664
|
+
changes.set(key, change = {
|
|
1665
|
+
at: ctx.timestamp,
|
|
1666
|
+
done
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
const { styled, dependants, base } = await change.done;
|
|
1670
|
+
if (styled.length === 0 && dependants.length === 0 && !base) return;
|
|
1671
|
+
const graph = this.environment.moduleGraph;
|
|
1672
|
+
const updates = new Set(ctx.modules);
|
|
1673
|
+
const files = [
|
|
1674
|
+
...dependants.map(fileOf),
|
|
1675
|
+
...styled.map((id) => fileOf(id) + MODULE_SUFFIX),
|
|
1676
|
+
...base ? [s.ids.base] : []
|
|
1677
|
+
];
|
|
1678
|
+
for (const updated of files) for (const mod of graph.getModulesByFile(updated) ?? []) {
|
|
1679
|
+
graph.invalidateModule(mod, /* @__PURE__ */ new Set(), ctx.timestamp, true);
|
|
1680
|
+
updates.add(mod);
|
|
1681
|
+
}
|
|
1682
|
+
return [...updates];
|
|
1683
|
+
},
|
|
1684
|
+
/**
|
|
1685
|
+
* Compiled code names classes by placeholder (core/names.ts): the
|
|
1686
|
+
* build's scan knows every class by now, the sheet's load having waited
|
|
1687
|
+
* for it, so each chunk gets the names the sheet prints (PLAN D31).
|
|
1688
|
+
*/
|
|
1689
|
+
async renderChunk(code, chunk) {
|
|
1690
|
+
if (state?.dev !== false || !hasPlaceholders(code)) return null;
|
|
1691
|
+
const sheet = await sheetFor(this);
|
|
1692
|
+
if (!sheet) return null;
|
|
1693
|
+
const edits = new SourceEdits(code);
|
|
1694
|
+
for (const { start, end, text } of renameClasses(code, (identity) => sheet.names.name(identity))) edits.overwrite(start, end, text);
|
|
1695
|
+
const file = chunk.fileName;
|
|
1696
|
+
return {
|
|
1697
|
+
code: edits.toString(),
|
|
1698
|
+
map: this.environment.config.build.sourcemap ? edits.generateMap({
|
|
1699
|
+
source: file,
|
|
1700
|
+
file,
|
|
1701
|
+
hires: true
|
|
1702
|
+
}) : null
|
|
1703
|
+
};
|
|
1704
|
+
},
|
|
1705
|
+
async generateBundle(_options, bundle) {
|
|
1706
|
+
if (state?.dev !== false) return;
|
|
1707
|
+
if (state.library && this.environment.config.build?.lib) {
|
|
1708
|
+
const s = state;
|
|
1709
|
+
const sheet = await sheetFor(this);
|
|
1710
|
+
if (!sheet) return;
|
|
1711
|
+
const result = sheet.scan;
|
|
1712
|
+
const classes = Object.fromEntries(printed(result).identities.map((identity) => [identity, sheet.names.name(identity)]));
|
|
1713
|
+
const manifest = createManifest(result, s.loaded.system, s.pkg, classes);
|
|
1714
|
+
this.emitFile({
|
|
1715
|
+
type: "asset",
|
|
1716
|
+
fileName: MANIFEST_FILE,
|
|
1717
|
+
source: `${JSON.stringify(manifest, null, 2)}\n`
|
|
1718
|
+
});
|
|
1719
|
+
this.emitFile({
|
|
1720
|
+
type: "asset",
|
|
1721
|
+
fileName: LIBRARY_SHEET_FILE,
|
|
1722
|
+
source: buildCss(result, {
|
|
1723
|
+
...s.print,
|
|
1724
|
+
names: sheet.names
|
|
1725
|
+
}, false).sheet
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
const names = [];
|
|
1729
|
+
const tokens = tokenAssets.get(this.environment);
|
|
1730
|
+
for (const chunk of Object.values(bundle)) {
|
|
1731
|
+
if (chunk.type !== "chunk") continue;
|
|
1732
|
+
const css = [...chunk.viteMetadata?.importedCss ?? []];
|
|
1733
|
+
if (chunk.moduleIds.includes(state.ids.sheet)) names.push(...css);
|
|
1734
|
+
if (tokens && chunk.facadeModuleId === state.ids.tokens) tokens.fileName = css[0];
|
|
1735
|
+
}
|
|
1736
|
+
sheetFiles.set(this.environment, names);
|
|
1737
|
+
},
|
|
1738
|
+
writeBundle(_options, bundle) {
|
|
1739
|
+
const names = [...sheetFiles.get(this.environment) ?? []];
|
|
1740
|
+
const tokens = tokenAssets.get(this.environment)?.fileName;
|
|
1741
|
+
if (!session.scan || names.length === 0) return;
|
|
1742
|
+
if (tokens) names.push(tokens);
|
|
1743
|
+
const files = names.flatMap((name) => {
|
|
1744
|
+
const asset = bundle[name];
|
|
1745
|
+
return asset?.type === "asset" ? [[name, asset.source]] : [];
|
|
1746
|
+
});
|
|
1747
|
+
if (files.length === names.length) summarize(session.scan, files);
|
|
1748
|
+
},
|
|
1749
|
+
async buildEnd(error) {
|
|
1750
|
+
if (error) session.failed = true;
|
|
1751
|
+
if (error || state?.dev !== false) return;
|
|
1752
|
+
session.failed = true;
|
|
1753
|
+
const sheet = await sheetFor(this);
|
|
1754
|
+
if (!sheet) return;
|
|
1755
|
+
session.failed = false;
|
|
1756
|
+
session.scan = sheet.scan;
|
|
1757
|
+
},
|
|
1758
|
+
async closeBundle() {
|
|
1759
|
+
const s = state;
|
|
1760
|
+
if (!s || s.dev || ++session.closed < Math.max(1, session.configs - 1)) return;
|
|
1761
|
+
await Promise.all(cacheWrites.splice(0));
|
|
1762
|
+
kept.clear();
|
|
1763
|
+
if (s.diskCache) {
|
|
1764
|
+
cache.clear();
|
|
1765
|
+
diskCache = void 0;
|
|
1766
|
+
}
|
|
1767
|
+
s.compile = {
|
|
1768
|
+
...s.compile,
|
|
1769
|
+
parsed: createParseCache()
|
|
1770
|
+
};
|
|
1771
|
+
if (session.scan && !session.failed) summarize(session.scan);
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
/**
|
|
1775
|
+
* `.vue`, `.svelte` and `.astro` modules once their framework's plugin has
|
|
1776
|
+
* compiled them to JS (PLAN §5.4): the same compile, CSS and registry as
|
|
1777
|
+
* the `pre` transform. The pre-scan read the files' scripts; a style call
|
|
1778
|
+
* in a template reaches this transform only, and the safety net says so.
|
|
1779
|
+
*/
|
|
1780
|
+
/**
|
|
1781
|
+
* The modules of installed libraries: they bring the CSS their classes
|
|
1782
|
+
* need, and name the app's custom properties where their JS names the
|
|
1783
|
+
* library's own (`token()`, runtime values) under another prefix.
|
|
1784
|
+
*/
|
|
1785
|
+
const libraries = {
|
|
1786
|
+
name: "vicss:libraries",
|
|
1787
|
+
sharedDuringBuild: true,
|
|
1788
|
+
config(config) {
|
|
1789
|
+
const root = path.resolve(config.root ?? process.cwd());
|
|
1790
|
+
const names = [...new Set(findLibraries(normalizePath(root)).map((library) => library.name))];
|
|
1791
|
+
if (names.length === 0) return void 0;
|
|
1792
|
+
return {
|
|
1793
|
+
optimizeDeps: { exclude: names },
|
|
1794
|
+
ssr: { noExternal: names }
|
|
1795
|
+
};
|
|
1796
|
+
},
|
|
1797
|
+
transform: {
|
|
1798
|
+
filter: libraryFilter,
|
|
1799
|
+
handler(code, id) {
|
|
1800
|
+
const s = ready();
|
|
1801
|
+
const file = fileOf(id);
|
|
1802
|
+
const library = SCRIPT_ID.test(file) ? libraryOf(s.loaded.libraries, file) : void 0;
|
|
1803
|
+
if (s.library || !library) return null;
|
|
1804
|
+
const renamed = library.renames && renameCustomProperties(code, file, library.renames);
|
|
1805
|
+
if (!renamed) return {
|
|
1806
|
+
code: code + libraryImports(s, library),
|
|
1807
|
+
map: null
|
|
1808
|
+
};
|
|
1809
|
+
return {
|
|
1810
|
+
code: renamed.code + libraryImports(s, library),
|
|
1811
|
+
map: renamed.map
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
},
|
|
1815
|
+
transformIndexHtml: {
|
|
1816
|
+
order: "pre",
|
|
1817
|
+
handler() {
|
|
1818
|
+
const s = ready();
|
|
1819
|
+
if (!s.dev || s.loaded.libraries.length === 0) return void 0;
|
|
1820
|
+
return [{
|
|
1821
|
+
tag: "script",
|
|
1822
|
+
attrs: { type: "module" },
|
|
1823
|
+
children: libraryImports(s).trim(),
|
|
1824
|
+
injectTo: "head-prepend"
|
|
1825
|
+
}];
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
const frameworks = {
|
|
1830
|
+
name: "vicss:frameworks",
|
|
1831
|
+
enforce: "post",
|
|
1832
|
+
sharedDuringBuild: true,
|
|
1833
|
+
transform: {
|
|
1834
|
+
filter: frameworkFilter,
|
|
1835
|
+
handler(code, id) {
|
|
1836
|
+
return transformStyled(this, code, id);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
const html = {
|
|
1841
|
+
name: "vicss:tokens-file",
|
|
1842
|
+
apply: "build",
|
|
1843
|
+
sharedDuringBuild: true,
|
|
1844
|
+
transformIndexHtml: {
|
|
1845
|
+
order: "post",
|
|
1846
|
+
handler(_html, ctx) {
|
|
1847
|
+
for (const asset of tokenAssets.values()) if (asset.fileName && ctx.bundle?.[asset.fileName]) {
|
|
1848
|
+
const page = normalizePath(path.relative(asset.config.root, ctx.filename));
|
|
1849
|
+
return [tokensLink(asset.config, asset.fileName, page)];
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
},
|
|
1853
|
+
buildEnd(error) {
|
|
1854
|
+
if (error) tokenAssets.delete(this.environment);
|
|
1855
|
+
},
|
|
1856
|
+
closeBundle() {
|
|
1857
|
+
tokenAssets.delete(this.environment);
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
/**
|
|
1861
|
+
* Build: what critical CSS did to each page, by its file, until the
|
|
1862
|
+
* bundle that emits the page claims it. The HTML hook has no
|
|
1863
|
+
* environment, and Rolldown hands each hook its own bundle object.
|
|
1864
|
+
*/
|
|
1865
|
+
const criticalPages = /* @__PURE__ */ new Map();
|
|
1866
|
+
/** Build: each bundle's CSS files, parsed once for all of its pages. */
|
|
1867
|
+
const criticalSheets = /* @__PURE__ */ new WeakMap();
|
|
1868
|
+
const criticalCss = {
|
|
1869
|
+
name: "vicss:critical",
|
|
1870
|
+
apply: "build",
|
|
1871
|
+
enforce: "post",
|
|
1872
|
+
sharedDuringBuild: true,
|
|
1873
|
+
transformIndexHtml: {
|
|
1874
|
+
order: "post",
|
|
1875
|
+
handler(html, ctx) {
|
|
1876
|
+
const settings = criticalSettings(options.critical);
|
|
1877
|
+
const bundle = ctx.bundle;
|
|
1878
|
+
if (!settings || !bundle || state?.dev !== false) return void 0;
|
|
1879
|
+
let output = criticalSheets.get(bundle);
|
|
1880
|
+
if (!output) {
|
|
1881
|
+
const assets = Object.values(bundle).flatMap((file) => file.type === "asset" && file.fileName.endsWith(".css") ? [file] : []);
|
|
1882
|
+
const sources = new Map(assets.map((asset) => [asset.fileName, asset.source]));
|
|
1883
|
+
const { publicDir } = state.config;
|
|
1884
|
+
const own = publicDir && existsSync(publicDir) ? publicStylesheets(publicDir) : [];
|
|
1885
|
+
const names = session.scan && printed(session.scan).names;
|
|
1886
|
+
const sheets = new SheetCache((file) => {
|
|
1887
|
+
const source = sources.get(file);
|
|
1888
|
+
if (source !== void 0) return typeof source === "string" ? source : Buffer.from(source).toString();
|
|
1889
|
+
return own.includes(file) ? readFileSync(path.join(publicDir, file), "utf8") : void 0;
|
|
1890
|
+
}, names ? (cls) => names.relates(cls) : void 0);
|
|
1891
|
+
output = {
|
|
1892
|
+
files: [...sources.keys(), ...own],
|
|
1893
|
+
sheets
|
|
1894
|
+
};
|
|
1895
|
+
criticalSheets.set(bundle, output);
|
|
1896
|
+
}
|
|
1897
|
+
const result = criticalPage({
|
|
1898
|
+
page: ctx.path.replace(/^\//, ""),
|
|
1899
|
+
html,
|
|
1900
|
+
...output,
|
|
1901
|
+
requireClasses: true
|
|
1902
|
+
});
|
|
1903
|
+
criticalPages.set(result.page, result);
|
|
1904
|
+
return result.html;
|
|
1905
|
+
}
|
|
1906
|
+
},
|
|
1907
|
+
generateBundle(_options, bundle) {
|
|
1908
|
+
const settings = criticalSettings(options.critical);
|
|
1909
|
+
const pages = [...criticalPages.values()].filter((page) => bundle[page.page]?.type === "asset");
|
|
1910
|
+
if (!settings || pages.length === 0) return;
|
|
1911
|
+
for (const page of pages) criticalPages.delete(page.page);
|
|
1912
|
+
const inlined = pages.some((page) => page.html !== void 0);
|
|
1913
|
+
if (settings.hashes !== false && inlined) this.emitFile({
|
|
1914
|
+
type: "asset",
|
|
1915
|
+
fileName: settings.hashes,
|
|
1916
|
+
source: hashesJson(pages, settings.budget)
|
|
1917
|
+
});
|
|
1918
|
+
const { logger } = this.environment.config;
|
|
1919
|
+
const result = report(pages, settings.budget, inlined && settings.hashes ? settings.hashes : void 0);
|
|
1920
|
+
logger.info(result.summary);
|
|
1921
|
+
for (const warning of result.warnings) logger.warn(`vicss: ${warning}`);
|
|
1922
|
+
for (const page of pages) if (page.skipped !== void 0 && !page.warning) logger.info(`vicss: ${page.page}: left as it was: ${page.skipped}`);
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
/** Where a check names the module: relative to the root, as `data-vx-src` does. */
|
|
1926
|
+
const sourceName = (id) => {
|
|
1927
|
+
const file = id.replace(/[?#].*$/, "");
|
|
1928
|
+
const root = ready().root.replace(/\/+$/, "");
|
|
1929
|
+
return file.startsWith(`${root}/`) ? file.slice(root.length + 1) : file;
|
|
1930
|
+
};
|
|
1931
|
+
return [
|
|
1932
|
+
main,
|
|
1933
|
+
{
|
|
1934
|
+
name: "vicss:style-checks",
|
|
1935
|
+
enforce: "pre",
|
|
1936
|
+
apply: "serve",
|
|
1937
|
+
sharedDuringBuild: true,
|
|
1938
|
+
transform: {
|
|
1939
|
+
order: "pre",
|
|
1940
|
+
filter: {
|
|
1941
|
+
id: SCRIPT_ID,
|
|
1942
|
+
code: "style="
|
|
1943
|
+
},
|
|
1944
|
+
handler(code, id) {
|
|
1945
|
+
if (id.includes("/node_modules/") || DECLARATION_ID.test(id)) return null;
|
|
1946
|
+
return checkStyles(code, id, sourceName(id));
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
},
|
|
1950
|
+
libraries,
|
|
1951
|
+
frameworks,
|
|
1952
|
+
{
|
|
1953
|
+
name: "vicss:attributes",
|
|
1954
|
+
enforce: "post",
|
|
1955
|
+
sharedDuringBuild: true,
|
|
1956
|
+
transform: {
|
|
1957
|
+
filter: {
|
|
1958
|
+
id: SFC_SCRIPT_ID,
|
|
1959
|
+
code: COMPONENT_CALLS
|
|
1960
|
+
},
|
|
1961
|
+
handler(code, id) {
|
|
1962
|
+
if (id.includes("/node_modules/")) return null;
|
|
1963
|
+
return rewriteComponent(code, id, {
|
|
1964
|
+
dev: ready().dev,
|
|
1965
|
+
src: sourceName(id)
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
},
|
|
1970
|
+
html,
|
|
1971
|
+
criticalCss
|
|
1972
|
+
];
|
|
1973
|
+
}
|
|
1974
|
+
/** The `.css` files under the public directory, relative to it, as Vite copies them to the output. */
|
|
1975
|
+
function publicStylesheets(dir) {
|
|
1976
|
+
return readdirSync(dir, {
|
|
1977
|
+
recursive: true,
|
|
1978
|
+
withFileTypes: true
|
|
1979
|
+
}).flatMap((entry) => entry.isFile() && entry.name.endsWith(".css") ? [normalizePath(path.relative(dir, path.join(entry.parentPath, entry.name)))] : []);
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* What the scan's worker threads build their compiler from: the merged config
|
|
1983
|
+
* as data, or, when it holds functions (utilities), the file that defines
|
|
1984
|
+
* it. A config object with functions cannot cross to a thread: no workers.
|
|
1985
|
+
*/
|
|
1986
|
+
function workerSetup(s) {
|
|
1987
|
+
const { system, file } = s.loaded;
|
|
1988
|
+
const common = {
|
|
1989
|
+
root: s.root,
|
|
1990
|
+
hash: system.hash,
|
|
1991
|
+
sourceMap: s.compile.sourceMap,
|
|
1992
|
+
dev: s.compile.dev === true,
|
|
1993
|
+
placeholders: s.compile.placeholders === true,
|
|
1994
|
+
package: s.pkg,
|
|
1995
|
+
frameworks: s.frameworks
|
|
1996
|
+
};
|
|
1997
|
+
if (!hasFunction(system.config)) return {
|
|
1998
|
+
...common,
|
|
1999
|
+
config: { data: system.config }
|
|
2000
|
+
};
|
|
2001
|
+
return file ? {
|
|
2002
|
+
...common,
|
|
2003
|
+
config: {
|
|
2004
|
+
file,
|
|
2005
|
+
shape: configShape(system.config)
|
|
2006
|
+
}
|
|
2007
|
+
} : void 0;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* An error thrown while the config loads (an invalid config, a library the
|
|
2011
|
+
* design system does not cover), worded as `fail` words a module's:
|
|
2012
|
+
* `vicss(CODE): message` with the hint below it. Vite prints a startup
|
|
2013
|
+
* error's message and stack, and the JS API's callers read the message; a
|
|
2014
|
+
* VicssError keeps its hint apart. A config error names the config file.
|
|
2015
|
+
*/
|
|
2016
|
+
function loadFailure(error, root, option) {
|
|
2017
|
+
if (!(error instanceof VicssError)) return error;
|
|
2018
|
+
const { code, hint } = error;
|
|
2019
|
+
const failure = /* @__PURE__ */ new Error(`vicss(${code}): ${error.message}${hint ? `\n${hint}` : ""}`);
|
|
2020
|
+
const file = typeof option === "string" ? normalizePath(path.resolve(root, option)) : option === void 0 ? CONFIG_FILES.map((name) => normalizePath(path.join(root, name))).find(existsSync) : void 0;
|
|
2021
|
+
return Object.assign(failure, {
|
|
2022
|
+
code,
|
|
2023
|
+
plugin: "vicss",
|
|
2024
|
+
...code === "CONFIG" && file ? { id: file } : {}
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* What a package manager writes when it installs: lockfiles, and the state
|
|
2029
|
+
* files some keep in `node_modules` (Vite's dependency optimizer reads the
|
|
2030
|
+
* same). The nearest directory above `root` that has any, as the workspace
|
|
2031
|
+
* root of a monorepo does.
|
|
2032
|
+
*/
|
|
2033
|
+
const LOCKFILES = [
|
|
2034
|
+
"pnpm-lock.yaml",
|
|
2035
|
+
"package-lock.json",
|
|
2036
|
+
"npm-shrinkwrap.json",
|
|
2037
|
+
"yarn.lock",
|
|
2038
|
+
"bun.lock",
|
|
2039
|
+
"bun.lockb",
|
|
2040
|
+
"node_modules/.pnpm/lock.yaml",
|
|
2041
|
+
"node_modules/.package-lock.json",
|
|
2042
|
+
"node_modules/.yarn-state.yml"
|
|
2043
|
+
];
|
|
2044
|
+
function lockfiles(root) {
|
|
2045
|
+
for (let dir = root;; dir = path.dirname(dir)) {
|
|
2046
|
+
const found = LOCKFILES.map((name) => normalizePath(path.join(dir, name))).filter(existsSync);
|
|
2047
|
+
if (found.length > 0 || path.dirname(dir) === dir) return found;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* The lines of a framework file that its compiled output came from, read
|
|
2052
|
+
* from the map Vite combined for the module so far; undefined without one.
|
|
2053
|
+
*/
|
|
2054
|
+
function mappedLines(ctx, id) {
|
|
2055
|
+
const file = fileOf(id);
|
|
2056
|
+
let map;
|
|
2057
|
+
try {
|
|
2058
|
+
map = ctx.getCombinedSourcemap();
|
|
2059
|
+
} catch {
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
const only = map.sources.length === 1;
|
|
2063
|
+
return originalLines(map, (source) => only || normalizePath(path.resolve(path.dirname(file), source)) === file);
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Whether a build ships readable names: `classNames.readable`, else
|
|
2067
|
+
* `VICSS_READABLE=1` in the environment, which debugs a production build,
|
|
2068
|
+
* and runs a test suite's builds, without editing the config.
|
|
2069
|
+
*/
|
|
2070
|
+
function readableBuild(options) {
|
|
2071
|
+
return options.readable ?? process.env.VICSS_READABLE === "1";
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* What a build names (core/names.ts): every atom, theme, flattened base and
|
|
2075
|
+
* marker of the sheet, and every class compiled code names (`names`, from
|
|
2076
|
+
* the scan): markers applied without a style, palette prefixes.
|
|
2077
|
+
*/
|
|
2078
|
+
function identitiesOf(styles, names) {
|
|
2079
|
+
const out = new Set(names);
|
|
2080
|
+
for (const atom of styles.atoms) {
|
|
2081
|
+
out.add(atom.className);
|
|
2082
|
+
for (const condition of atom.chain) {
|
|
2083
|
+
const marker = /^_vx:m:([gp]):([0-9a-z]{6}):/.exec(condition);
|
|
2084
|
+
if (marker) out.add(`vx-${marker[1]}${marker[2]}_m`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
for (const rule of styles.rules) if (rule.kind !== "keyframes") out.add(rule.id);
|
|
2088
|
+
return [...out].sort();
|
|
2089
|
+
}
|
|
2090
|
+
/** One build: the configs it resolved, the environments that closed, its scan. */
|
|
2091
|
+
function newSession() {
|
|
2092
|
+
return {
|
|
2093
|
+
started: false,
|
|
2094
|
+
configs: 1,
|
|
2095
|
+
closed: 0,
|
|
2096
|
+
failed: false,
|
|
2097
|
+
scan: void 0
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Resolution for the compiler, through the environment's plugins. Constants
|
|
2102
|
+
* become class names the server must render as the client does, so an
|
|
2103
|
+
* installed package resolves to the file the client reads, in every
|
|
2104
|
+
* environment and in the scan whichever environment starts it; one a server
|
|
2105
|
+
* build externalizes is still read at build time.
|
|
2106
|
+
*/
|
|
2107
|
+
function constantsResolver(s, env, resolve) {
|
|
2108
|
+
return async (source, importer) => {
|
|
2109
|
+
const resolved = await resolve(source, importer);
|
|
2110
|
+
const server = env.config.consumer === "server";
|
|
2111
|
+
if (!resolved || !resolved.external && !(server && resolved.id.includes("/node_modules/"))) return resolved;
|
|
2112
|
+
const { conditions, mainFields } = s.config.environments.client.resolve;
|
|
2113
|
+
s.clientResolve ??= createIdResolver(s.config, {
|
|
2114
|
+
conditions,
|
|
2115
|
+
mainFields,
|
|
2116
|
+
external: [],
|
|
2117
|
+
noExternal: true
|
|
2118
|
+
});
|
|
2119
|
+
const id = await s.clientResolve(env, source, importer);
|
|
2120
|
+
if (id) return { id };
|
|
2121
|
+
if (!resolved.external) return resolved;
|
|
2122
|
+
return path.isAbsolute(resolved.id) ? { id: resolved.id } : null;
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
function contextResolver(s, ctx) {
|
|
2126
|
+
return constantsResolver(s, ctx.environment, (source, importer) => ctx.resolve(source, importer, { skipSelf: true }));
|
|
2127
|
+
}
|
|
2128
|
+
/** Resolution outside a plugin hook, through the environment's plugins. */
|
|
2129
|
+
function envResolver(s, env) {
|
|
2130
|
+
return constantsResolver(s, env, (source, importer) => env.pluginContainer.resolveId(source, importer));
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Resolutions by importer directory and request: a request resolves the same
|
|
2134
|
+
* from every file of a directory, and most files import the same
|
|
2135
|
+
* design-system modules, so each is resolved once.
|
|
2136
|
+
*/
|
|
2137
|
+
function memoize(resolved, prefix, resolve) {
|
|
2138
|
+
return (source, importer) => {
|
|
2139
|
+
const key = `${prefix}\0${path.dirname(importer)}\0${source}`;
|
|
2140
|
+
let found = resolved.get(key);
|
|
2141
|
+
if (!found) {
|
|
2142
|
+
resolved.set(key, found = resolve(source, importer));
|
|
2143
|
+
found.catch(() => resolved.delete(key));
|
|
2144
|
+
}
|
|
2145
|
+
return found;
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
/** `run` over `items`, `BATCH` at a time, results in order. */
|
|
2149
|
+
async function inBatches(items, run) {
|
|
2150
|
+
const out = [];
|
|
2151
|
+
for (let i = 0; i < items.length; i += BATCH) out.push(...await Promise.all(items.slice(i, i + BATCH).map(run)));
|
|
2152
|
+
return out;
|
|
2153
|
+
}
|
|
2154
|
+
/** The styles of several modules (a framework file's scripts) as one. */
|
|
2155
|
+
function mergeStyles(parts) {
|
|
2156
|
+
return {
|
|
2157
|
+
atoms: [...new Map(parts.flatMap((part) => part.atoms).map((a) => [a.className, a])).values()].sort((a, b) => a.className < b.className ? -1 : 1),
|
|
2158
|
+
tokens: parts.flatMap((part) => part.tokens),
|
|
2159
|
+
rules: parts.flatMap((part) => part.rules),
|
|
2160
|
+
dependencies: [...new Set(parts.flatMap((part) => part.dependencies))]
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
/** Whether a module's dev CSS prints alike: the same atoms and flattened bases. */
|
|
2164
|
+
const sameCss = (a, b) => {
|
|
2165
|
+
const bases = (styles) => styles.rules.filter((rule) => rule.kind === "base").map((rule) => rule.id);
|
|
2166
|
+
const [x, y] = [bases(a), bases(b)];
|
|
2167
|
+
return a.atoms.length === b.atoms.length && a.atoms.every((atom, i) => atom.className === b.atoms[i].className) && x.length === y.length && x.every((id, i) => id === y[i]);
|
|
2168
|
+
};
|
|
2169
|
+
/** Why a style in a framework file's template fails (PLAN §5.4). */
|
|
2170
|
+
const TEMPLATE_HINT = "styles written in a component's template are not supported, since the pre-scan reads only the file's scripts. Move the style into the <script> (or the frontmatter): `const card = css({ … })`, then use `card` in the template.";
|
|
2171
|
+
/** Matches no module id: a filter with no patterns would match every one. */
|
|
2172
|
+
const NOTHING = /^\0vicss:no-library$/;
|
|
2173
|
+
function hasFunction(value) {
|
|
2174
|
+
if (typeof value === "function") return true;
|
|
2175
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2176
|
+
return Object.values(value).some(hasFunction);
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* `./X.tsx.vicss.css` from its importer, `/src/X.tsx.vicss.css` from the
|
|
2180
|
+
* root (the dev server's URL), or an absolute path (`/@fs/…` or `/@id/…`
|
|
2181
|
+
* outside the root).
|
|
2182
|
+
*/
|
|
2183
|
+
function moduleCssId(root, file, importer) {
|
|
2184
|
+
if (file.startsWith("./") || file.startsWith("../")) {
|
|
2185
|
+
const from = importer ? path.dirname(splitQuery(importer)[0]) : root;
|
|
2186
|
+
return normalizePath(path.resolve(from, file));
|
|
2187
|
+
}
|
|
2188
|
+
if (file.startsWith("/@fs/")) return file.slice(4);
|
|
2189
|
+
if (file.startsWith(`${root}/`)) return file;
|
|
2190
|
+
const source = file.slice(0, -10);
|
|
2191
|
+
return existsSync(source) && !existsSync(root + source) ? file : root + file;
|
|
2192
|
+
}
|
|
2193
|
+
/** The safety net: a module compiled outside the scan needs atoms or rules the sheet lacks. */
|
|
2194
|
+
function checkSheet(ctx, sheet, tokenKeys, result, id, root) {
|
|
2195
|
+
const clean = fileOf(id);
|
|
2196
|
+
const file = path.relative(root, clean) || clean;
|
|
2197
|
+
const why = !sheet.files.includes(clean) ? "the file is outside `include` in vicss.config.ts, so the pre-scan never compiled it. Add it to `include` (or remove it from `exclude`)." : isSfc(clean) ? TEMPLATE_HINT : `the pre-scan compiled this file to other atoms: one of its imports resolves differently in the ${ctx.environment.name} environment, or the file changed during the build.`;
|
|
2198
|
+
const atom = result.atoms.find((a) => !sheet.classes.has(a.className));
|
|
2199
|
+
if (atom) ctx.error({
|
|
2200
|
+
code: "MISSING_ATOM",
|
|
2201
|
+
id,
|
|
2202
|
+
message: `vicss(MISSING_ATOM): ${file} uses the class \`${atom.className}\`, which the sheet lacks: ${why}`
|
|
2203
|
+
});
|
|
2204
|
+
const rule = result.rules.find((r) => !sheet.ruleIds.has(r.id));
|
|
2205
|
+
if (rule) {
|
|
2206
|
+
const kind = rule.kind === "base" ? "flattened recipe base" : rule.kind;
|
|
2207
|
+
ctx.error({
|
|
2208
|
+
code: "MISSING_RULE",
|
|
2209
|
+
id,
|
|
2210
|
+
message: `vicss(MISSING_RULE): ${file} uses the ${kind} \`${rule.id}\`, which the sheet lacks: ${why}`
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
const token = result.tokens.find((ref) => !tokenKeys.has(`${ref.scale}:${ref.path}`));
|
|
2214
|
+
if (token) ctx.error({
|
|
2215
|
+
code: "MISSING_TOKEN",
|
|
2216
|
+
id,
|
|
2217
|
+
message: `vicss(MISSING_TOKEN): ${file} uses the token \`${token.scale}.${token.path}\`, which the sheet lacks: ${why}`
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* A VicssError becomes a located plugin error reading `vicss(CODE): message`
|
|
2222
|
+
* and the hint, with a code frame (PLAN §5.4) as `frame`, which Vite's
|
|
2223
|
+
* overlay and logs and Rolldown's build error all print under the message.
|
|
2224
|
+
* `code` is the source of `id` as compiled (a framework's output, or a
|
|
2225
|
+
* framework file whose scripts compiled with the rest blanked, so lines and
|
|
2226
|
+
* columns are the file's); a location in another file is framed from disk.
|
|
2227
|
+
*/
|
|
2228
|
+
function fail(ctx, error, where) {
|
|
2229
|
+
if (!(error instanceof VicssError)) throw error;
|
|
2230
|
+
const { loc, hint } = error;
|
|
2231
|
+
const message = `vicss(${error.code}): ${error.message}${hint ? `\n${hint}` : ""}`;
|
|
2232
|
+
const id = loc?.file ?? where.id;
|
|
2233
|
+
const source = loc && (loc.file === where.id ? where.code : readSource(loc.file));
|
|
2234
|
+
const frame = loc && source !== void 0 ? codeFrame(source, loc.line, loc.column) : void 0;
|
|
2235
|
+
ctx.error({
|
|
2236
|
+
message,
|
|
2237
|
+
code: error.code,
|
|
2238
|
+
...id ? { id } : {},
|
|
2239
|
+
...loc ? { loc: {
|
|
2240
|
+
file: loc.file,
|
|
2241
|
+
line: loc.line,
|
|
2242
|
+
column: loc.column
|
|
2243
|
+
} } : {},
|
|
2244
|
+
...frame ? { frame } : {}
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Vitest serves modules through a dev server (`command: "serve"`) in mode
|
|
2249
|
+
* `test`, or another mode with `--mode`, always with its own `vitest:`
|
|
2250
|
+
* plugins. Its output is asserted and snapshotted, where a source line in
|
|
2251
|
+
* every `sx()` result would break on any unrelated edit above it.
|
|
2252
|
+
*/
|
|
2253
|
+
function isTestRunner(config) {
|
|
2254
|
+
return config.mode === "test" || config.plugins.some((plugin) => plugin.name.startsWith("vitest:"));
|
|
2255
|
+
}
|
|
2256
|
+
function readSource(file) {
|
|
2257
|
+
try {
|
|
2258
|
+
return readFileSync(file.replace(/[?#].*$/, ""), "utf8");
|
|
2259
|
+
} catch {
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
//#endregion
|
|
2264
|
+
export { critical, vicss as default, vicss };
|