@menapo/vicss 0.0.0-stage → 1.0.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 +627 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1311 -0
- package/dist/config-loader-DzP68Snn.js +162 -0
- package/dist/config-loader-pLzP9par.js +1 -0
- package/dist/config.d.ts +821 -0
- package/dist/config.js +2690 -0
- package/dist/index.d.ts +1471 -0
- package/dist/index.js +369 -0
- package/dist/lint.d.ts +114 -0
- package/dist/lint.js +0 -0
- package/dist/pool-BGXMhLfL.js +2942 -0
- package/dist/print-Ds6fkgio.js +5093 -0
- package/dist/snapshot-BQb8ZmSV.js +149 -0
- package/dist/theme-C-0LpsJx.js +2437 -0
- package/dist/types-file-R_knFzME.js +2481 -0
- package/dist/vite.d.ts +245 -0
- package/dist/vite.js +1926 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +113 -0
- package/package.json +66 -3
|
@@ -0,0 +1,2942 @@
|
|
|
1
|
+
import { $ as keepAtom, A as getProperty, At as splitClasses, Ct as makeClassName, F as markerId, Ft as stableStringify, I as markerLabel, K as placeholder, L as markerSites, M as numberKind, Mt as toRecord, Nt as valueLabel, Ot as propertyKey, P as MarkerValue, Pt as hash, Tt as mergeClassNames, _t as VicssError, at as SPACING_STEP, b as hasPrivateAccess, ct as cssVarName, dt as isScaleName, ft as lookupToken, g as normalizeStyle, h as compileKeyframes, ht as suggestTokens, it as SCALE_NAMESPACES, jt as styleLiteral, k as PROPERTIES, kt as readableClass, lt as didYouMean, mt as scaleTokens, nt as typedPaletteNames, ot as closestNames, rt as SCALES, ut as isPlainObject, vt as classAttribute, x as privateTokenError, xt as fromRecord, yt as compareStrings } from "./print-Ds6fkgio.js";
|
|
2
|
+
import { C as createLocator, _ as evaluate, a as buildRecipe, c as resolveRecipe, h as NOT_HANDLED, i as RecipeValue, m as shadowedByEarlier, n as compileTheme, o as printTable, p as isBaseClass, r as analyzeScopes, s as recipeKey, v as evaluateBinding, y as isDynamic } from "./theme-C-0LpsJx.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { availableParallelism } from "node:os";
|
|
7
|
+
import { Worker } from "node:worker_threads";
|
|
8
|
+
//#region src/core/edits.ts
|
|
9
|
+
/**
|
|
10
|
+
* Source edits with a source map (PLAN D20): the few operations the rewrite
|
|
11
|
+
* step needs, without a dependency. Offsets always refer to the original
|
|
12
|
+
* string, so edits can be recorded in any order.
|
|
13
|
+
*/
|
|
14
|
+
var SourceEdits = class {
|
|
15
|
+
original;
|
|
16
|
+
/** Sorted by start, never overlapping. */
|
|
17
|
+
#replacements = [];
|
|
18
|
+
#inserts = /* @__PURE__ */ new Map();
|
|
19
|
+
#prefix = "";
|
|
20
|
+
#keepLines;
|
|
21
|
+
constructor(original, options = {}) {
|
|
22
|
+
this.original = original;
|
|
23
|
+
this.#keepLines = options.keepLines === true;
|
|
24
|
+
}
|
|
25
|
+
overwrite(start, end, content) {
|
|
26
|
+
this.#checkRange(start, end);
|
|
27
|
+
if (start === end) throw new RangeError(`SourceEdits: empty range at ${start}; use insert()`);
|
|
28
|
+
let index = this.#replacements.findIndex((r) => r.start >= start);
|
|
29
|
+
if (index === -1) index = this.#replacements.length;
|
|
30
|
+
const before = this.#replacements[index - 1];
|
|
31
|
+
const after = this.#replacements[index];
|
|
32
|
+
const clash = before && before.end > start ? before : after && after.start < end ? after : null;
|
|
33
|
+
if (clash) throw new Error(`SourceEdits: [${start}, ${end}) overlaps the overwrite of [${clash.start}, ${clash.end})`);
|
|
34
|
+
for (const offset of this.#inserts.keys()) if (offset > start && offset < end) throw new Error(`SourceEdits: [${start}, ${end}) would drop the insert at ${offset}`);
|
|
35
|
+
if (this.#keepLines) content += "\n".repeat(Math.max(0, lineBreaks(this.original, start, end) - lineBreaks(content)));
|
|
36
|
+
this.#replacements.splice(index, 0, {
|
|
37
|
+
start,
|
|
38
|
+
end,
|
|
39
|
+
content
|
|
40
|
+
});
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
/** Inserts before the original character at `offset`; inserts at one offset keep call order. */
|
|
44
|
+
insert(offset, content) {
|
|
45
|
+
this.#checkRange(offset, offset);
|
|
46
|
+
const inside = this.#replacements.find((r) => offset > r.start && offset < r.end);
|
|
47
|
+
if (inside) throw new Error(`SourceEdits: insert at ${offset} falls inside the overwrite of [${inside.start}, ${inside.end})`);
|
|
48
|
+
const list = this.#inserts.get(offset);
|
|
49
|
+
if (list) list.push(content);
|
|
50
|
+
else this.#inserts.set(offset, [content]);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
/** Adds `content` at the very start of the output, before earlier prepends and inserts. */
|
|
54
|
+
prepend(content) {
|
|
55
|
+
this.#prefix = content + this.#prefix;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
remove(start, end) {
|
|
59
|
+
this.#checkRange(start, end);
|
|
60
|
+
return start === end ? this : this.overwrite(start, end, "");
|
|
61
|
+
}
|
|
62
|
+
hasChanged() {
|
|
63
|
+
return this.toString() !== this.original;
|
|
64
|
+
}
|
|
65
|
+
toString() {
|
|
66
|
+
let out = "";
|
|
67
|
+
for (const chunk of this.#chunks()) out += chunk.text;
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Segments start every output line and every edit boundary. Unchanged text
|
|
72
|
+
* maps 1:1; replaced and inserted text maps to the start of the range it
|
|
73
|
+
* replaced or to its insertion point.
|
|
74
|
+
*/
|
|
75
|
+
generateMap(options) {
|
|
76
|
+
const locate = createLocator(this.original);
|
|
77
|
+
let mappings = "";
|
|
78
|
+
let column = 0;
|
|
79
|
+
let lineHasSegment = false;
|
|
80
|
+
let prevColumn = 0;
|
|
81
|
+
let prevSourceLine = 0;
|
|
82
|
+
let prevSourceColumn = 0;
|
|
83
|
+
const add = (offset) => {
|
|
84
|
+
const { line, column: sourceColumn } = locate(offset);
|
|
85
|
+
if (lineHasSegment) mappings += ",";
|
|
86
|
+
mappings += vlq(column - prevColumn) + "A" + vlq(line - 1 - prevSourceLine) + vlq(sourceColumn - prevSourceColumn);
|
|
87
|
+
lineHasSegment = true;
|
|
88
|
+
prevColumn = column;
|
|
89
|
+
prevSourceLine = line - 1;
|
|
90
|
+
prevSourceColumn = sourceColumn;
|
|
91
|
+
};
|
|
92
|
+
for (const { text, origin, copied } of this.#chunks()) {
|
|
93
|
+
add(origin);
|
|
94
|
+
for (let i = 0; i < text.length; i++) {
|
|
95
|
+
if (text.charCodeAt(i) === 10) {
|
|
96
|
+
mappings += ";";
|
|
97
|
+
column = 0;
|
|
98
|
+
prevColumn = 0;
|
|
99
|
+
lineHasSegment = false;
|
|
100
|
+
} else column++;
|
|
101
|
+
const next = i + 1;
|
|
102
|
+
if (next === text.length) continue;
|
|
103
|
+
if (column === 0 || options.hires && copied && isWordStart(text, next)) add(copied ? origin + next : origin);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
version: 3,
|
|
108
|
+
...options.file === void 0 ? {} : { file: options.file },
|
|
109
|
+
sources: [options.source],
|
|
110
|
+
...options.includeContent ? { sourcesContent: [this.original] } : {},
|
|
111
|
+
names: [],
|
|
112
|
+
mappings
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
#checkRange(start, end) {
|
|
116
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > this.original.length) throw new RangeError(`SourceEdits: invalid range [${start}, ${end}) for a string of length ${this.original.length}`);
|
|
117
|
+
}
|
|
118
|
+
#chunks() {
|
|
119
|
+
const chunks = [];
|
|
120
|
+
const push = (text, origin, copied) => {
|
|
121
|
+
if (text !== "") chunks.push({
|
|
122
|
+
text,
|
|
123
|
+
origin,
|
|
124
|
+
copied
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
let pos = 0;
|
|
128
|
+
const copyTo = (end) => {
|
|
129
|
+
if (end > pos) push(this.original.slice(pos, end), pos, true);
|
|
130
|
+
pos = end;
|
|
131
|
+
};
|
|
132
|
+
const replacements = this.#replacements;
|
|
133
|
+
let r = 0;
|
|
134
|
+
const replaceBefore = (limit) => {
|
|
135
|
+
for (let next = replacements[r]; next && next.start < limit; next = replacements[++r]) {
|
|
136
|
+
copyTo(next.start);
|
|
137
|
+
push(next.content, next.start, false);
|
|
138
|
+
pos = next.end;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
push(this.#prefix, 0, false);
|
|
142
|
+
for (const offset of [...this.#inserts.keys()].sort((a, b) => a - b)) {
|
|
143
|
+
replaceBefore(offset);
|
|
144
|
+
copyTo(offset);
|
|
145
|
+
for (const text of this.#inserts.get(offset)) push(text, offset, false);
|
|
146
|
+
}
|
|
147
|
+
replaceBefore(Infinity);
|
|
148
|
+
copyTo(this.original.length);
|
|
149
|
+
return chunks;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const WORD = /[\w$]/;
|
|
153
|
+
function isWordStart(text, i) {
|
|
154
|
+
return WORD.test(text[i]) && !WORD.test(text[i - 1]);
|
|
155
|
+
}
|
|
156
|
+
const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
157
|
+
/** Base64 VLQ: sign in the lowest bit, then 5-bit groups, least significant first. */
|
|
158
|
+
function vlq(value) {
|
|
159
|
+
let rest = value < 0 ? -value << 1 | 1 : value << 1;
|
|
160
|
+
let out = "";
|
|
161
|
+
do {
|
|
162
|
+
let digit = rest & 31;
|
|
163
|
+
rest >>>= 5;
|
|
164
|
+
if (rest > 0) digit |= 32;
|
|
165
|
+
out += BASE64[digit];
|
|
166
|
+
} while (rest > 0);
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Reads a map back: the 1-based line of the original source a generated
|
|
171
|
+
* position (1-based line, 0-based column) came from, or undefined where the
|
|
172
|
+
* map says nothing or names another source. For `data-vx-src` in a
|
|
173
|
+
* framework file (PLAN §5.14), whose compiler moved its lines before vicss
|
|
174
|
+
* compiles its output: the map Vite combined says where each line was.
|
|
175
|
+
*/
|
|
176
|
+
function originalLines(map, isSource) {
|
|
177
|
+
const lines = [];
|
|
178
|
+
let source = 0;
|
|
179
|
+
let sourceLine = 0;
|
|
180
|
+
for (const text of map.mappings.split(";")) {
|
|
181
|
+
const segments = [];
|
|
182
|
+
let column = 0;
|
|
183
|
+
for (const part of text.split(",")) {
|
|
184
|
+
if (!part) continue;
|
|
185
|
+
const fields = decodeVlq(part);
|
|
186
|
+
column += fields[0] ?? 0;
|
|
187
|
+
if (fields.length < 4) continue;
|
|
188
|
+
source += fields[1];
|
|
189
|
+
sourceLine += fields[2];
|
|
190
|
+
segments.push([
|
|
191
|
+
column,
|
|
192
|
+
source,
|
|
193
|
+
sourceLine
|
|
194
|
+
]);
|
|
195
|
+
}
|
|
196
|
+
lines.push(segments);
|
|
197
|
+
}
|
|
198
|
+
const own = map.sources.map((name) => name !== null && isSource(name));
|
|
199
|
+
return (line, column) => {
|
|
200
|
+
const segments = lines[line - 1] ?? [];
|
|
201
|
+
let found = segments[0];
|
|
202
|
+
for (const segment of segments) {
|
|
203
|
+
if (segment[0] > column) break;
|
|
204
|
+
found = segment;
|
|
205
|
+
}
|
|
206
|
+
return found && own[found[1]] ? found[2] + 1 : void 0;
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** The `\n`s in `text`, or in its range `[start, end)`. */
|
|
210
|
+
function lineBreaks(text, start = 0, end = text.length) {
|
|
211
|
+
let count = 0;
|
|
212
|
+
for (let i = text.indexOf("\n", start); i !== -1 && i < end; i = text.indexOf("\n", i + 1)) count++;
|
|
213
|
+
return count;
|
|
214
|
+
}
|
|
215
|
+
/** The values of one segment's Base64 VLQ fields. */
|
|
216
|
+
function decodeVlq(text) {
|
|
217
|
+
const out = [];
|
|
218
|
+
let value = 0;
|
|
219
|
+
let shift = 0;
|
|
220
|
+
for (const char of text) {
|
|
221
|
+
const digit = BASE64.indexOf(char);
|
|
222
|
+
if (digit === -1) return out;
|
|
223
|
+
value += (digit & 31) << shift;
|
|
224
|
+
if (digit & 32) {
|
|
225
|
+
shift += 5;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
out.push(value & 1 ? -(value >>> 1) : value >>> 1);
|
|
229
|
+
value = 0;
|
|
230
|
+
shift = 0;
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/core/parser.ts
|
|
236
|
+
let parser;
|
|
237
|
+
let loading;
|
|
238
|
+
/** Installs the parser before the first compile (a worker that loaded it itself). */
|
|
239
|
+
function setParser(parse) {
|
|
240
|
+
parser = parse;
|
|
241
|
+
}
|
|
242
|
+
/** The parser, loading Vite once if nothing installed one. */
|
|
243
|
+
function loadParser() {
|
|
244
|
+
if (parser) return parser;
|
|
245
|
+
return loading ??= import("vite").then((vite) => parser = vite.parseSync);
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/core/compile.ts
|
|
249
|
+
/** One entry per module id: an edited module replaces its stale parse. */
|
|
250
|
+
function createModuleCache() {
|
|
251
|
+
return {
|
|
252
|
+
parsed: /* @__PURE__ */ new Map(),
|
|
253
|
+
recipes: /* @__PURE__ */ new WeakMap()
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** Distinct recipe definitions a cache keeps per system before it starts over. */
|
|
257
|
+
const RECIPE_CACHE_SIZE = 1e3;
|
|
258
|
+
/** Compiles one module. Throws VicssError, with a location, for anything it refuses. */
|
|
259
|
+
async function compileModule(code, options) {
|
|
260
|
+
const sources = [.../* @__PURE__ */ new Set([RUNTIME_PACKAGE, ...options.importSources ?? []])];
|
|
261
|
+
if (!sources.some((source) => code.includes(source))) return unchanged(code);
|
|
262
|
+
const compiler = new Compiler(code, options, sources, await loadParser());
|
|
263
|
+
if (compiler.root.runtime.size === 0) return unchanged(code);
|
|
264
|
+
for (;;) try {
|
|
265
|
+
return compiler.run();
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (!(error instanceof PendingImport)) throw error;
|
|
268
|
+
await Promise.all(error.requests.map(([importer, source]) => compiler.load(importer, source, error.probes.has(requestKey(importer, source)))));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const RUNTIME_PACKAGE = "@menapo/vicss";
|
|
272
|
+
const RUNTIME_EXPORTS = /* @__PURE__ */ new Set([
|
|
273
|
+
"css",
|
|
274
|
+
"sx",
|
|
275
|
+
"token",
|
|
276
|
+
"recipe",
|
|
277
|
+
"theme",
|
|
278
|
+
"group",
|
|
279
|
+
"peer",
|
|
280
|
+
"keyframes"
|
|
281
|
+
]);
|
|
282
|
+
/**
|
|
283
|
+
* What `sx()` returns per framework (PLAN §5.12): the class attribute a
|
|
284
|
+
* folded call sets, the runtime export an `sx` import is pointed at
|
|
285
|
+
* (React's shape is `sx` itself), and the dev variant a runtime call becomes
|
|
286
|
+
* so the element names its source line (runtime/dev.ts). Every shape's
|
|
287
|
+
* `style` is what its framework accepts: an object, or a `a:b;c:d` string.
|
|
288
|
+
* `data-vx-src` is a plain attribute name, which every framework passes
|
|
289
|
+
* through from a spread object as it is.
|
|
290
|
+
*/
|
|
291
|
+
const SX_SHAPES = {
|
|
292
|
+
react: {
|
|
293
|
+
attribute: "className",
|
|
294
|
+
dev: "__vxSxDev"
|
|
295
|
+
},
|
|
296
|
+
preact: {
|
|
297
|
+
attribute: "className",
|
|
298
|
+
dev: "__vxSxDev"
|
|
299
|
+
},
|
|
300
|
+
solid: {
|
|
301
|
+
attribute: "class",
|
|
302
|
+
target: "__vxSxClass",
|
|
303
|
+
dev: "__vxSxDevClass"
|
|
304
|
+
},
|
|
305
|
+
vue: {
|
|
306
|
+
attribute: "class",
|
|
307
|
+
target: "__vxSxClass",
|
|
308
|
+
dev: "__vxSxDevClass"
|
|
309
|
+
},
|
|
310
|
+
svelte: {
|
|
311
|
+
attribute: "class",
|
|
312
|
+
target: "__vxSxString",
|
|
313
|
+
dev: "__vxSxDevString"
|
|
314
|
+
},
|
|
315
|
+
astro: {
|
|
316
|
+
attribute: "class",
|
|
317
|
+
target: "__vxSxString",
|
|
318
|
+
dev: "__vxSxDevString"
|
|
319
|
+
},
|
|
320
|
+
vanilla: {
|
|
321
|
+
attribute: "class",
|
|
322
|
+
target: "__vxSxString",
|
|
323
|
+
dev: "__vxSxDevString"
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
/** D18: a folded `sx()` precomputes 2^n strings for n conditional arguments. */
|
|
327
|
+
const MAX_FOLD_CONDITIONS = 2;
|
|
328
|
+
/** Stands for a runtime value while the normaliser checks the rest of a function style. */
|
|
329
|
+
const PLACEHOLDER = "\0vx";
|
|
330
|
+
const WRAPPERS = /* @__PURE__ */ new Set([
|
|
331
|
+
"ParenthesizedExpression",
|
|
332
|
+
"TSAsExpression",
|
|
333
|
+
"TSSatisfiesExpression",
|
|
334
|
+
"TSNonNullExpression",
|
|
335
|
+
"TSTypeAssertion",
|
|
336
|
+
"TSInstantiationExpression"
|
|
337
|
+
]);
|
|
338
|
+
/** Keys that hold types or metadata: no runtime call lives below them. */
|
|
339
|
+
const SKIPPED_KEYS = /* @__PURE__ */ new Set([
|
|
340
|
+
"parent",
|
|
341
|
+
"range",
|
|
342
|
+
"typeAnnotation",
|
|
343
|
+
"typeArguments",
|
|
344
|
+
"typeParameters",
|
|
345
|
+
"returnType",
|
|
346
|
+
"superTypeArguments",
|
|
347
|
+
"implements"
|
|
348
|
+
]);
|
|
349
|
+
/** Tests that read the same without parentheses in front of `?`. */
|
|
350
|
+
const BARE_TESTS = /* @__PURE__ */ new Set([
|
|
351
|
+
"Identifier",
|
|
352
|
+
"MemberExpression",
|
|
353
|
+
"ChainExpression",
|
|
354
|
+
"CallExpression",
|
|
355
|
+
"ParenthesizedExpression",
|
|
356
|
+
"UnaryExpression",
|
|
357
|
+
"Literal",
|
|
358
|
+
"ThisExpression"
|
|
359
|
+
]);
|
|
360
|
+
const LANGS = {
|
|
361
|
+
js: "js",
|
|
362
|
+
mjs: "js",
|
|
363
|
+
cjs: "js",
|
|
364
|
+
jsx: "jsx",
|
|
365
|
+
ts: "ts",
|
|
366
|
+
mts: "ts",
|
|
367
|
+
cts: "ts",
|
|
368
|
+
tsx: "tsx",
|
|
369
|
+
vue: "js",
|
|
370
|
+
svelte: "js",
|
|
371
|
+
astro: "js"
|
|
372
|
+
};
|
|
373
|
+
/** What a `css()` call is inside another static expression: its classes, as a class list. */
|
|
374
|
+
var CompiledStyle = class {
|
|
375
|
+
__vxSpecial = "style";
|
|
376
|
+
classes;
|
|
377
|
+
constructor(classes) {
|
|
378
|
+
this.classes = classes;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
/** A function style inside another expression: never a constant. */
|
|
382
|
+
const FUNCTION_STYLE = { __vxSpecial: "functionStyle" };
|
|
383
|
+
/**
|
|
384
|
+
* Thrown through the evaluator when a constant lives in a module that is not
|
|
385
|
+
* loaded yet. A VicssError, so the evaluator passes it on untouched.
|
|
386
|
+
*/
|
|
387
|
+
var PendingImport = class extends VicssError {
|
|
388
|
+
/** `[importer, source]` pairs, each once. */
|
|
389
|
+
requests;
|
|
390
|
+
/** Keys of the requests only a recipe probe made (`Compiler.probe`). */
|
|
391
|
+
probes;
|
|
392
|
+
constructor(requests, probes = false) {
|
|
393
|
+
super("PENDING_IMPORT", `${requests.map(([, source]) => `"${source}"`).join(", ")} not loaded yet`);
|
|
394
|
+
this.requests = requests;
|
|
395
|
+
this.probes = probes === true ? new Set(requests.map((request) => requestKey(...request))) : probes || /* @__PURE__ */ new Set();
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
var Compiler = class {
|
|
399
|
+
options;
|
|
400
|
+
system;
|
|
401
|
+
sources;
|
|
402
|
+
root;
|
|
403
|
+
modules = /* @__PURE__ */ new Map();
|
|
404
|
+
/** `importer\0source` → the module it loads, or why it cannot be loaded. */
|
|
405
|
+
requests = /* @__PURE__ */ new Map();
|
|
406
|
+
dependencies = /* @__PURE__ */ new Set();
|
|
407
|
+
/** Requests a recipe probe made for a package's module, which was not read (`load`). */
|
|
408
|
+
skipped = /* @__PURE__ */ new Set();
|
|
409
|
+
/** Above 0 while a callee that may be a recipe is evaluated (`probe`). */
|
|
410
|
+
probing = 0;
|
|
411
|
+
/** Exports being resolved, to stop re-export cycles that the evaluator never sees. */
|
|
412
|
+
resolving = /* @__PURE__ */ new Set();
|
|
413
|
+
/**
|
|
414
|
+
* `css()` calls being compiled: a function style evaluates in a context of
|
|
415
|
+
* its own, where the evaluator cannot see that it reads its own result.
|
|
416
|
+
*/
|
|
417
|
+
compiling = /* @__PURE__ */ new Set();
|
|
418
|
+
/** Every atom compiled in any module, by class name: folded classes report theirs. */
|
|
419
|
+
atomIndex = /* @__PURE__ */ new Map();
|
|
420
|
+
/** Every rule compiled in any module, by id. */
|
|
421
|
+
rules = /* @__PURE__ */ new Map();
|
|
422
|
+
/**
|
|
423
|
+
* This module's values outside their scales, by where they are written:
|
|
424
|
+
* a style evaluated in several passes counts once.
|
|
425
|
+
*/
|
|
426
|
+
arbitrary = /* @__PURE__ */ new Set();
|
|
427
|
+
/** The app's own class names this module's calls write (`CompileResult.names`). */
|
|
428
|
+
names = /* @__PURE__ */ new Set();
|
|
429
|
+
/** Module id → whether `privateAccess` lists it (PLAN §4.1). */
|
|
430
|
+
access = /* @__PURE__ */ new Map();
|
|
431
|
+
/** Every module's markers by family and id, as messages name them (`[row.hover]`). */
|
|
432
|
+
markerLabels = /* @__PURE__ */ new Map();
|
|
433
|
+
parseSync;
|
|
434
|
+
/** What `sx()` returns in this module, and the runtime functions of that shape. */
|
|
435
|
+
shape;
|
|
436
|
+
/** How a class is written into the output: readable, or a build's placeholder. */
|
|
437
|
+
emit;
|
|
438
|
+
pass;
|
|
439
|
+
constructor(code, options, sources, parseSync) {
|
|
440
|
+
this.options = options;
|
|
441
|
+
this.system = options.system;
|
|
442
|
+
this.sources = sources;
|
|
443
|
+
this.parseSync = parseSync;
|
|
444
|
+
this.shape = SX_SHAPES[options.framework ?? "react"];
|
|
445
|
+
this.emit = options.placeholders ? placeholder : readableClass;
|
|
446
|
+
this.root = this.createModule(options.id, code, options.lang, options.functionBody);
|
|
447
|
+
this.modules.set(options.id, this.root);
|
|
448
|
+
this.pass = newPass(code, options.keepLines === true);
|
|
449
|
+
}
|
|
450
|
+
run() {
|
|
451
|
+
const { root } = this;
|
|
452
|
+
this.pass = newPass(root.code, this.options.keepLines === true);
|
|
453
|
+
this.rewrite(root.program, this.pass.edits);
|
|
454
|
+
if (this.pass.pending.size > 0) throw new PendingImport([...this.pass.pending.values()], this.pass.probes);
|
|
455
|
+
for (const error of this.pass.errors) this.options.onError?.(error);
|
|
456
|
+
this.pruneImports();
|
|
457
|
+
const { edits, tokens, folded } = this.pass;
|
|
458
|
+
if (this.pass.devSx) edits.insert(root.code.length, `\nimport { ${this.shape.dev} } from "${RUNTIME_PACKAGE}";\n`);
|
|
459
|
+
const names = this.names.size > 0 && { names: [...this.names].sort(compareStrings) };
|
|
460
|
+
if (!edits.hasChanged()) return {
|
|
461
|
+
...unchanged(root.code),
|
|
462
|
+
arbitrary: this.arbitrary.size,
|
|
463
|
+
...names
|
|
464
|
+
};
|
|
465
|
+
const atoms = /* @__PURE__ */ new Map();
|
|
466
|
+
for (const style of root.styles.values()) for (const atom of style.atoms) keepAtom(atoms, atom);
|
|
467
|
+
for (const recipe of root.recipes.values()) for (const atom of recipe.atoms) keepAtom(atoms, atom);
|
|
468
|
+
const own = {
|
|
469
|
+
classes: [...atoms.keys()].sort(compareStrings),
|
|
470
|
+
rules: [.../* @__PURE__ */ new Set([...[...root.rules.values()].map((rule) => rule.id), ...[...root.recipes.values()].flatMap((recipe) => recipe.rules.map((rule) => rule.id))])].sort(compareStrings)
|
|
471
|
+
};
|
|
472
|
+
for (const cls of folded) {
|
|
473
|
+
const atom = this.atomIndex.get(cls);
|
|
474
|
+
if (atom && !atoms.has(cls)) atoms.set(cls, atom);
|
|
475
|
+
}
|
|
476
|
+
const file = root.id.replace(/[?#].*$/, "");
|
|
477
|
+
return {
|
|
478
|
+
code: edits.toString(),
|
|
479
|
+
map: this.options.sourceMap === false ? null : edits.generateMap({
|
|
480
|
+
source: file,
|
|
481
|
+
file,
|
|
482
|
+
includeContent: true,
|
|
483
|
+
hires: true
|
|
484
|
+
}),
|
|
485
|
+
atoms: [...atoms.values()].sort((a, b) => compareStrings(a.className, b.className)),
|
|
486
|
+
dependencies: [...this.dependencies],
|
|
487
|
+
tokens: [...tokens.values()],
|
|
488
|
+
rules: [...this.rules.values()].sort((a, b) => compareStrings(a.id, b.id)),
|
|
489
|
+
arbitrary: this.arbitrary.size,
|
|
490
|
+
...names,
|
|
491
|
+
own,
|
|
492
|
+
changed: true
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Reads a module the evaluation needs. For a `probe`, a module under
|
|
497
|
+
* `node_modules` is not parsed or recorded as a dependency: any call of an
|
|
498
|
+
* import (`useState()`, `useQuery({ … })`) is probed for a recipe, and a
|
|
499
|
+
* package's barrel would otherwise load its whole graph on every compile.
|
|
500
|
+
* Packages hold compiled code, which never yields a recipe to fold.
|
|
501
|
+
*/
|
|
502
|
+
async load(importer, source, probe = false) {
|
|
503
|
+
const key = requestKey(importer, source);
|
|
504
|
+
if (this.requests.has(key) || probe && this.skipped.has(key)) return;
|
|
505
|
+
let loaded = null;
|
|
506
|
+
let reason = `"${source}" cannot be loaded from ${importer}`;
|
|
507
|
+
try {
|
|
508
|
+
loaded = await this.options.loadModule(source, importer);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
reason = `"${source}" cannot be loaded from ${importer}: ${error instanceof Error ? error.message : String(error)}`;
|
|
511
|
+
}
|
|
512
|
+
if (!loaded) {
|
|
513
|
+
this.requests.set(key, reason);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (probe && /[\\/]node_modules[\\/]/.test(loaded.id)) {
|
|
517
|
+
this.skipped.add(key);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
let target = this.modules.get(loaded.id);
|
|
521
|
+
if (!target) {
|
|
522
|
+
try {
|
|
523
|
+
target = this.createModule(loaded.id, loaded.code);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (!(error instanceof VicssError)) throw error;
|
|
526
|
+
this.requests.set(key, `"${source}" (${loaded.id}) cannot be parsed: ${error.message}`);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this.modules.set(loaded.id, target);
|
|
530
|
+
}
|
|
531
|
+
this.requests.set(key, target);
|
|
532
|
+
if (target !== this.root) this.dependencies.add(loaded.id);
|
|
533
|
+
}
|
|
534
|
+
createModule(id, code, lang, functionBody = false) {
|
|
535
|
+
const parsedAs = isJson(id) ? "js" : lang ?? langOf(id);
|
|
536
|
+
const { program, scopes } = this.parse(id, code, parsedAs, functionBody);
|
|
537
|
+
const ctx = {
|
|
538
|
+
file: id,
|
|
539
|
+
code,
|
|
540
|
+
scopes,
|
|
541
|
+
resolveImport: (binding) => this.resolveImport(mod, binding),
|
|
542
|
+
callHandler: (callee, _binding, args, evaluateNode) => this.evaluateCall(mod, callee, args, evaluateNode),
|
|
543
|
+
isHarmlessCall: (call) => runtimeName(mod, call.callee) !== void 0 || isUnref(mod, call.callee, call.arguments)
|
|
544
|
+
};
|
|
545
|
+
const mod = {
|
|
546
|
+
id,
|
|
547
|
+
code,
|
|
548
|
+
program,
|
|
549
|
+
scopes,
|
|
550
|
+
runtime: runtimeBindings(program, scopes, this.sources),
|
|
551
|
+
vue: this.options.framework === "vue" && id === this.options.id,
|
|
552
|
+
lang: parsedAs,
|
|
553
|
+
ctx,
|
|
554
|
+
styles: /* @__PURE__ */ new Map(),
|
|
555
|
+
rules: /* @__PURE__ */ new Map(),
|
|
556
|
+
recipes: /* @__PURE__ */ new Map(),
|
|
557
|
+
markers: /* @__PURE__ */ new Map(),
|
|
558
|
+
markerKeys: /* @__PURE__ */ new Map(),
|
|
559
|
+
markerSites: void 0,
|
|
560
|
+
locate: createLocator(code)
|
|
561
|
+
};
|
|
562
|
+
return mod;
|
|
563
|
+
}
|
|
564
|
+
/** A parse from the shared cache, or a new one. JSON modules read as their exports. */
|
|
565
|
+
parse(id, code, lang, functionBody = false) {
|
|
566
|
+
const key = `${lang === langOf(id) ? id : `${id}\0${lang}`}${functionBody ? "\0body" : ""}`;
|
|
567
|
+
const cached = this.options.cache?.parsed.get(key);
|
|
568
|
+
if (cached?.code === code) return cached;
|
|
569
|
+
const source = isJson(id) ? jsonModule(id, code) : code;
|
|
570
|
+
const program = parse(this.parseSync, id, source, lang, functionBody);
|
|
571
|
+
const parsed = {
|
|
572
|
+
code,
|
|
573
|
+
program,
|
|
574
|
+
scopes: analyzeScopes(program)
|
|
575
|
+
};
|
|
576
|
+
this.options.cache?.parsed.set(key, parsed);
|
|
577
|
+
return parsed;
|
|
578
|
+
}
|
|
579
|
+
/** Rewrites the runtime calls under `node`. */
|
|
580
|
+
rewrite(node, editor) {
|
|
581
|
+
walk(this.root, node, (child) => {
|
|
582
|
+
if (child.type !== "CallExpression") return true;
|
|
583
|
+
const name = runtimeName(this.root, child.callee);
|
|
584
|
+
try {
|
|
585
|
+
if (name === void 0) return !this.foldRecipeCall(child, editor);
|
|
586
|
+
return !this.rewriteCall(child, name, editor);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
if (error instanceof VicssError && !(error instanceof PendingImport) && this.options.onError) {
|
|
589
|
+
this.pass.errors.push(error);
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
if (!(error instanceof PendingImport)) throw error;
|
|
593
|
+
const { pending, probes } = this.pass;
|
|
594
|
+
for (const request of error.requests) {
|
|
595
|
+
const key = requestKey(...request);
|
|
596
|
+
if (!error.probes.has(key)) probes.delete(key);
|
|
597
|
+
else if (!pending.has(key)) probes.add(key);
|
|
598
|
+
pending.set(key, request);
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
/** Returns whether the call was replaced; otherwise its arguments are rewritten in place. */
|
|
605
|
+
rewriteCall(call, name, editor) {
|
|
606
|
+
const { root } = this;
|
|
607
|
+
const evaluateHere = (node) => evaluate(node, root.ctx);
|
|
608
|
+
switch (name) {
|
|
609
|
+
case "css":
|
|
610
|
+
this.rewriteStyle(call, editor);
|
|
611
|
+
return true;
|
|
612
|
+
case "token": {
|
|
613
|
+
const token = this.tokenOf(root, call.callee, call.arguments, evaluateHere);
|
|
614
|
+
this.pass.tokens.set(`${token.scale}:${token.path}`, {
|
|
615
|
+
scale: token.scale,
|
|
616
|
+
path: token.path
|
|
617
|
+
});
|
|
618
|
+
this.replace(editor, call.start, call.end, JSON.stringify(`var(${token.cssVar})`));
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
case "sx":
|
|
622
|
+
if (this.fold(call, editor)) return true;
|
|
623
|
+
if (this.options.dev) this.devCall(call, editor);
|
|
624
|
+
return false;
|
|
625
|
+
case "keyframes":
|
|
626
|
+
case "theme": {
|
|
627
|
+
const rule = this.compileRule(root, name, call.callee, call.arguments, evaluateHere);
|
|
628
|
+
const text = name === "theme" ? `(${styleLiteral(rule.id, this.emit)})` : JSON.stringify(rule.id);
|
|
629
|
+
this.replace(editor, call.start, call.end, text);
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
case "recipe": {
|
|
633
|
+
const recipe = this.compileRecipe(root, call.callee, call.arguments, evaluateHere);
|
|
634
|
+
const helper = this.helper("__vxRecipe");
|
|
635
|
+
const extra = recipe.table.q ? `, ${this.helper("__vxResponsive")}` : "";
|
|
636
|
+
const table = this.options.dev && recipe.rules.length > 0 ? {
|
|
637
|
+
...recipe.table,
|
|
638
|
+
f: Object.fromEntries(recipe.rules.map((rule) => [rule.id, toRecord((rule.atoms ?? []).map((atom) => atom.className).join(" "))]))
|
|
639
|
+
} : recipe.table;
|
|
640
|
+
const text = `/*#__PURE__*/${helper}(${printTable(table, this.emit)}${extra})`;
|
|
641
|
+
const imports = root.program.body.filter((node) => node.type === "ImportDeclaration");
|
|
642
|
+
if (editor === this.pass.edits && imports.length > 0 && this.inFunction(call.start)) {
|
|
643
|
+
const local = this.hoistName();
|
|
644
|
+
this.pass.edits.insert(imports.at(-1).end, this.options.keepLines ? `;const ${local} = ${text};` : `\nconst ${local} = ${text};`);
|
|
645
|
+
this.replace(editor, call.start, call.end, local);
|
|
646
|
+
} else this.replace(editor, call.start, call.end, text);
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
case "group":
|
|
650
|
+
case "peer": {
|
|
651
|
+
const marker = this.markerOf(root, name, call.callee, call.arguments, evaluateHere);
|
|
652
|
+
this.replace(editor, call.start, call.end, `(${styleLiteral(marker.className, this.emit)})`);
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
default: return false;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/** Whether `offset` lies in a function (or a class field), which may run many times. */
|
|
659
|
+
inFunction(offset) {
|
|
660
|
+
const { root } = this;
|
|
661
|
+
if (!root.functions) {
|
|
662
|
+
const ranges = [];
|
|
663
|
+
walk(root, root.program, (node) => {
|
|
664
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "PropertyDefinition" && !node.static) {
|
|
665
|
+
ranges.push([node.start, node.end]);
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
});
|
|
670
|
+
root.functions = ranges;
|
|
671
|
+
}
|
|
672
|
+
return root.functions.some(([start, end]) => offset > start && offset < end);
|
|
673
|
+
}
|
|
674
|
+
/** A module-level name for a hoisted recipe, unused in the module. */
|
|
675
|
+
hoistName() {
|
|
676
|
+
const { code } = this.root;
|
|
677
|
+
let name;
|
|
678
|
+
do
|
|
679
|
+
name = `__vxRecipeTable${++this.pass.hoisted}`;
|
|
680
|
+
while (code.includes(name));
|
|
681
|
+
return name;
|
|
682
|
+
}
|
|
683
|
+
/** The local name of a runtime export the output calls; `pruneImports` adds the import. */
|
|
684
|
+
helper(name) {
|
|
685
|
+
const { helpers } = this.pass;
|
|
686
|
+
const known = helpers.get(name);
|
|
687
|
+
if (known) return known.local;
|
|
688
|
+
const { root } = this;
|
|
689
|
+
for (const binding of root.scopes.moduleBindings.values()) if (binding.kind === "import" && binding.source === RUNTIME_PACKAGE && binding.imported === name) {
|
|
690
|
+
helpers.set(name, {
|
|
691
|
+
local: binding.name,
|
|
692
|
+
add: false
|
|
693
|
+
});
|
|
694
|
+
return binding.name;
|
|
695
|
+
}
|
|
696
|
+
let local = name;
|
|
697
|
+
for (let i = 1; root.code.includes(local); i++) local = `${name}${i}`;
|
|
698
|
+
helpers.set(name, {
|
|
699
|
+
local,
|
|
700
|
+
add: true
|
|
701
|
+
});
|
|
702
|
+
return local;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* `button({ size: "sm" })` with a recipe the compiler knows and literal props
|
|
706
|
+
* becomes its classes (an object of them for slots), with up to D18's two
|
|
707
|
+
* runtime tests among the props. Returns whether the call was replaced.
|
|
708
|
+
*/
|
|
709
|
+
foldRecipeCall(call, editor) {
|
|
710
|
+
if (!this.mayBeRecipeCall(call)) return false;
|
|
711
|
+
const { root } = this;
|
|
712
|
+
const target = this.probe(() => attempt(() => evaluate(call.callee, root.ctx)));
|
|
713
|
+
if (!(target?.result instanceof RecipeValue)) return false;
|
|
714
|
+
const { recipe } = target.result;
|
|
715
|
+
const [arg] = call.arguments;
|
|
716
|
+
const props = arg ? this.foldProps(arg) : {
|
|
717
|
+
tests: [],
|
|
718
|
+
build: () => void 0
|
|
719
|
+
};
|
|
720
|
+
if (!props || props.tests.length > MAX_FOLD_CONDITIONS) return false;
|
|
721
|
+
const fail = this.propsError(root, call, arg);
|
|
722
|
+
const n = props.tests.length;
|
|
723
|
+
const slots = recipe.table.s;
|
|
724
|
+
const leaves = [];
|
|
725
|
+
for (let i = 0; i < 2 ** n; i++) {
|
|
726
|
+
const picks = props.tests.map((_, j) => (i >> n - 1 - j & 1) === 0);
|
|
727
|
+
const classes = resolveRecipe(recipe, props.build(picks), fail);
|
|
728
|
+
const styles = slots ? Object.values(classes) : [classes];
|
|
729
|
+
for (const style of styles) for (const cls of Object.values(style)) this.pass.folded.add(cls);
|
|
730
|
+
leaves.push(slots ? Object.fromEntries(slots.map((slot) => [slot, styleLiteral(classes[slot], this.emit)])) : { "": styleLiteral(classes, this.emit) });
|
|
731
|
+
}
|
|
732
|
+
const tests = props.tests.map((test) => this.testText(test));
|
|
733
|
+
const choose = (texts, depth = 0, at = 0) => {
|
|
734
|
+
if (depth === n) return texts[at];
|
|
735
|
+
const yes = choose(texts, depth + 1, at);
|
|
736
|
+
const no = choose(texts, depth + 1, at + 2 ** (n - depth - 1));
|
|
737
|
+
return yes === no ? yes : `${tests[depth]} ? ${yes} : ${no}`;
|
|
738
|
+
};
|
|
739
|
+
let text;
|
|
740
|
+
if (!slots) text = `(${choose(leaves.map((leaf) => leaf[""]))})`;
|
|
741
|
+
else text = `({ ${slots.map((slot) => {
|
|
742
|
+
const value = choose(leaves.map((leaf) => leaf[slot]));
|
|
743
|
+
return `${propertyKey(slot)}: ${value}`;
|
|
744
|
+
}).join(", ")} })`;
|
|
745
|
+
this.replace(editor, call.start, call.end, text);
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
/** Evaluates a callee that may be a recipe: packages it imports are not read (`load`). */
|
|
749
|
+
probe(evaluateCallee) {
|
|
750
|
+
this.probing++;
|
|
751
|
+
try {
|
|
752
|
+
return evaluateCallee();
|
|
753
|
+
} finally {
|
|
754
|
+
this.probing--;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* A call that could be a recipe's: its callee is a binding that can hold
|
|
759
|
+
* one (a `const` or an import, or a member of one), and it passes nothing
|
|
760
|
+
* or an object literal. Only these callees are evaluated.
|
|
761
|
+
*/
|
|
762
|
+
mayBeRecipeCall(call) {
|
|
763
|
+
const args = call.arguments;
|
|
764
|
+
if (args.length > 1 || args[0] && unwrap(args[0]).type !== "ObjectExpression") return false;
|
|
765
|
+
let target = unwrap(call.callee);
|
|
766
|
+
if (target.type === "CallExpression" && isUnref(this.root, target.callee, target.arguments)) target = unwrap(target.arguments[0]);
|
|
767
|
+
if (target.type === "MemberExpression") {
|
|
768
|
+
if (target.computed && target.property.type !== "Literal") return false;
|
|
769
|
+
target = unwrap(target.object);
|
|
770
|
+
}
|
|
771
|
+
if (target.type !== "Identifier") return false;
|
|
772
|
+
const binding = this.root.scopes.bindingOf(target);
|
|
773
|
+
if (!binding) return false;
|
|
774
|
+
if (binding.kind === "import") return binding.source !== RUNTIME_PACKAGE && !(this.sources.includes(binding.source) && RUNTIME_EXPORTS.has(binding.imported));
|
|
775
|
+
if (binding.kind !== "const" || !binding.init) return false;
|
|
776
|
+
const init = unwrap(binding.init).type;
|
|
777
|
+
return init !== "ArrowFunctionExpression" && init !== "FunctionExpression";
|
|
778
|
+
}
|
|
779
|
+
/** Static props, or an object literal of them with runtime tests between static values. */
|
|
780
|
+
foldProps(node) {
|
|
781
|
+
const known = attempt(() => evaluate(node, this.root.ctx));
|
|
782
|
+
if (known) return {
|
|
783
|
+
tests: [],
|
|
784
|
+
build: () => known.result
|
|
785
|
+
};
|
|
786
|
+
const tests = [];
|
|
787
|
+
const build = this.propsShape(unwrap(node), tests);
|
|
788
|
+
return build && {
|
|
789
|
+
tests,
|
|
790
|
+
build
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
propsShape(node, tests) {
|
|
794
|
+
if (node.type !== "ObjectExpression") return void 0;
|
|
795
|
+
const entries = [];
|
|
796
|
+
for (const property of node.properties) {
|
|
797
|
+
if (property.type === "SpreadElement" || property.method || property.kind !== "init") return;
|
|
798
|
+
let key = staticKey(property);
|
|
799
|
+
if (property.computed) {
|
|
800
|
+
const value = attempt(() => evaluate(property.key, this.root.ctx))?.result;
|
|
801
|
+
key = typeof value === "string" || typeof value === "number" ? String(value) : void 0;
|
|
802
|
+
}
|
|
803
|
+
const value = key === void 0 ? void 0 : this.propValue(property.value, tests);
|
|
804
|
+
if (key === void 0 || !value) return void 0;
|
|
805
|
+
entries.push([key, value]);
|
|
806
|
+
}
|
|
807
|
+
return (picks) => Object.fromEntries(entries.map(([key, value]) => [key, value(picks)]));
|
|
808
|
+
}
|
|
809
|
+
propValue(node, tests) {
|
|
810
|
+
const known = attempt(() => evaluate(node, this.root.ctx));
|
|
811
|
+
if (known) return () => known.result;
|
|
812
|
+
const inner = unwrap(node);
|
|
813
|
+
if (inner.type === "ObjectExpression") return this.propsShape(inner, tests);
|
|
814
|
+
if (inner.type !== "ConditionalExpression") return void 0;
|
|
815
|
+
const index = tests.push(inner.test) - 1;
|
|
816
|
+
const yes = this.propValue(inner.consequent, tests);
|
|
817
|
+
const no = yes && this.propValue(inner.alternate, tests);
|
|
818
|
+
return yes && no && ((picks) => picks[index] ? yes(picks) : no(picks));
|
|
819
|
+
}
|
|
820
|
+
/** Errors in a recipe call's props, located at the prop. */
|
|
821
|
+
propsError(mod, at, arg) {
|
|
822
|
+
return (path, message, hint) => new VicssError("BAD_VARIANT", path ? `${path}: ${message}` : message, {
|
|
823
|
+
loc: arg ? this.locateStyle(mod, arg, path) : this.loc(mod, at),
|
|
824
|
+
hint
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
rewriteStyle(call, editor) {
|
|
828
|
+
const style = this.compileStyle(this.root, call.callee, call.arguments);
|
|
829
|
+
const { fn, vars } = style;
|
|
830
|
+
if (!fn || !vars) {
|
|
831
|
+
this.replace(editor, call.start, call.end, `(${styleLiteral(style.classes, this.emit)})`);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const constant = this.localName("__vxStyle");
|
|
835
|
+
const memo = this.localName("__vxStyles");
|
|
836
|
+
const palettes = style.palettes ?? [];
|
|
837
|
+
this.replace(editor, call.start, fn.start, palettes.length === 0 ? `((${constant}) => ` : `((${constant}, ${memo} = {}) => `);
|
|
838
|
+
for (const param of fn.params) this.rewrite(param, editor);
|
|
839
|
+
const entries = [...new Map(vars.map((v) => [v.variable, v])).values()].map((v) => `${JSON.stringify(v.variable)}: ${this.runtimeValue(v.value, v.property)}`);
|
|
840
|
+
let classes = constant;
|
|
841
|
+
let body;
|
|
842
|
+
if (palettes.length === 0) body = entries.length > 0 ? `({ $c: ${classes}, $s: { ${entries.join(", ")} } })` : `({ $c: ${classes} })`;
|
|
843
|
+
else {
|
|
844
|
+
const picks = palettes.map((_, i) => `${memo}${i}`);
|
|
845
|
+
const add = palettes.map((p, i) => `...(${picks[i]} && { ${JSON.stringify(p.prefix.slice(0, -1))}: ${picks[i]} })`).join(", ");
|
|
846
|
+
classes = `(${memo}[${picks.join(" + \" \" + ")}] ??= { ...${constant}, ${add} })`;
|
|
847
|
+
const style$ = entries.length > 0 ? `{ $c: ${classes}, $s: { ${entries.join(", ")} } }` : `{ $c: ${classes} }`;
|
|
848
|
+
body = `((${picks.join(", ")}) => (${style$}))(${palettes.map((p) => this.paletteClass(p.prefix, p.value)).join(", ")})`;
|
|
849
|
+
}
|
|
850
|
+
this.replace(editor, fn.body.start, fn.body.end, body);
|
|
851
|
+
this.replace(editor, fn.end, call.end, `)(${styleLiteral(style.classes, this.emit)})`);
|
|
852
|
+
}
|
|
853
|
+
/** A name for a binding the output adds, unused in the module. */
|
|
854
|
+
localName(base) {
|
|
855
|
+
const { code } = this.root;
|
|
856
|
+
let name = base;
|
|
857
|
+
for (let i = 1; code.includes(name); i++) name = `${base}${i}`;
|
|
858
|
+
return name;
|
|
859
|
+
}
|
|
860
|
+
/** The class a runtime `colorPalette` picks (`"md:palette_" + tone`), or `""` for none. */
|
|
861
|
+
paletteClass(prefix, value) {
|
|
862
|
+
const named = JSON.stringify(this.emit(prefix));
|
|
863
|
+
if (isDynamic(value)) {
|
|
864
|
+
const { choice } = value;
|
|
865
|
+
if (choice) return `(${`${this.testText(choice.test)}${choice.nullish ? " != null" : ""}`} ? ${this.paletteClass(prefix, choice.consequent)} : ${this.paletteClass(prefix, choice.alternate)})`;
|
|
866
|
+
const { node } = value;
|
|
867
|
+
if (node.type === "Identifier") {
|
|
868
|
+
const name = node.name;
|
|
869
|
+
return `(${name} == null ? "" : ${named} + ${name})`;
|
|
870
|
+
}
|
|
871
|
+
return `((v) => v == null ? "" : ${named} + v)(${this.copy(node)})`;
|
|
872
|
+
}
|
|
873
|
+
return typeof value === "string" ? JSON.stringify(this.emit(prefix + value)) : "\"\"";
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* The expression that sets a custom property for `property`: tests run,
|
|
877
|
+
* numbers map like S9 at build time. In dev, a value the browser would
|
|
878
|
+
* drop warns once (runtime/dev.ts).
|
|
879
|
+
*/
|
|
880
|
+
runtimeValue(value, property) {
|
|
881
|
+
switch (value.kind) {
|
|
882
|
+
case "static": return value.css === void 0 ? "undefined" : JSON.stringify(value.css);
|
|
883
|
+
case "choice": return `(${`${this.testText(value.test)}${value.nullish ? " != null" : ""}`} ? ${this.runtimeValue(value.consequent, property)} : ${this.runtimeValue(value.alternate, property)})`;
|
|
884
|
+
case "token":
|
|
885
|
+
if (!this.options.dev) return tokenExpression(value.name, value.param);
|
|
886
|
+
return `${this.helper("__vxDevToken")}(${JSON.stringify(property)}, ${JSON.stringify(value.param.prefix)}, ${value.name})`;
|
|
887
|
+
case "runtime": {
|
|
888
|
+
const { node, numbers } = value;
|
|
889
|
+
const bare = node.type === "Identifier";
|
|
890
|
+
const text = bare ? node.name : this.copy(node, value.tokens);
|
|
891
|
+
const checked = (x) => this.options.dev ? `${this.helper("__vxDevValue")}(${JSON.stringify(property)}, ${x})` : x;
|
|
892
|
+
if (node.type === "TemplateLiteral") return checked(text);
|
|
893
|
+
const step = cssVarName(this.system.cssVarPrefix, "spacing", SPACING_STEP);
|
|
894
|
+
const map = (x) => {
|
|
895
|
+
const lookup = Object.keys(numbers.lookup).length > 0;
|
|
896
|
+
const fallback = {
|
|
897
|
+
calc: `"calc(var(${step}) * " + ${x} + ")"`,
|
|
898
|
+
px: `${x} + "px"`,
|
|
899
|
+
ms: `${x} + "ms"`,
|
|
900
|
+
deg: `${x} + "deg"`,
|
|
901
|
+
plain: `String(${x})`,
|
|
902
|
+
unset: "undefined"
|
|
903
|
+
}[numbers.fallback];
|
|
904
|
+
return `typeof ${x} === "number" ? ${lookup ? `${JSON.stringify(numbers.lookup)}[${x}] ?? ` : ""}${fallback} : ${x}`;
|
|
905
|
+
};
|
|
906
|
+
return checked(bare ? map(text) : `((v) => ${map("v")})(${text})`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* `sx(a, cond && b, x ? c : d)` with every style known at build time
|
|
912
|
+
* becomes its merged result, one string per combination of the tests.
|
|
913
|
+
*/
|
|
914
|
+
fold(call, editor) {
|
|
915
|
+
for (const arg of call.arguments) this.noteNames(arg);
|
|
916
|
+
const parts = [];
|
|
917
|
+
let conditions = 0;
|
|
918
|
+
for (const arg of call.arguments) {
|
|
919
|
+
if (arg.type === "SpreadElement") return false;
|
|
920
|
+
const known = this.known(arg);
|
|
921
|
+
if (known !== void 0) {
|
|
922
|
+
parts.push({ classes: known });
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const node = unwrap(arg);
|
|
926
|
+
let part;
|
|
927
|
+
if (node.type === "LogicalExpression" && node.operator === "&&") part = {
|
|
928
|
+
test: node.left,
|
|
929
|
+
ifTrue: this.known(node.right),
|
|
930
|
+
ifFalse: ""
|
|
931
|
+
};
|
|
932
|
+
else if (node.type === "ConditionalExpression") part = {
|
|
933
|
+
test: node.test,
|
|
934
|
+
ifTrue: this.known(node.consequent),
|
|
935
|
+
ifFalse: this.known(node.alternate)
|
|
936
|
+
};
|
|
937
|
+
else return false;
|
|
938
|
+
if (part.ifTrue === void 0 || part.ifFalse === void 0) return false;
|
|
939
|
+
parts.push({
|
|
940
|
+
test: part.test,
|
|
941
|
+
ifTrue: part.ifTrue,
|
|
942
|
+
ifFalse: part.ifFalse
|
|
943
|
+
});
|
|
944
|
+
conditions++;
|
|
945
|
+
}
|
|
946
|
+
if (conditions > MAX_FOLD_CONDITIONS) return false;
|
|
947
|
+
const tests = parts.flatMap((part) => "test" in part ? [this.testText(part.test)] : []);
|
|
948
|
+
let dropped = false;
|
|
949
|
+
let flattened = false;
|
|
950
|
+
const leaf = (picks) => {
|
|
951
|
+
let i = 0;
|
|
952
|
+
const chosen = parts.map((part) => "test" in part ? picks[i++] ? part.ifTrue : part.ifFalse : part.classes);
|
|
953
|
+
if (!dropped) dropped = this.warnDroppedThemes(call, chosen);
|
|
954
|
+
if (!flattened) flattened = this.warnFlattenedBases(call, chosen);
|
|
955
|
+
const merged = mergeClassNames(chosen);
|
|
956
|
+
for (const cls of merged) if (cls.startsWith("!")) this.names.add(cls.slice(1));
|
|
957
|
+
else this.pass.folded.add(cls);
|
|
958
|
+
return classAttribute(merged.join(" "), this.emit);
|
|
959
|
+
};
|
|
960
|
+
const choose = (picks) => picks.length === tests.length ? leaf(picks) : `${tests[picks.length]} ? ${choose([...picks, true])} : ${choose([...picks, false])}`;
|
|
961
|
+
const value = choose([]);
|
|
962
|
+
const src = this.options.dev ? `, "data-vx-src": ${this.sourceOf(call)}` : "";
|
|
963
|
+
this.replace(editor, call.start, call.end, `({ ${this.shape.attribute}: ${value}${src} })`);
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* A theme is one class with one merge key, so `sx(brand, round)` keeps
|
|
968
|
+
* `round` alone, as it keeps the last value of a property: two themes on
|
|
969
|
+
* one element could set the same custom properties (a token, or a token
|
|
970
|
+
* both re-declare as an alias), and the sheet's order, not the call's,
|
|
971
|
+
* would pick the winner. Folding sees the arguments, so it says so.
|
|
972
|
+
* Returns whether it warned.
|
|
973
|
+
*/
|
|
974
|
+
warnDroppedThemes(call, chosen) {
|
|
975
|
+
const seen = /* @__PURE__ */ new Set();
|
|
976
|
+
for (const cls of chosen.flatMap((classes) => classes.split(" "))) {
|
|
977
|
+
if (!cls.startsWith(`vx-theme_`)) continue;
|
|
978
|
+
seen.delete(cls);
|
|
979
|
+
seen.add(cls);
|
|
980
|
+
}
|
|
981
|
+
const themes = [...seen];
|
|
982
|
+
if (themes.length < 2) return false;
|
|
983
|
+
const kept = themes.at(-1);
|
|
984
|
+
const lost = themes.slice(0, -1).map((theme) => `\`${theme}\``);
|
|
985
|
+
const hint = "Write one theme() with every override, or apply the themes to nested elements.";
|
|
986
|
+
this.options.warn?.(`sx() keeps one theme per element, the last (\`${kept}\`): ${lost.join(", ")} ${lost.length === 1 ? "is" : "are"} dropped. ${hint}`, {
|
|
987
|
+
code: "DROPPED_THEME",
|
|
988
|
+
loc: {
|
|
989
|
+
file: this.root.id,
|
|
990
|
+
...this.root.locate(call.start)
|
|
991
|
+
},
|
|
992
|
+
hint
|
|
993
|
+
});
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Flattened recipe bases (core/flatten.ts) share one merge key, so an
|
|
998
|
+
* element keeps the last, as with themes; and a flattened base applies
|
|
999
|
+
* under every atom of its element, so an earlier argument's class it
|
|
1000
|
+
* would have cleared atomically stays. Folding sees both. Returns
|
|
1001
|
+
* whether it warned.
|
|
1002
|
+
*/
|
|
1003
|
+
warnFlattenedBases(call, chosen) {
|
|
1004
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1005
|
+
for (const cls of chosen.flatMap((classes) => classes.split(" "))) {
|
|
1006
|
+
if (!isBaseClass(cls)) continue;
|
|
1007
|
+
seen.delete(cls);
|
|
1008
|
+
seen.add(cls);
|
|
1009
|
+
}
|
|
1010
|
+
if (seen.size === 0) return false;
|
|
1011
|
+
const loc = {
|
|
1012
|
+
file: this.root.id,
|
|
1013
|
+
...this.root.locate(call.start)
|
|
1014
|
+
};
|
|
1015
|
+
const bases = [...seen];
|
|
1016
|
+
if (bases.length > 1) {
|
|
1017
|
+
const kept = bases.at(-1);
|
|
1018
|
+
const lost = bases.slice(0, -1).map((base) => `\`${base}\``);
|
|
1019
|
+
const hint = "Flatten one of the recipes only (`flatten: false` on the other), or apply them to nested elements.";
|
|
1020
|
+
this.options.warn?.(`sx() keeps one flattened recipe base per element, the last (\`${kept}\`): ${lost.join(", ")} ${lost.length === 1 ? "is" : "are"} dropped. ${hint}`, {
|
|
1021
|
+
code: "DROPPED_BASE",
|
|
1022
|
+
loc,
|
|
1023
|
+
hint
|
|
1024
|
+
});
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
const shadowed = shadowedByEarlier(chosen, (base) => this.rules.get(base)?.atoms?.map((atom) => atom.className));
|
|
1028
|
+
if (!shadowed) return false;
|
|
1029
|
+
const classes = shadowed.classes.map((cls) => `\`${cls}\``).join(", ");
|
|
1030
|
+
const hint = "Pass the recipe's result first (sx(recipe(…), …)), or leave its base atomic (`flatten: false`).";
|
|
1031
|
+
this.options.warn?.(`sx() applies the flattened recipe base \`${shadowed.base}\` under every class of the element: ${classes}, from an earlier argument, ${shadowed.classes.length === 1 ? "wins" : "win"} over what the base sets, which an atomic base would override. ${hint}`, {
|
|
1032
|
+
code: "BASE_ORDER",
|
|
1033
|
+
loc,
|
|
1034
|
+
hint
|
|
1035
|
+
});
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* `sx(a, b)` → `__vxSxDev("src/Card.tsx:12", a, b)`, in dev, for a call
|
|
1040
|
+
* that stays a runtime call: the variant of the module's shape.
|
|
1041
|
+
*/
|
|
1042
|
+
devCall(call, editor) {
|
|
1043
|
+
const { code } = this.root;
|
|
1044
|
+
const open = skipTrivia(code, Math.max(call.callee.end, call.typeArguments?.end ?? 0));
|
|
1045
|
+
if (call.optional || code[open] !== "(") return;
|
|
1046
|
+
const rest = call.arguments.length > 0 ? ", " : "";
|
|
1047
|
+
const dev = `${this.shape.dev}(${this.sourceOf(call)}${rest}`;
|
|
1048
|
+
this.replace(editor, call.callee.start, open + 1, dev);
|
|
1049
|
+
this.pass.devSx = true;
|
|
1050
|
+
}
|
|
1051
|
+
/** `"src/Card.tsx:12"`: where a call is, for `data-vx-src`. */
|
|
1052
|
+
sourceOf(call) {
|
|
1053
|
+
const file = this.root.id.replace(/[?#].*$/, "");
|
|
1054
|
+
const root = this.options.root?.replace(/\/+$/, "");
|
|
1055
|
+
const name = root && file.startsWith(`${root}/`) ? file.slice(root.length + 1) : file;
|
|
1056
|
+
const { line, column } = this.root.locate(call.start);
|
|
1057
|
+
const at = this.options.sourceLine ? this.options.sourceLine(line, column) : line;
|
|
1058
|
+
return JSON.stringify(at === void 0 ? name : `${name}:${at}`);
|
|
1059
|
+
}
|
|
1060
|
+
/** The classes an `sx()` argument adds, when the compiler knows them. */
|
|
1061
|
+
/** The classes a string written in an `sx()` argument names, through `&&` and `?:`. */
|
|
1062
|
+
noteNames(node) {
|
|
1063
|
+
const inner = unwrap(node);
|
|
1064
|
+
if (inner.type === "Literal" && typeof inner.value === "string") for (const cls of splitClasses(inner.value)) this.names.add(cls);
|
|
1065
|
+
else if (inner.type === "TemplateLiteral") inner.quasis.forEach((quasi, i) => {
|
|
1066
|
+
const text = quasi.value.cooked ?? "";
|
|
1067
|
+
const words = splitClasses(text);
|
|
1068
|
+
if (i > 0 && /^\S/.test(text)) words.shift();
|
|
1069
|
+
if (i < inner.quasis.length - 1 && /\S$/.test(text)) words.pop();
|
|
1070
|
+
for (const cls of words) this.names.add(cls);
|
|
1071
|
+
});
|
|
1072
|
+
else if (inner.type === "LogicalExpression") this.noteNames(inner.right);
|
|
1073
|
+
else if (inner.type === "ConditionalExpression") {
|
|
1074
|
+
this.noteNames(inner.consequent);
|
|
1075
|
+
this.noteNames(inner.alternate);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
known(node) {
|
|
1079
|
+
const value = attempt(() => evaluate(node, this.root.ctx));
|
|
1080
|
+
return value ? classesOf(value.result) : void 0;
|
|
1081
|
+
}
|
|
1082
|
+
testText(test) {
|
|
1083
|
+
const text = this.copy(test);
|
|
1084
|
+
return BARE_TESTS.has(test.type) ? text : `(${text})`;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Source text moved into generated code, with the runtime calls inside it
|
|
1088
|
+
* rewritten, and token-typed parameters read as their `var(--…)`.
|
|
1089
|
+
*/
|
|
1090
|
+
copy(node, tokens) {
|
|
1091
|
+
this.pass.spans.push({
|
|
1092
|
+
start: node.start,
|
|
1093
|
+
end: node.end,
|
|
1094
|
+
live: true
|
|
1095
|
+
});
|
|
1096
|
+
const splice = new Splice(this.root.code, node.start, node.end);
|
|
1097
|
+
this.rewrite(node, splice);
|
|
1098
|
+
if (tokens && tokens.size > 0) {
|
|
1099
|
+
const { scopes } = this.root;
|
|
1100
|
+
const paramOf = (id) => {
|
|
1101
|
+
if (id.type !== "Identifier") return void 0;
|
|
1102
|
+
const binding = scopes.bindingOf(id);
|
|
1103
|
+
return binding && binding.id !== id ? tokens.get(binding) : void 0;
|
|
1104
|
+
};
|
|
1105
|
+
walk(this.root, node, (child) => {
|
|
1106
|
+
if (splice.touches(child.start, child.end)) return false;
|
|
1107
|
+
if (child.type === "Property" && child.shorthand) {
|
|
1108
|
+
const param = paramOf(child.value);
|
|
1109
|
+
if (!param) return true;
|
|
1110
|
+
const name = child.value.name;
|
|
1111
|
+
splice.overwrite(child.start, child.end, `${name}: ${tokenExpression(name, param)}`);
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
const param = paramOf(child);
|
|
1115
|
+
if (param) {
|
|
1116
|
+
const name = child.name;
|
|
1117
|
+
const text = tokenExpression(name, param);
|
|
1118
|
+
splice.overwrite(child.start, child.end, param.nullable ? text : `(${text})`);
|
|
1119
|
+
}
|
|
1120
|
+
return true;
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
return splice.toString();
|
|
1124
|
+
}
|
|
1125
|
+
replace(editor, start, end, content) {
|
|
1126
|
+
this.pass.spans.push({
|
|
1127
|
+
start,
|
|
1128
|
+
end,
|
|
1129
|
+
live: false
|
|
1130
|
+
});
|
|
1131
|
+
if (content === "") editor.remove(start, end);
|
|
1132
|
+
else editor.overwrite(start, end, content);
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Drops import specifiers that only rewritten code referenced, and
|
|
1136
|
+
* declarations left empty. Specifiers that were never referenced as values
|
|
1137
|
+
* (types, side effects, JSX the scope analysis does not see) stay. An `sx`
|
|
1138
|
+
* the output still calls is pointed at the framework's shape (PLAN §5.12):
|
|
1139
|
+
* renamed in an import of the runtime, imported from the runtime instead of
|
|
1140
|
+
* a wrapper module (whose re-exports need not include the shapes).
|
|
1141
|
+
*/
|
|
1142
|
+
pruneImports() {
|
|
1143
|
+
const { root } = this;
|
|
1144
|
+
const references = [];
|
|
1145
|
+
const jsx = /* @__PURE__ */ new Set();
|
|
1146
|
+
/** `ns.sx` where `ns` is a namespace import of a runtime source. */
|
|
1147
|
+
const members = [];
|
|
1148
|
+
walk(root, root.program, (node) => {
|
|
1149
|
+
if (node.type === "JSXIdentifier") jsx.add(node.name);
|
|
1150
|
+
if (node.type === "MemberExpression" && memberKey(node) === "sx") {
|
|
1151
|
+
const binding = namespaceOf(root, node.object);
|
|
1152
|
+
if (binding) members.push({
|
|
1153
|
+
node,
|
|
1154
|
+
binding
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
if (node.type !== "Identifier") return true;
|
|
1158
|
+
const binding = root.scopes.bindingOf(node);
|
|
1159
|
+
if (binding?.kind === "import" && binding.id !== node) references.push({
|
|
1160
|
+
binding,
|
|
1161
|
+
offset: node.start
|
|
1162
|
+
});
|
|
1163
|
+
return true;
|
|
1164
|
+
});
|
|
1165
|
+
const live = this.liveness(references.map((reference) => reference.offset));
|
|
1166
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1167
|
+
const kept = /* @__PURE__ */ new Set();
|
|
1168
|
+
references.forEach(({ binding }, i) => {
|
|
1169
|
+
referenced.add(binding);
|
|
1170
|
+
if (live[i]) kept.add(binding);
|
|
1171
|
+
});
|
|
1172
|
+
const { code } = root;
|
|
1173
|
+
const edits = this.pass.edits;
|
|
1174
|
+
const target = this.shape.target;
|
|
1175
|
+
const direct = (locals) => ` import { ${locals.map((local) => `${target} as ${local}`).join(", ")} } from "${RUNTIME_PACKAGE}";`;
|
|
1176
|
+
const renamed = /* @__PURE__ */ new Map();
|
|
1177
|
+
const slice = (node) => renamed.get(node) ?? code.slice(node.start, node.end);
|
|
1178
|
+
const added = [...this.pass.helpers].filter(([, helper]) => helper.add).map(([name, { local }]) => local === name ? name : `${name} as ${local}`);
|
|
1179
|
+
const host = added.length === 0 ? void 0 : root.program.body.find((node) => node.type === "ImportDeclaration" && node.importKind !== "type" && node.source.value === RUNTIME_PACKAGE && node.specifiers.some((s) => s.type === "ImportSpecifier"));
|
|
1180
|
+
if (added.length > 0 && !host) {
|
|
1181
|
+
const first = root.program.body.find((node) => !(node.type === "ExpressionStatement" && typeof node.directive === "string"));
|
|
1182
|
+
const statement = `import { ${added.join(", ")} } from "${RUNTIME_PACKAGE}";`;
|
|
1183
|
+
if (first) edits.insert(first.start, `${statement}${this.options.keepLines ? " " : "\n"}`);
|
|
1184
|
+
else edits.insert(code.length, `\n${statement}\n`);
|
|
1185
|
+
}
|
|
1186
|
+
for (const declaration of root.program.body) {
|
|
1187
|
+
if (declaration.type !== "ImportDeclaration" || declaration.importKind === "type") continue;
|
|
1188
|
+
const remaining = new Set(declaration.specifiers.filter((specifier) => {
|
|
1189
|
+
const binding = root.scopes.bindingOf(specifier.local);
|
|
1190
|
+
return !binding || !referenced.has(binding) || kept.has(binding) || jsx.has(binding.name);
|
|
1191
|
+
}));
|
|
1192
|
+
const moved = [];
|
|
1193
|
+
for (const specifier of target ? remaining : []) {
|
|
1194
|
+
const binding = root.scopes.bindingOf(specifier.local);
|
|
1195
|
+
if (!binding || !kept.has(binding) || root.runtime.get(binding) !== "sx") continue;
|
|
1196
|
+
if (declaration.source.value === RUNTIME_PACKAGE) renamed.set(specifier, `${target} as ${specifier.local.name}`);
|
|
1197
|
+
else {
|
|
1198
|
+
remaining.delete(specifier);
|
|
1199
|
+
moved.push(specifier.local.name);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (moved.length > 0) edits.insert(declaration.end, direct(moved));
|
|
1203
|
+
if (declaration === host) {
|
|
1204
|
+
this.addHelpers(declaration, remaining, added, slice);
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
const named = declaration.specifiers.filter((s) => s.type === "ImportSpecifier");
|
|
1208
|
+
if (named.some((s) => renamed.has(s)) && remaining.size === declaration.specifiers.length) {
|
|
1209
|
+
edits.overwrite(named[0].start, named.at(-1).end, named.map(slice).join(", "));
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (remaining.size === declaration.specifiers.length) continue;
|
|
1213
|
+
if (remaining.size === 0) {
|
|
1214
|
+
if (declaration.source.value !== RUNTIME_PACKAGE) {
|
|
1215
|
+
edits.overwrite(declaration.start, declaration.end, `import ${code.slice(declaration.source.start, declaration.end)}`);
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
const end = code.startsWith("\r\n", declaration.end) ? declaration.end + 2 : code[declaration.end] === "\n" ? declaration.end + 1 : declaration.end;
|
|
1219
|
+
edits.remove(declaration.start, end);
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
const clause = declaration.specifiers.find((s) => s.type !== "ImportSpecifier");
|
|
1223
|
+
const namespace = declaration.specifiers.find((s) => s.type === "ImportNamespaceSpecifier");
|
|
1224
|
+
const keptNamed = named.filter((s) => remaining.has(s));
|
|
1225
|
+
if (named.length > 0 && keptNamed.length === 0) {
|
|
1226
|
+
const close = skipTrivia(code, named.at(-1).end);
|
|
1227
|
+
const brace = code[close] === "," ? skipTrivia(code, close + 1) : close;
|
|
1228
|
+
edits.remove(clause.end, brace + 1);
|
|
1229
|
+
} else if (keptNamed.length < named.length || keptNamed.some((s) => renamed.has(s))) edits.overwrite(named[0].start, named.at(-1).end, keptNamed.map(slice).join(", "));
|
|
1230
|
+
const defaulted = declaration.specifiers.find((s) => s.type === "ImportDefaultSpecifier");
|
|
1231
|
+
if (defaulted && !remaining.has(defaulted)) {
|
|
1232
|
+
const next = namespace ?? named[0];
|
|
1233
|
+
edits.remove(defaulted.start, next === namespace ? next.start : openBrace(code, defaulted.end));
|
|
1234
|
+
} else if (namespace && !remaining.has(namespace) && defaulted) edits.remove(defaulted.end, namespace.end);
|
|
1235
|
+
}
|
|
1236
|
+
if (!target) return;
|
|
1237
|
+
let alias = false;
|
|
1238
|
+
for (const { node, binding } of members) {
|
|
1239
|
+
if (this.pass.spans.some((s) => !s.live && s.start <= node.start && node.end <= s.end)) continue;
|
|
1240
|
+
if (binding.source === RUNTIME_PACKAGE) {
|
|
1241
|
+
const property = node.property;
|
|
1242
|
+
edits.overwrite(property.start, property.end, node.computed ? JSON.stringify(target) : target);
|
|
1243
|
+
} else {
|
|
1244
|
+
edits.overwrite(node.start, node.end, "__vxSx");
|
|
1245
|
+
const declaration = root.program.body.find((d) => d.type === "ImportDeclaration" && d.specifiers.some((s) => s === binding.node));
|
|
1246
|
+
if (!alias && declaration) edits.insert(declaration.end, direct(["__vxSx"]));
|
|
1247
|
+
alias = true;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Prunes the runtime import that takes the helpers, which has named
|
|
1253
|
+
* specifiers and no namespace: `import { recipe, sx }` → `import { sx, __vxRecipe }`.
|
|
1254
|
+
* `slice` gives a kept specifier's text, an `sx` pointed at its shape included.
|
|
1255
|
+
*/
|
|
1256
|
+
addHelpers(declaration, remaining, added, slice) {
|
|
1257
|
+
const { code } = this.root;
|
|
1258
|
+
const edits = this.pass.edits;
|
|
1259
|
+
const named = declaration.specifiers.filter((s) => s.type === "ImportSpecifier");
|
|
1260
|
+
if (remaining.size === 0) {
|
|
1261
|
+
const from = code.slice(declaration.source.start, declaration.end);
|
|
1262
|
+
edits.overwrite(declaration.start, declaration.end, `import { ${added.join(", ")} } from ${from}`);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const keptNamed = named.filter((s) => remaining.has(s)).map(slice);
|
|
1266
|
+
if (keptNamed.length === named.length && named.every((s, i) => keptNamed[i] === code.slice(s.start, s.end))) edits.insert(named.at(-1).end, `, ${added.join(", ")}`);
|
|
1267
|
+
else edits.overwrite(named[0].start, named.at(-1).end, [...keptNamed, ...added].join(", "));
|
|
1268
|
+
const defaulted = declaration.specifiers.find((s) => s.type === "ImportDefaultSpecifier");
|
|
1269
|
+
if (defaulted && !remaining.has(defaulted)) edits.remove(defaulted.start, openBrace(code, defaulted.end));
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Whether the output still holds the original code at each offset: the
|
|
1273
|
+
* innermost span around it decides. Spans nest or are disjoint, so one
|
|
1274
|
+
* sweep with a stack of open spans answers every offset.
|
|
1275
|
+
*/
|
|
1276
|
+
liveness(offsets) {
|
|
1277
|
+
const spans = this.pass.spans.map((span, index) => ({
|
|
1278
|
+
span,
|
|
1279
|
+
index
|
|
1280
|
+
})).sort((a, b) => a.span.start - b.span.start || b.span.end - a.span.end || a.index - b.index).map((entry) => entry.span);
|
|
1281
|
+
const order = offsets.map((_, i) => i).sort((a, b) => offsets[a] - offsets[b]);
|
|
1282
|
+
const open = [];
|
|
1283
|
+
const out = [];
|
|
1284
|
+
let next = 0;
|
|
1285
|
+
for (const i of order) {
|
|
1286
|
+
const offset = offsets[i];
|
|
1287
|
+
for (let span = spans[next]; span && span.start <= offset; span = spans[++next]) {
|
|
1288
|
+
while (open.length > 0 && open.at(-1).end <= span.start) open.pop();
|
|
1289
|
+
open.push(span);
|
|
1290
|
+
}
|
|
1291
|
+
while (open.length > 0 && open.at(-1).end <= offset) open.pop();
|
|
1292
|
+
out[i] = open.at(-1)?.live ?? true;
|
|
1293
|
+
}
|
|
1294
|
+
return out;
|
|
1295
|
+
}
|
|
1296
|
+
/** Gives runtime calls their compile-time value inside a static expression. */
|
|
1297
|
+
evaluateCall(mod, callee, args, evaluateNode) {
|
|
1298
|
+
if (isUnref(mod, callee, args)) return evaluateNode(args[0]);
|
|
1299
|
+
const name = runtimeName(mod, callee);
|
|
1300
|
+
if (name === void 0) return this.evaluateRecipeCall(mod, callee, args, evaluateNode);
|
|
1301
|
+
switch (name) {
|
|
1302
|
+
case "css": {
|
|
1303
|
+
const style = this.compileStyle(mod, callee, args);
|
|
1304
|
+
return style.vars ? FUNCTION_STYLE : new CompiledStyle(style.classes);
|
|
1305
|
+
}
|
|
1306
|
+
case "recipe": return new RecipeValue(this.compileRecipe(mod, callee, args, evaluateNode));
|
|
1307
|
+
case "group":
|
|
1308
|
+
case "peer": return this.markerOf(mod, name, callee, args, evaluateNode);
|
|
1309
|
+
case "token": {
|
|
1310
|
+
const token = this.tokenOf(mod, callee, args, evaluateNode);
|
|
1311
|
+
return `{${token.scale}.${token.path}}`;
|
|
1312
|
+
}
|
|
1313
|
+
case "keyframes": return this.compileRule(mod, "keyframes", callee, args, evaluateNode).id;
|
|
1314
|
+
case "theme": return new CompiledStyle(this.compileRule(mod, "theme", callee, args, evaluateNode).id);
|
|
1315
|
+
default: return NOT_HANDLED;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* A call of a recipe with static props, inside a static expression: its
|
|
1320
|
+
* classes, as a compiled style or an object of them per slot. Any other
|
|
1321
|
+
* call is not handled, and the evaluator reports it.
|
|
1322
|
+
*/
|
|
1323
|
+
evaluateRecipeCall(mod, callee, args, evaluateNode) {
|
|
1324
|
+
const [arg] = args;
|
|
1325
|
+
if (args.length > 1 || arg?.type === "SpreadElement") return NOT_HANDLED;
|
|
1326
|
+
const target = this.probe(() => attempt(() => evaluateNode(callee)));
|
|
1327
|
+
if (!(target?.result instanceof RecipeValue)) return NOT_HANDLED;
|
|
1328
|
+
const props = arg ? evaluateNode(arg) : void 0;
|
|
1329
|
+
const { recipe } = target.result;
|
|
1330
|
+
const classes = resolveRecipe(recipe, props, this.propsError(mod, callee, arg));
|
|
1331
|
+
if (!recipe.table.s) return new CompiledStyle(fromRecord(classes));
|
|
1332
|
+
return Object.fromEntries(Object.entries(classes).map(([slot, style]) => [slot, new CompiledStyle(fromRecord(style))]));
|
|
1333
|
+
}
|
|
1334
|
+
/** A `recipe()` call: its table and atoms (PLAN §5.7). */
|
|
1335
|
+
compileRecipe(mod, callee, args, evaluateNode) {
|
|
1336
|
+
const cached = mod.recipes.get(args);
|
|
1337
|
+
if (cached) return cached;
|
|
1338
|
+
const [arg] = args;
|
|
1339
|
+
if (args.length !== 1 || !arg || arg.type === "SpreadElement") throw this.error(mod, callee, "BAD_CALL", "recipe() takes one definition object", RECIPE_HINT);
|
|
1340
|
+
const definition = evaluateNode(arg);
|
|
1341
|
+
const { cache } = this.options;
|
|
1342
|
+
const key = cache?.recipes && (mod !== this.root || !this.options.warn) ? `${recipeKey(definition)}${this.privateAccess(mod) ? "\0private" : ""}` : void 0;
|
|
1343
|
+
let shared = key === void 0 ? void 0 : cache.recipes.get(this.system);
|
|
1344
|
+
if (key !== void 0 && !shared) cache.recipes.set(this.system, shared = /* @__PURE__ */ new Map());
|
|
1345
|
+
const hit = key === void 0 ? void 0 : shared.get(key);
|
|
1346
|
+
const recipe = hit ?? buildRecipe(definition, {
|
|
1347
|
+
system: this.system,
|
|
1348
|
+
normalize: (style, path) => {
|
|
1349
|
+
if (containsCompiled(style)) throw new VicssError("BAD_STYLE", `${path}: a compiled style is used inside a recipe; recipes take style objects`, {
|
|
1350
|
+
loc: this.locateStyle(mod, arg, path),
|
|
1351
|
+
hint: "Write the style object itself."
|
|
1352
|
+
});
|
|
1353
|
+
return normalizeStyle(style, {
|
|
1354
|
+
...this.normalizeContext(mod, arg),
|
|
1355
|
+
root: path
|
|
1356
|
+
});
|
|
1357
|
+
},
|
|
1358
|
+
error: (path, code, message, hint) => new VicssError(code, message, {
|
|
1359
|
+
loc: this.locateStyle(mod, arg, path),
|
|
1360
|
+
hint
|
|
1361
|
+
})
|
|
1362
|
+
});
|
|
1363
|
+
if (key !== void 0 && !hit) {
|
|
1364
|
+
if (shared.size >= RECIPE_CACHE_SIZE) shared.clear();
|
|
1365
|
+
shared.set(key, recipe);
|
|
1366
|
+
}
|
|
1367
|
+
mod.recipes.set(args, recipe);
|
|
1368
|
+
for (const atom of recipe.atoms) if (!this.atomIndex.has(atom.className)) this.atomIndex.set(atom.className, atom);
|
|
1369
|
+
for (const rule of recipe.rules) if (!this.rules.has(rule.id)) this.rules.set(rule.id, rule);
|
|
1370
|
+
return recipe;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* A `group()` or `peer()` call. Its key in the module is the name it is
|
|
1374
|
+
* given, else the const it initialises, else its position among the
|
|
1375
|
+
* module's marker calls; the id hashes the key with the module's path.
|
|
1376
|
+
*/
|
|
1377
|
+
markerOf(mod, kind, callee, args, evaluateNode) {
|
|
1378
|
+
const cached = mod.markers.get(args);
|
|
1379
|
+
if (cached) return cached;
|
|
1380
|
+
const [arg] = args;
|
|
1381
|
+
const hint = `Write const row = ${kind}(), or ${kind}("row") to name it.`;
|
|
1382
|
+
if (args.length > 1 || arg?.type === "SpreadElement") throw this.error(mod, callee, "BAD_CALL", `${kind}() takes an optional name`, hint);
|
|
1383
|
+
let name;
|
|
1384
|
+
if (arg) {
|
|
1385
|
+
const value = evaluateNode(arg);
|
|
1386
|
+
if (typeof value !== "string" || !/^[\w$-]+$/.test(value)) throw this.error(mod, arg, "BAD_CALL", `a ${kind}() name is a string of letters, digits, \`_\`, \`$\` or \`-\``, hint);
|
|
1387
|
+
name = value;
|
|
1388
|
+
}
|
|
1389
|
+
mod.markerSites ??= markerSites(mod.program, (call) => {
|
|
1390
|
+
const callName = runtimeName(mod, call.callee);
|
|
1391
|
+
return callName === "group" || callName === "peer";
|
|
1392
|
+
});
|
|
1393
|
+
const site = mod.markerSites.get(args);
|
|
1394
|
+
const key = name ?? site?.binding ?? `#${site?.index ?? 0}`;
|
|
1395
|
+
const family = kind === "group" ? "g" : "p";
|
|
1396
|
+
const claimed = mod.markerKeys.get(`${family}:${key}`);
|
|
1397
|
+
if (claimed && claimed !== args) throw this.error(mod, callee, "DUPLICATE_MARKER", `another ${kind}() in this module is also called \`${key}\`, so the two would mark elements with one class`, `Name one of them: ${kind}("${key}2").`);
|
|
1398
|
+
mod.markerKeys.set(`${family}:${key}`, args);
|
|
1399
|
+
const marker = new MarkerValue(family, markerId(mod.id, this.options.root ?? process.cwd(), key, this.options.package), this.system);
|
|
1400
|
+
mod.markers.set(args, marker);
|
|
1401
|
+
this.markerLabels.set(`${family}:${marker.id}`, markerLabel(family, key));
|
|
1402
|
+
return marker;
|
|
1403
|
+
}
|
|
1404
|
+
/** A `keyframes()` or `theme()` call: its rule, which names the call's result. */
|
|
1405
|
+
compileRule(mod, kind, callee, args, evaluateNode) {
|
|
1406
|
+
const cached = mod.rules.get(args);
|
|
1407
|
+
if (cached) return cached;
|
|
1408
|
+
const [arg] = args;
|
|
1409
|
+
if (args.length !== 1 || !arg || arg.type === "SpreadElement") throw this.error(mod, callee, "BAD_CALL", kind === "theme" ? "theme() takes one object of token overrides" : "keyframes() takes one object of frames", kind === "theme" ? THEME_HINT : KEYFRAMES_HINT);
|
|
1410
|
+
const value = evaluateNode(arg);
|
|
1411
|
+
const rule = kind === "theme" ? compileTheme(value, this.system, { locate: (path) => this.locateStyle(mod, arg, path) }) : compileKeyframes(value, this.normalizeContext(mod, arg));
|
|
1412
|
+
mod.rules.set(args, rule);
|
|
1413
|
+
if (!this.rules.has(rule.id)) this.rules.set(rule.id, rule);
|
|
1414
|
+
return rule;
|
|
1415
|
+
}
|
|
1416
|
+
compileStyle(mod, callee, args) {
|
|
1417
|
+
const cached = mod.styles.get(args);
|
|
1418
|
+
if (cached) return cached;
|
|
1419
|
+
if (this.compiling.has(args)) throw this.error(mod, callee, "NOT_STATIC", "this style reads its own result", "A style cannot depend on the value it compiles to.");
|
|
1420
|
+
this.compiling.add(args);
|
|
1421
|
+
try {
|
|
1422
|
+
return this.compileStyleOnce(mod, callee, args);
|
|
1423
|
+
} finally {
|
|
1424
|
+
this.compiling.delete(args);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
compileStyleOnce(mod, callee, args) {
|
|
1428
|
+
const [arg, options] = args;
|
|
1429
|
+
if (args.length < 1 || args.length > 2 || !arg || arg.type === "SpreadElement" || options?.type === "SpreadElement") throw this.error(mod, callee, "BAD_CALL", "css() takes one style object, and its options", CSS_HINT);
|
|
1430
|
+
const inner = unwrap(arg);
|
|
1431
|
+
if (inner.type === "FunctionExpression") throw this.error(mod, inner, "BAD_STYLE", "a function style is an arrow function", CSS_HINT);
|
|
1432
|
+
const name = options && this.styleName(mod, options);
|
|
1433
|
+
let style = inner.type === "ArrowFunctionExpression" ? this.compileFunctionStyle(mod, inner) : this.compileStaticStyle(mod, arg);
|
|
1434
|
+
if (name) style = {
|
|
1435
|
+
...style,
|
|
1436
|
+
classes: `!${name} ${style.classes}`.trim()
|
|
1437
|
+
};
|
|
1438
|
+
mod.styles.set(args, style);
|
|
1439
|
+
for (const atom of style.atoms) if (!this.atomIndex.has(atom.className)) this.atomIndex.set(atom.className, atom);
|
|
1440
|
+
return style;
|
|
1441
|
+
}
|
|
1442
|
+
/** `css(style, { name: "card" })`: the class the style adds as it is. */
|
|
1443
|
+
styleName(mod, node) {
|
|
1444
|
+
const value = evaluate(node, mod.ctx);
|
|
1445
|
+
const keys = isPlainObject(value) ? Object.keys(value) : void 0;
|
|
1446
|
+
const name = isPlainObject(value) ? value.name : void 0;
|
|
1447
|
+
const bad = keys?.find((key) => key !== "name");
|
|
1448
|
+
if (!keys || bad !== void 0) throw this.error(mod, node, "BAD_CALL", bad === void 0 ? "css() takes its options as an object: { name }" : `\`${bad}\` is not an option of css(): it takes \`name\``, "Write css({ … }, { name: \"card\" }).");
|
|
1449
|
+
if (name === void 0) return void 0;
|
|
1450
|
+
if (typeof name !== "string" || !CLASS_NAME.test(name) || name.startsWith("vx-")) throw this.error(mod, node, "BAD_CALL", typeof name === "string" && name.startsWith("vx-") ? "class names starting with `vx-` are reserved for vicss" : "a style's `name` is one class name: letters, digits, `-` and `_`, not starting with a digit", "Write css({ … }, { name: \"card\" }).");
|
|
1451
|
+
this.names.add(name);
|
|
1452
|
+
return name;
|
|
1453
|
+
}
|
|
1454
|
+
compileStaticStyle(mod, arg) {
|
|
1455
|
+
const value = evaluate(arg, mod.ctx);
|
|
1456
|
+
this.checkComposition(mod, arg, value, mod.ctx);
|
|
1457
|
+
const atoms = normalizeStyle(value, this.normalizeContext(mod, arg));
|
|
1458
|
+
return {
|
|
1459
|
+
classes: classString(atoms),
|
|
1460
|
+
atoms
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* PLAN §5.8: parameters are runtime values. Every property they reach
|
|
1465
|
+
* becomes an atom reading `var(--vx-<hash>)`; the rest compiles statically.
|
|
1466
|
+
*/
|
|
1467
|
+
compileFunctionStyle(mod, fn) {
|
|
1468
|
+
if (fn.async) throw this.error(mod, fn, "BAD_STYLE", "a function style cannot be async");
|
|
1469
|
+
const body = unwrap(fn.body);
|
|
1470
|
+
if (body.type !== "ObjectExpression") throw this.error(mod, fn.body, "BAD_STYLE", "a function style must return an object literal", "Write css((value) => ({ width: value })).");
|
|
1471
|
+
const params = /* @__PURE__ */ new Map();
|
|
1472
|
+
for (const param of fn.params) for (const id of bindingIds(param)) {
|
|
1473
|
+
const binding = mod.scopes.bindingOf(id);
|
|
1474
|
+
if (binding) params.set(binding, {
|
|
1475
|
+
__vxSpecial: "dynamic",
|
|
1476
|
+
node: id
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
const tokenParams = this.tokenParams(mod, fn);
|
|
1480
|
+
const ctx = {
|
|
1481
|
+
...mod.ctx,
|
|
1482
|
+
params
|
|
1483
|
+
};
|
|
1484
|
+
const value = evaluate(body, ctx);
|
|
1485
|
+
this.checkComposition(mod, body, value, ctx);
|
|
1486
|
+
const found = [];
|
|
1487
|
+
const normalize = this.normalizeContext(mod, body);
|
|
1488
|
+
const { style: withoutPalettes, palettes, paletteAtoms } = this.runtimePalettes(mod, value, normalize);
|
|
1489
|
+
const style = withPlaceholders(withoutPalettes, found);
|
|
1490
|
+
this.checkRuntimeTargets(mod, body, style, "");
|
|
1491
|
+
const vars = [];
|
|
1492
|
+
const atoms = normalizeStyle(style, normalize).map((atom) => {
|
|
1493
|
+
const index = placeholderIndex(atom.value);
|
|
1494
|
+
if (index === void 0) {
|
|
1495
|
+
if (atom.value.includes(PLACEHOLDER)) throw this.error(mod, body, "BAD_STYLE", `\`${atom.property}\` cannot take a runtime value`);
|
|
1496
|
+
return atom;
|
|
1497
|
+
}
|
|
1498
|
+
const dynamic = found[index];
|
|
1499
|
+
const label = valueLabel({
|
|
1500
|
+
kind: "dynamic",
|
|
1501
|
+
property: atom.property,
|
|
1502
|
+
value: `${atom.chain.join(":")}:${sourceText(mod, dynamic.node)}`
|
|
1503
|
+
});
|
|
1504
|
+
const variable = `--vx-${label.slice(1)}`;
|
|
1505
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
1506
|
+
const scales = /* @__PURE__ */ new Set();
|
|
1507
|
+
const info = propertyByCss(atom.property);
|
|
1508
|
+
vars.push({
|
|
1509
|
+
variable,
|
|
1510
|
+
property: atom.property,
|
|
1511
|
+
value: this.describe(mod, body, info, dynamic, {
|
|
1512
|
+
tokens,
|
|
1513
|
+
scales
|
|
1514
|
+
}, tokenParams)
|
|
1515
|
+
});
|
|
1516
|
+
const labels = atom.chain.map((name) => this.system.conditions.get(name).label);
|
|
1517
|
+
return {
|
|
1518
|
+
...atom,
|
|
1519
|
+
className: makeClassName(labels, atom.key, label),
|
|
1520
|
+
value: `var(${variable})`,
|
|
1521
|
+
variable,
|
|
1522
|
+
tokens: [...tokens.values()],
|
|
1523
|
+
...scales.size > 0 ? { scales: [...scales].sort(compareStrings) } : {}
|
|
1524
|
+
};
|
|
1525
|
+
});
|
|
1526
|
+
atoms.sort((a, b) => compareStrings(a.className, b.className));
|
|
1527
|
+
if (palettes.length === 0) return {
|
|
1528
|
+
classes: classString(atoms),
|
|
1529
|
+
atoms,
|
|
1530
|
+
fn,
|
|
1531
|
+
vars
|
|
1532
|
+
};
|
|
1533
|
+
const all = [...atoms, ...paletteAtoms].sort((a, b) => compareStrings(a.className, b.className));
|
|
1534
|
+
return {
|
|
1535
|
+
classes: classString(atoms),
|
|
1536
|
+
atoms: all,
|
|
1537
|
+
fn,
|
|
1538
|
+
vars,
|
|
1539
|
+
palettes
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* The `colorPalette` values of a function style that runtime values
|
|
1544
|
+
* choose, taken out of `value`: each becomes a class the function adds
|
|
1545
|
+
* (`RuntimePalette`), and every palette it may name an atom. A parameter
|
|
1546
|
+
* typed as a union of names (`tone: "red" | "blue"`) may name those; any
|
|
1547
|
+
* other runtime value, every palette.
|
|
1548
|
+
*/
|
|
1549
|
+
runtimePalettes(mod, value, normalize) {
|
|
1550
|
+
const palettes = [];
|
|
1551
|
+
const paletteAtoms = /* @__PURE__ */ new Map();
|
|
1552
|
+
const names = [...this.system.palettes?.groups.keys() ?? []];
|
|
1553
|
+
/**
|
|
1554
|
+
* `name` as the style would hold it statically, under the condition
|
|
1555
|
+
* blocks and the value per condition's keys it is written in: anything
|
|
1556
|
+
* but a palette's name fails here, with the path a static value has.
|
|
1557
|
+
*/
|
|
1558
|
+
const compileOne = (blocks, conditions, name, any = false) => {
|
|
1559
|
+
let value = name;
|
|
1560
|
+
for (const key of [...conditions].reverse()) value = { [key]: value };
|
|
1561
|
+
let style = { colorPalette: value };
|
|
1562
|
+
for (const key of [...blocks].reverse()) style = { [key]: style };
|
|
1563
|
+
const [compiled] = normalizeStyle(style, normalize);
|
|
1564
|
+
const atom = any ? {
|
|
1565
|
+
...compiled,
|
|
1566
|
+
anyPalette: true
|
|
1567
|
+
} : compiled;
|
|
1568
|
+
keepAtom(paletteAtoms, atom);
|
|
1569
|
+
return atom;
|
|
1570
|
+
};
|
|
1571
|
+
/** Each palette name `v` may hold at runtime, compiled where it is written; its class prefix. */
|
|
1572
|
+
const candidates = (v, blocks, conditions) => {
|
|
1573
|
+
let prefix;
|
|
1574
|
+
const add = (name, any = false) => {
|
|
1575
|
+
const atom = compileOne(blocks, conditions, name, any);
|
|
1576
|
+
prefix ??= atom.className.slice(0, -name.length);
|
|
1577
|
+
};
|
|
1578
|
+
const visit = (x) => {
|
|
1579
|
+
if (isDynamic(x)) {
|
|
1580
|
+
if (x.choice) {
|
|
1581
|
+
visit(x.choice.consequent);
|
|
1582
|
+
visit(x.choice.alternate);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (names.length === 0) compileOne(blocks, conditions, "");
|
|
1586
|
+
const typed = typedPaletteNames(mod.scopes, x.node);
|
|
1587
|
+
for (const name of typed ?? names) add(name, typed === void 0);
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (typeof x === "string") add(x);
|
|
1591
|
+
else if (x !== void 0 && x !== null && x !== false) compileOne(blocks, conditions, x);
|
|
1592
|
+
};
|
|
1593
|
+
visit(v);
|
|
1594
|
+
return prefix;
|
|
1595
|
+
};
|
|
1596
|
+
const take = (node, blocks) => {
|
|
1597
|
+
if (!isPlainObject(node) || isDynamic(node)) return node;
|
|
1598
|
+
const out = {};
|
|
1599
|
+
for (const [key, inner] of Object.entries(node)) {
|
|
1600
|
+
if (key === "colorPalette" && containsDynamic(inner)) {
|
|
1601
|
+
const leaves = (v, conditions) => {
|
|
1602
|
+
if (isPlainObject(v) && !isDynamic(v)) {
|
|
1603
|
+
const kept = {};
|
|
1604
|
+
for (const [k, x] of Object.entries(v)) {
|
|
1605
|
+
const rest = leaves(x, [...conditions, k]);
|
|
1606
|
+
if (rest !== void 0) kept[k] = rest;
|
|
1607
|
+
}
|
|
1608
|
+
return Object.keys(kept).length > 0 ? kept : void 0;
|
|
1609
|
+
}
|
|
1610
|
+
if (!containsDynamic(v)) return v;
|
|
1611
|
+
const prefix = candidates(v, blocks, conditions);
|
|
1612
|
+
if (prefix !== void 0) palettes.push({
|
|
1613
|
+
prefix,
|
|
1614
|
+
value: v
|
|
1615
|
+
});
|
|
1616
|
+
};
|
|
1617
|
+
const rest = leaves(inner, []);
|
|
1618
|
+
if (rest !== void 0) out[key] = rest;
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
out[key] = key.startsWith("_") ? take(inner, [...blocks, key]) : inner;
|
|
1622
|
+
}
|
|
1623
|
+
return out;
|
|
1624
|
+
};
|
|
1625
|
+
return {
|
|
1626
|
+
style: take(value, []),
|
|
1627
|
+
palettes,
|
|
1628
|
+
paletteAtoms: [...paletteAtoms.values()]
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* How a runtime value reaches `info`: the branches of a runtime test are
|
|
1633
|
+
* compiled now, with the token check and S9, so `on ? "accent" : 4` sets
|
|
1634
|
+
* `var(--color-accent)` or `calc(var(--spacing) * 4)`. `named` collects
|
|
1635
|
+
* the tokens the value may name, and the scales a Token-typed parameter
|
|
1636
|
+
* may name any token of: those stay scales, so an app that prints a
|
|
1637
|
+
* library's atom counts its own tokens of them, not the library's.
|
|
1638
|
+
*/
|
|
1639
|
+
describe(mod, root, info, value, named, tokenParams) {
|
|
1640
|
+
const use = (refs) => {
|
|
1641
|
+
for (const ref of refs) named.tokens.set(`${ref.scale}:${ref.path}`, ref);
|
|
1642
|
+
};
|
|
1643
|
+
if (isDynamic(value)) {
|
|
1644
|
+
const { choice } = value;
|
|
1645
|
+
if (!choice) {
|
|
1646
|
+
const { node } = value;
|
|
1647
|
+
const binding = node.type === "Identifier" ? mod.scopes.bindingOf(node) : void 0;
|
|
1648
|
+
const param = binding && tokenParams.get(binding);
|
|
1649
|
+
if (param) {
|
|
1650
|
+
this.checkTokenTarget(mod, root, info, binding.name, param);
|
|
1651
|
+
named.scales.add(param.scale);
|
|
1652
|
+
return {
|
|
1653
|
+
kind: "token",
|
|
1654
|
+
name: binding.name,
|
|
1655
|
+
param
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
const read = /* @__PURE__ */ new Map();
|
|
1659
|
+
if (tokenParams.size > 0) walk(mod, node, (child) => {
|
|
1660
|
+
const ref = child.type === "Identifier" ? mod.scopes.bindingOf(child) : void 0;
|
|
1661
|
+
const token = ref && tokenParams.get(ref);
|
|
1662
|
+
if (token) read.set(ref, token);
|
|
1663
|
+
return true;
|
|
1664
|
+
});
|
|
1665
|
+
for (const token of read.values()) named.scales.add(token.scale);
|
|
1666
|
+
const numbers = this.numberMapping(info);
|
|
1667
|
+
if (node.type !== "TemplateLiteral") use(numbers.tokens);
|
|
1668
|
+
return {
|
|
1669
|
+
kind: "runtime",
|
|
1670
|
+
node,
|
|
1671
|
+
numbers: numbers.mapping,
|
|
1672
|
+
...read.size > 0 ? { tokens: read } : {}
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
return {
|
|
1676
|
+
kind: "choice",
|
|
1677
|
+
test: choice.test,
|
|
1678
|
+
nullish: choice.nullish ?? false,
|
|
1679
|
+
consequent: this.describe(mod, root, info, choice.consequent, named, tokenParams),
|
|
1680
|
+
alternate: this.describe(mod, root, info, choice.alternate, named, tokenParams)
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
if (value === void 0 || value === null || value === false) return {
|
|
1684
|
+
kind: "static",
|
|
1685
|
+
css: void 0
|
|
1686
|
+
};
|
|
1687
|
+
if (isPlainObject(value)) throw this.error(mod, root, "BAD_STYLE", `\`${info.name}\`: a runtime test chooses between values, not between values per condition`, "Put the test inside each condition: { base: on ? a : b, _md: on ? c : d }.");
|
|
1688
|
+
const atom = normalizeStyle({ [info.name]: value }, this.normalizeContext(mod, root)).find((a) => a.property === info.css);
|
|
1689
|
+
if (!atom) throw this.error(mod, root, "BAD_STYLE", `\`${info.name}\` cannot take this value`);
|
|
1690
|
+
use(atom.tokens);
|
|
1691
|
+
return {
|
|
1692
|
+
kind: "static",
|
|
1693
|
+
css: atom.value
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Parameters typed `Token<"scale">`, with `Token` imported from a vicss
|
|
1698
|
+
* import source under any name (`T<"colors">`, `vx.Token<"colors">`),
|
|
1699
|
+
* plain, defaulted or destructured from a type literal.
|
|
1700
|
+
*/
|
|
1701
|
+
tokenParams(mod, fn) {
|
|
1702
|
+
const out = /* @__PURE__ */ new Map();
|
|
1703
|
+
const types = /* @__PURE__ */ new Set();
|
|
1704
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
1705
|
+
for (const node of mod.program.body) {
|
|
1706
|
+
if (node.type !== "ImportDeclaration" || !this.sources.includes(node.source.value)) continue;
|
|
1707
|
+
for (const specifier of node.specifiers) if (specifier.type === "ImportNamespaceSpecifier") namespaces.add(specifier.local.name);
|
|
1708
|
+
else if (specifier.type === "ImportSpecifier" && importedName(specifier) === "Token") types.add(specifier.local.name);
|
|
1709
|
+
}
|
|
1710
|
+
if (types.size === 0 && namespaces.size === 0) return out;
|
|
1711
|
+
const isToken = (name) => name.type === "Identifier" ? types.has(name.name) : name.type === "TSQualifiedName" && name.left.type === "Identifier" && namespaces.has(name.left.name) && name.right.name === "Token";
|
|
1712
|
+
const visit = (pattern, type, optional) => {
|
|
1713
|
+
const annotation = pattern.typeAnnotation?.typeAnnotation ?? type;
|
|
1714
|
+
switch (pattern.type) {
|
|
1715
|
+
case "Identifier": {
|
|
1716
|
+
const param = annotation && this.tokenParam(mod, annotation, isToken, optional || pattern.optional === true);
|
|
1717
|
+
const binding = param && mod.scopes.bindingOf(pattern);
|
|
1718
|
+
if (param && binding) out.set(binding, param);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
case "AssignmentPattern":
|
|
1722
|
+
visit(pattern.left, annotation, optional);
|
|
1723
|
+
return;
|
|
1724
|
+
case "ObjectPattern":
|
|
1725
|
+
if (annotation?.type !== "TSTypeLiteral") return;
|
|
1726
|
+
for (const property of pattern.properties) {
|
|
1727
|
+
if (property.type === "RestElement") continue;
|
|
1728
|
+
const key = patternKey(property);
|
|
1729
|
+
const member = annotation.members.find((m) => m.type === "TSPropertySignature" && !m.computed && (m.key.type === "Identifier" ? m.key.name : String(m.key.value)) === key);
|
|
1730
|
+
if (member) visit(property.value, member.typeAnnotation?.typeAnnotation, member.optional);
|
|
1731
|
+
}
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
for (const param of fn.params) visit(param, void 0, false);
|
|
1736
|
+
return out;
|
|
1737
|
+
}
|
|
1738
|
+
/** `Token<"colors">`, alone or with undefined or null: the scale and how to read it. */
|
|
1739
|
+
tokenParam(mod, annotation, isToken, optional) {
|
|
1740
|
+
let type = annotation;
|
|
1741
|
+
let nullable = optional;
|
|
1742
|
+
if (type.type === "TSUnionType") {
|
|
1743
|
+
const rest = type.types.filter((t) => t.type !== "TSUndefinedKeyword" && t.type !== "TSNullKeyword");
|
|
1744
|
+
if (rest.length !== 1) return void 0;
|
|
1745
|
+
nullable ||= rest.length < type.types.length;
|
|
1746
|
+
type = rest[0];
|
|
1747
|
+
}
|
|
1748
|
+
if (type.type !== "TSTypeReference" || !isToken(type.typeName)) return void 0;
|
|
1749
|
+
const [arg, ...more] = type.typeArguments?.params ?? [];
|
|
1750
|
+
const value = arg?.type === "TSLiteralType" && arg.literal.type === "Literal" && more.length === 0 ? arg.literal.value : void 0;
|
|
1751
|
+
const literal = typeof value === "string" ? value : void 0;
|
|
1752
|
+
const hint = "Name the scale as a literal: (tone: Token<\"colors\">) => ({ background: tone }).";
|
|
1753
|
+
if (literal === void 0) throw this.error(mod, type, "BAD_STYLE", "Token<…> takes one scale name", hint);
|
|
1754
|
+
if (!isScaleName(literal)) throw this.error(mod, type, "UNKNOWN_TOKEN", `\`${literal}\` is not a scale`, didYouMean(closestNames(literal, SCALES)) ?? hint);
|
|
1755
|
+
const scale = literal;
|
|
1756
|
+
if (scaleTokens(this.system, scale).length === 0) throw this.error(mod, type, "NOT_A_TOKEN", `the ${scale} scale has no tokens to name`, scale === "spacing" && this.system.spacingStep !== void 0 ? "The spacing scale is a step: type the parameter as a number, which counts steps." : `Add ${scale} tokens to the config, or type the parameter as a string.`);
|
|
1757
|
+
return {
|
|
1758
|
+
scale,
|
|
1759
|
+
prefix: `--${this.system.cssVarPrefix}${SCALE_NAMESPACES[scale]}-`,
|
|
1760
|
+
nullable
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
/** A token of one scale on a property that draws from another is a mistake, not a value. */
|
|
1764
|
+
checkTokenTarget(mod, root, info, name, param) {
|
|
1765
|
+
const { scale } = info;
|
|
1766
|
+
if (!scale || scale === param.scale || scale === "sizes" && param.scale === "spacing") return;
|
|
1767
|
+
throw new VicssError("BAD_STYLE", `\`${info.name}\` takes ${scale} tokens, and \`${name}\` names ${param.scale} tokens`, {
|
|
1768
|
+
loc: this.locateStyle(mod, root, info.name),
|
|
1769
|
+
hint: `Type the parameter Token<"${scale}">.`
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
/** S9 at runtime for `info`, and the tokens it may name. */
|
|
1773
|
+
numberMapping(info) {
|
|
1774
|
+
const lookup = {};
|
|
1775
|
+
const tokens = [];
|
|
1776
|
+
const named = (scale) => {
|
|
1777
|
+
for (const token of this.system.tokens.get(scale)?.values() ?? []) {
|
|
1778
|
+
if (token.path in lookup || String(Number(token.path)) !== token.path) continue;
|
|
1779
|
+
lookup[token.path] = `var(${token.cssVar})`;
|
|
1780
|
+
tokens.push({
|
|
1781
|
+
scale,
|
|
1782
|
+
path: token.path
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
if (info.scale) named(info.scale);
|
|
1787
|
+
const kind = numberKind(info);
|
|
1788
|
+
let fallback;
|
|
1789
|
+
if (kind === "spacing" || kind === "sizes") {
|
|
1790
|
+
if (this.system.spacingStep !== void 0) {
|
|
1791
|
+
fallback = "calc";
|
|
1792
|
+
tokens.push({
|
|
1793
|
+
scale: "spacing",
|
|
1794
|
+
path: SPACING_STEP
|
|
1795
|
+
});
|
|
1796
|
+
} else {
|
|
1797
|
+
if (kind === "sizes") named("spacing");
|
|
1798
|
+
fallback = "unset";
|
|
1799
|
+
}
|
|
1800
|
+
} else fallback = kind === "length" ? "px" : kind === "time" ? "ms" : kind === "angle" ? "deg" : "plain";
|
|
1801
|
+
return {
|
|
1802
|
+
mapping: {
|
|
1803
|
+
lookup,
|
|
1804
|
+
fallback
|
|
1805
|
+
},
|
|
1806
|
+
tokens
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
/** Runtime values only reach properties that keep their value whole. */
|
|
1810
|
+
checkRuntimeTargets(mod, root, style, path) {
|
|
1811
|
+
if (!isPlainObject(style)) return;
|
|
1812
|
+
for (const [key, value] of Object.entries(style)) {
|
|
1813
|
+
if (!hasPlaceholder(value)) continue;
|
|
1814
|
+
const here = path ? `${path}.${key}` : key;
|
|
1815
|
+
if (key.startsWith("_")) {
|
|
1816
|
+
this.checkRuntimeTargets(mod, root, value, here);
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
if (this.system.utilities.has(key)) continue;
|
|
1820
|
+
if (key === "textStyle" || getProperty(key)?.expand) throw new VicssError("BAD_STYLE", `${here}: \`${key}\` cannot take a runtime value`, {
|
|
1821
|
+
loc: this.locateStyle(mod, root, here),
|
|
1822
|
+
hint: key === "textStyle" ? "Text styles are chosen at build time." : "It is split into longhands at build time; set the longhands instead."
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
/** A compiled style is a class string: it composes through sx(), never inside a style object. */
|
|
1827
|
+
checkComposition(mod, arg, value, ctx) {
|
|
1828
|
+
if (value instanceof CompiledStyle) throw this.error(mod, arg, "BAD_STYLE", "this style is already compiled; css() takes a style object", "Combine compiled styles with sx(): sx(base, css({ … })).");
|
|
1829
|
+
const spreads = [];
|
|
1830
|
+
collectSpreads(arg, spreads);
|
|
1831
|
+
for (const spread of spreads) {
|
|
1832
|
+
const result = attempt(() => evaluate(spread.argument, ctx))?.result;
|
|
1833
|
+
if (result instanceof MarkerValue) throw this.error(mod, spread, "BAD_STYLE", `\`${excerpt(mod.code, spread)}\` spreads a marker: a marker is applied with sx(), and its states are condition keys`, "sx(row, css({ opacity: { base: 0, [row.hover]: 1 } })).");
|
|
1834
|
+
if (result instanceof CompiledStyle || typeof result === "string") throw this.error(mod, spread, "BAD_STYLE", `\`${excerpt(mod.code, spread)}\` spreads a compiled style: compose compiled styles with sx(), not with spread`, "sx(base, css({ … })) merges them, last value wins.");
|
|
1835
|
+
}
|
|
1836
|
+
if (containsCompiled(value)) throw this.error(mod, arg, "BAD_STYLE", "a compiled style is used inside a style object: compose compiled styles with sx(), not with spread", "sx(base, css({ … })) merges them, last value wins.");
|
|
1837
|
+
}
|
|
1838
|
+
/** Whether styles written in `mod` may name private tokens: `privateAccess` lists it. */
|
|
1839
|
+
privateAccess(mod) {
|
|
1840
|
+
let access = this.access.get(mod.id);
|
|
1841
|
+
if (access === void 0) {
|
|
1842
|
+
access = hasPrivateAccess(this.system, this.options.root ?? process.cwd(), mod.id);
|
|
1843
|
+
this.access.set(mod.id, access);
|
|
1844
|
+
}
|
|
1845
|
+
return access;
|
|
1846
|
+
}
|
|
1847
|
+
normalizeContext(mod, root) {
|
|
1848
|
+
const { options } = this;
|
|
1849
|
+
return {
|
|
1850
|
+
system: this.system,
|
|
1851
|
+
privateAccess: this.privateAccess(mod),
|
|
1852
|
+
locate: (path) => this.locateStyle(mod, root, path),
|
|
1853
|
+
markerLabels: this.markerLabels,
|
|
1854
|
+
...options.warn && mod === this.root ? { warn: (message, warning) => options.warn?.(message, warning) } : {},
|
|
1855
|
+
...mod === this.root && { arbitrary: ({ value, path }) => this.arbitrary.add(`${root.start}:${path}:${value}`) }
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
/** Where a style path (`padding._md`) was written: its property, or the closest enclosing one. */
|
|
1859
|
+
locateStyle(mod, root, path) {
|
|
1860
|
+
let at = root;
|
|
1861
|
+
let value = root;
|
|
1862
|
+
for (const key of path === "" ? [] : path.split(".")) {
|
|
1863
|
+
const property = findProperty(mod, value, key);
|
|
1864
|
+
if (!property) break;
|
|
1865
|
+
at = property;
|
|
1866
|
+
value = property.value;
|
|
1867
|
+
}
|
|
1868
|
+
return this.loc(mod, at);
|
|
1869
|
+
}
|
|
1870
|
+
tokenOf(mod, callee, args, evaluateNode) {
|
|
1871
|
+
const [arg] = args;
|
|
1872
|
+
if (args.length !== 1 || !arg || arg.type === "SpreadElement") throw this.error(mod, callee, "BAD_CALL", "token() takes one token path", TOKEN_HINT);
|
|
1873
|
+
const path = evaluateNode(arg);
|
|
1874
|
+
if (typeof path !== "string") throw this.error(mod, arg, "BAD_CALL", "token() takes a token path string", TOKEN_HINT);
|
|
1875
|
+
const dot = path.indexOf(".");
|
|
1876
|
+
const scale = dot === -1 ? path : path.slice(0, dot);
|
|
1877
|
+
if (dot === -1 || !isScaleName(scale)) throw this.error(mod, arg, "UNKNOWN_TOKEN", `\`${path}\` does not start with a scale`, didYouMean(closestNames(scale, SCALES).map((s) => `${s}.…`)) ?? TOKEN_HINT);
|
|
1878
|
+
const rest = path.slice(dot + 1);
|
|
1879
|
+
const token = lookupToken(this.system, scale, rest);
|
|
1880
|
+
if (!token) throw this.error(mod, arg, "UNKNOWN_TOKEN", `\`${path}\` names no token`, didYouMean(suggestTokens(this.system, scale, rest).map((p) => `${scale}.${p}`)));
|
|
1881
|
+
if (token.private && !this.privateAccess(mod)) {
|
|
1882
|
+
const e = privateTokenError(this.system, token);
|
|
1883
|
+
throw this.error(mod, arg, e.code, e.message, e.hint);
|
|
1884
|
+
}
|
|
1885
|
+
return token;
|
|
1886
|
+
}
|
|
1887
|
+
resolveImport(mod, binding) {
|
|
1888
|
+
const source = binding.source;
|
|
1889
|
+
const name = binding.imported;
|
|
1890
|
+
if (this.sources.includes(source) && (name === "*" || RUNTIME_EXPORTS.has(name))) throw new Error("it is part of the vicss runtime, not a value");
|
|
1891
|
+
if (!this.options.loadModule) throw new Error("the compiler was given no module loader");
|
|
1892
|
+
return this.exportValue(this.target(mod.id, source), name);
|
|
1893
|
+
}
|
|
1894
|
+
target(importer, source) {
|
|
1895
|
+
const key = requestKey(importer, source);
|
|
1896
|
+
const entry = this.requests.get(key);
|
|
1897
|
+
if (entry === void 0) {
|
|
1898
|
+
if (this.probing > 0 && this.skipped.has(key)) throw new Error(notSearched(source));
|
|
1899
|
+
throw new PendingImport([[importer, source]], this.probing > 0);
|
|
1900
|
+
}
|
|
1901
|
+
if (typeof entry === "string") throw new Error(entry);
|
|
1902
|
+
return entry;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* The modules `mod` star-exports, loaded or failed. A barrel of a hundred
|
|
1906
|
+
* `export *` loads its targets together rather than one per pass.
|
|
1907
|
+
*/
|
|
1908
|
+
stars(mod) {
|
|
1909
|
+
const probing = this.probing > 0;
|
|
1910
|
+
const missing = mod.scopes.starExports.filter((star) => {
|
|
1911
|
+
const key = requestKey(mod.id, star);
|
|
1912
|
+
return !this.requests.has(key) && !(probing && this.skipped.has(key));
|
|
1913
|
+
});
|
|
1914
|
+
if (missing.length > 0) throw new PendingImport(missing.map((star) => [mod.id, star]), probing);
|
|
1915
|
+
return mod.scopes.starExports.map((star) => this.requests.get(requestKey(mod.id, star)) ?? notSearched(star));
|
|
1916
|
+
}
|
|
1917
|
+
exportValue(mod, name) {
|
|
1918
|
+
const key = requestKey(mod.id, name);
|
|
1919
|
+
if (this.resolving.has(key)) throw new Error(`\`${name}\` of ${mod.id} depends on itself through an import cycle`);
|
|
1920
|
+
this.resolving.add(key);
|
|
1921
|
+
try {
|
|
1922
|
+
if (name === "*") return this.namespace(mod);
|
|
1923
|
+
const entry = mod.scopes.exports.get(name);
|
|
1924
|
+
if (entry?.local !== void 0) {
|
|
1925
|
+
const binding = mod.scopes.moduleBindings.get(entry.local);
|
|
1926
|
+
if (!binding) throw new Error(`${mod.id} exports \`${name}\` without declaring it`);
|
|
1927
|
+
return evaluateBinding(binding, mod.ctx);
|
|
1928
|
+
}
|
|
1929
|
+
if (entry?.source !== void 0) return this.exportValue(this.target(mod.id, entry.source), entry.imported);
|
|
1930
|
+
const unread = [];
|
|
1931
|
+
for (const inner of this.stars(mod)) if (typeof inner === "string") unread.push(inner);
|
|
1932
|
+
else if (this.exportsName(inner, name, /* @__PURE__ */ new Set(), unread)) return this.exportValue(inner, name);
|
|
1933
|
+
throw new Error(`${mod.id} has no export \`${name}\`${unread.length > 0 ? `; ${unread.join("; ")}` : ""}`);
|
|
1934
|
+
} finally {
|
|
1935
|
+
this.resolving.delete(key);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
exportsName(mod, name, seen, unread) {
|
|
1939
|
+
if (seen.has(mod)) return false;
|
|
1940
|
+
seen.add(mod);
|
|
1941
|
+
if (mod.scopes.exports.has(name)) return true;
|
|
1942
|
+
return this.stars(mod).some((inner) => {
|
|
1943
|
+
if (typeof inner !== "string") return this.exportsName(inner, name, seen, unread);
|
|
1944
|
+
unread.push(inner);
|
|
1945
|
+
return false;
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
/** Every name `mod` exports, its `export *` targets' included (never their default). */
|
|
1949
|
+
exportNames(mod, seen = /* @__PURE__ */ new Set()) {
|
|
1950
|
+
const names = new Set(mod.scopes.exports.keys());
|
|
1951
|
+
if (seen.has(mod)) return names;
|
|
1952
|
+
seen.add(mod);
|
|
1953
|
+
for (const inner of this.stars(mod)) {
|
|
1954
|
+
if (typeof inner === "string") continue;
|
|
1955
|
+
for (const name of this.exportNames(inner, seen)) if (name !== "default") names.add(name);
|
|
1956
|
+
}
|
|
1957
|
+
return names;
|
|
1958
|
+
}
|
|
1959
|
+
/** `import * as ns`: exports are evaluated when read, so non-static siblings do not matter. */
|
|
1960
|
+
namespace(mod) {
|
|
1961
|
+
const { exports } = mod.scopes;
|
|
1962
|
+
const has = (key) => typeof key === "string" && (exports.has(key) || key !== "default" && this.exportsName(mod, key, /* @__PURE__ */ new Set(), []));
|
|
1963
|
+
return new Proxy({}, {
|
|
1964
|
+
ownKeys: () => [...this.exportNames(mod)],
|
|
1965
|
+
has: (_, key) => has(key),
|
|
1966
|
+
get: (_, key) => has(key) ? this.exportValue(mod, key) : void 0,
|
|
1967
|
+
getOwnPropertyDescriptor: (_, key) => has(key) ? {
|
|
1968
|
+
value: this.exportValue(mod, key),
|
|
1969
|
+
enumerable: true,
|
|
1970
|
+
configurable: true
|
|
1971
|
+
} : void 0
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
loc(mod, node) {
|
|
1975
|
+
return {
|
|
1976
|
+
file: mod.id,
|
|
1977
|
+
...mod.locate(node.start)
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
error(mod, node, code, message, hint) {
|
|
1981
|
+
return new VicssError(code, message, {
|
|
1982
|
+
loc: this.loc(mod, node),
|
|
1983
|
+
hint
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
/** A class an app names: what `css(style, { name })` takes. */
|
|
1988
|
+
const CLASS_NAME = /^-?[A-Za-z_][\w-]*$/;
|
|
1989
|
+
const CSS_HINT = "Write css({ padding: 4 }), or css((value) => ({ width: value })) for runtime values.";
|
|
1990
|
+
const TOKEN_HINT = "Write token(\"colors.accent\"): a scale, then the token path.";
|
|
1991
|
+
const THEME_HINT = "Write theme({ colors: { accent: \"{colors.red.600}\" } }).";
|
|
1992
|
+
const KEYFRAMES_HINT = "Write keyframes({ from: { opacity: 0 }, to: { opacity: 1 } }).";
|
|
1993
|
+
const RECIPE_HINT = "Write recipe({ base: { … }, variants: { size: { sm: { … } } } }).";
|
|
1994
|
+
/** The first position at or after `pos` that is not whitespace or a comment. */
|
|
1995
|
+
function skipTrivia(code, pos) {
|
|
1996
|
+
for (let i = pos; i < code.length;) if (/\s/.test(code[i])) i++;
|
|
1997
|
+
else if (code.startsWith("//", i)) i = code.includes("\n", i) ? code.indexOf("\n", i) : code.length;
|
|
1998
|
+
else if (code.startsWith("/*", i)) i = code.indexOf("*/", i + 2) + 2;
|
|
1999
|
+
else return i;
|
|
2000
|
+
return code.length;
|
|
2001
|
+
}
|
|
2002
|
+
/** After a default import specifier: the `{` that opens the named ones. */
|
|
2003
|
+
function openBrace(code, pos) {
|
|
2004
|
+
const comma = skipTrivia(code, pos);
|
|
2005
|
+
return skipTrivia(code, code[comma] === "," ? comma + 1 : comma);
|
|
2006
|
+
}
|
|
2007
|
+
function unchanged(code) {
|
|
2008
|
+
return {
|
|
2009
|
+
code,
|
|
2010
|
+
map: null,
|
|
2011
|
+
atoms: [],
|
|
2012
|
+
dependencies: [],
|
|
2013
|
+
tokens: [],
|
|
2014
|
+
rules: [],
|
|
2015
|
+
arbitrary: 0,
|
|
2016
|
+
changed: false
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function newPass(code, keepLines) {
|
|
2020
|
+
return {
|
|
2021
|
+
edits: new SourceEdits(code, { keepLines }),
|
|
2022
|
+
spans: [],
|
|
2023
|
+
tokens: /* @__PURE__ */ new Map(),
|
|
2024
|
+
folded: /* @__PURE__ */ new Set(),
|
|
2025
|
+
pending: /* @__PURE__ */ new Map(),
|
|
2026
|
+
probes: /* @__PURE__ */ new Set(),
|
|
2027
|
+
helpers: /* @__PURE__ */ new Map(),
|
|
2028
|
+
hoisted: 0,
|
|
2029
|
+
devSx: false,
|
|
2030
|
+
errors: []
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function notSearched(source) {
|
|
2034
|
+
return `"${source}" is a package, which is not searched for recipes`;
|
|
2035
|
+
}
|
|
2036
|
+
function requestKey(a, b) {
|
|
2037
|
+
return `${a}\0${b}`;
|
|
2038
|
+
}
|
|
2039
|
+
const isJson = (id) => /\.json$/i.test(id.replace(/[?#].*$/, ""));
|
|
2040
|
+
const RESERVED = new Set("await break case catch class const continue debugger default delete do else enum export extends false finally for function if implements import in instanceof interface let new null package private protected public return static super switch this throw true try typeof var void while with yield".split(" "));
|
|
2041
|
+
/**
|
|
2042
|
+
* A JSON module as the JS module bundlers make of it: the value as the
|
|
2043
|
+
* default export, and its top-level keys that are identifiers as named ones.
|
|
2044
|
+
*/
|
|
2045
|
+
function jsonModule(id, code) {
|
|
2046
|
+
let value;
|
|
2047
|
+
try {
|
|
2048
|
+
value = JSON.parse(code);
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
throw new VicssError("PARSE_ERROR", error instanceof Error ? error.message : String(error), { loc: {
|
|
2051
|
+
file: id,
|
|
2052
|
+
line: 1,
|
|
2053
|
+
column: 0
|
|
2054
|
+
} });
|
|
2055
|
+
}
|
|
2056
|
+
let out = `export default ${JSON.stringify(value)};\n`;
|
|
2057
|
+
if (isPlainObject(value)) {
|
|
2058
|
+
for (const [key, item] of Object.entries(value)) if (/^[A-Za-z_$][\w$]*$/.test(key) && !RESERVED.has(key)) out += `export const ${key} = ${JSON.stringify(item)};\n`;
|
|
2059
|
+
}
|
|
2060
|
+
return out;
|
|
2061
|
+
}
|
|
2062
|
+
function langOf(id) {
|
|
2063
|
+
const ext = /\.([a-z]+)$/i.exec(id.replace(/[?#].*$/, ""))?.[1]?.toLowerCase();
|
|
2064
|
+
return ext !== void 0 && LANGS[ext] || "tsx";
|
|
2065
|
+
}
|
|
2066
|
+
function parse(parseSync, id, code, lang, functionBody) {
|
|
2067
|
+
const { program, errors } = parseSync(id, code, {
|
|
2068
|
+
lang,
|
|
2069
|
+
sourceType: "module"
|
|
2070
|
+
});
|
|
2071
|
+
const returns = functionBody && errors.length > 0 ? topLevelReturns(program) : void 0;
|
|
2072
|
+
const error = errors.find((e) => e.severity === "Error" && !(returns && e.labels.length === 1 && returns.has(e.labels[0].start)));
|
|
2073
|
+
if (error) {
|
|
2074
|
+
const start = error.labels[0]?.start;
|
|
2075
|
+
throw new VicssError("PARSE_ERROR", error.message, {
|
|
2076
|
+
loc: start === void 0 ? void 0 : {
|
|
2077
|
+
file: id,
|
|
2078
|
+
...createLocator(code)(start)
|
|
2079
|
+
},
|
|
2080
|
+
hint: error.helpMessage ?? void 0
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
return program;
|
|
2084
|
+
}
|
|
2085
|
+
/** Where the `return` statements outside any function start. */
|
|
2086
|
+
function topLevelReturns(program) {
|
|
2087
|
+
const out = /* @__PURE__ */ new Set();
|
|
2088
|
+
const visit = (value) => {
|
|
2089
|
+
if (Array.isArray(value)) return value.forEach(visit);
|
|
2090
|
+
if (!isNode(value)) return;
|
|
2091
|
+
if (FUNCTIONS.has(value.type)) return;
|
|
2092
|
+
if (value.type === "ReturnStatement") out.add(value.start);
|
|
2093
|
+
for (const key in value) if (key !== "type") visit(value[key]);
|
|
2094
|
+
};
|
|
2095
|
+
visit(program.body);
|
|
2096
|
+
return out;
|
|
2097
|
+
}
|
|
2098
|
+
const FUNCTIONS = /* @__PURE__ */ new Set([
|
|
2099
|
+
"FunctionDeclaration",
|
|
2100
|
+
"FunctionExpression",
|
|
2101
|
+
"ArrowFunctionExpression",
|
|
2102
|
+
"ClassDeclaration",
|
|
2103
|
+
"ClassExpression"
|
|
2104
|
+
]);
|
|
2105
|
+
function runtimeBindings(program, scopes, sources) {
|
|
2106
|
+
const out = /* @__PURE__ */ new Map();
|
|
2107
|
+
for (const node of program.body) {
|
|
2108
|
+
if (node.type !== "ImportDeclaration" || node.importKind === "type") continue;
|
|
2109
|
+
if (!sources.includes(node.source.value)) continue;
|
|
2110
|
+
for (const specifier of node.specifiers) {
|
|
2111
|
+
const binding = scopes.bindingOf(specifier.local);
|
|
2112
|
+
const name = binding?.imported;
|
|
2113
|
+
if (binding && name && (name === "*" || RUNTIME_EXPORTS.has(name))) out.set(binding, name);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
return out;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* The runtime function a callee names: `css`, an alias of it, or `ns.css` —
|
|
2120
|
+
* shadowing respected. In a Vue component's output, a template reaches the
|
|
2121
|
+
* script's `css` as `_unref(css)`, `$setup.css` or `_ctx.css` too.
|
|
2122
|
+
*/
|
|
2123
|
+
function runtimeName(mod, callee) {
|
|
2124
|
+
const target = unwrap(callee);
|
|
2125
|
+
if (target.type === "Identifier") {
|
|
2126
|
+
const binding = mod.scopes.bindingOf(target);
|
|
2127
|
+
const name = binding && mod.runtime.get(binding);
|
|
2128
|
+
return name === "*" ? void 0 : name;
|
|
2129
|
+
}
|
|
2130
|
+
if (target.type === "CallExpression") {
|
|
2131
|
+
const [arg] = target.arguments;
|
|
2132
|
+
return arg && isUnref(mod, target.callee, target.arguments) ? runtimeName(mod, arg) : void 0;
|
|
2133
|
+
}
|
|
2134
|
+
if (target.type !== "MemberExpression") return void 0;
|
|
2135
|
+
const key = memberKey(target);
|
|
2136
|
+
if (key === void 0) return void 0;
|
|
2137
|
+
const setup = vueBinding(mod, target);
|
|
2138
|
+
if (setup) {
|
|
2139
|
+
const imported = mod.runtime.get(setup);
|
|
2140
|
+
return imported === "*" ? void 0 : imported;
|
|
2141
|
+
}
|
|
2142
|
+
return namespaceOf(mod, target.object) && RUNTIME_EXPORTS.has(key) ? key : void 0;
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* The namespace import of a runtime source that `node` reads: `ns`, or in a
|
|
2146
|
+
* Vue component's output `_unref(ns)`, `$setup.ns` and `_ctx.ns`.
|
|
2147
|
+
*/
|
|
2148
|
+
function namespaceOf(mod, node) {
|
|
2149
|
+
const target = unwrap(node);
|
|
2150
|
+
let binding;
|
|
2151
|
+
if (target.type === "Identifier") binding = mod.scopes.bindingOf(target);
|
|
2152
|
+
else if (target.type === "CallExpression") {
|
|
2153
|
+
const [arg] = target.arguments;
|
|
2154
|
+
return arg && isUnref(mod, target.callee, target.arguments) ? namespaceOf(mod, arg) : void 0;
|
|
2155
|
+
} else if (target.type === "MemberExpression") binding = vueBinding(mod, target);
|
|
2156
|
+
return binding && mod.runtime.get(binding) === "*" ? binding : void 0;
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* The script binding a Vue render function reads as `$setup.x` or `_ctx.x`:
|
|
2160
|
+
* those hold the script's bindings by name.
|
|
2161
|
+
*/
|
|
2162
|
+
function vueBinding(mod, node) {
|
|
2163
|
+
if (!mod.vue) return void 0;
|
|
2164
|
+
const object = unwrap(node.object);
|
|
2165
|
+
const key = memberKey(node);
|
|
2166
|
+
if (object.type !== "Identifier" || key === void 0) return void 0;
|
|
2167
|
+
const { name } = object;
|
|
2168
|
+
if (name !== "$setup" && name !== "_ctx") return void 0;
|
|
2169
|
+
const own = mod.scopes.bindingOf(object);
|
|
2170
|
+
if (own && own.kind !== "param") return void 0;
|
|
2171
|
+
return mod.scopes.moduleBindings.get(key);
|
|
2172
|
+
}
|
|
2173
|
+
/** `ns.name` or `ns["name"]`: the name. */
|
|
2174
|
+
function memberKey(node) {
|
|
2175
|
+
if (!node.computed) return node.property.name;
|
|
2176
|
+
const property = node.property;
|
|
2177
|
+
return property.type === "Literal" && typeof property.value === "string" ? property.value : void 0;
|
|
2178
|
+
}
|
|
2179
|
+
/** A call of Vue's `unref` (`_unref(x)` in compiled templates): x itself for a constant. */
|
|
2180
|
+
function isUnref(mod, callee, args) {
|
|
2181
|
+
const target = unwrap(callee);
|
|
2182
|
+
if (target.type !== "Identifier" || args.length !== 1 || args[0].type === "SpreadElement") return false;
|
|
2183
|
+
const binding = mod.scopes.bindingOf(target);
|
|
2184
|
+
return binding?.kind === "import" && binding.source === "vue" && binding.imported === "unref";
|
|
2185
|
+
}
|
|
2186
|
+
function unwrap(node) {
|
|
2187
|
+
let inner = node;
|
|
2188
|
+
while (WRAPPERS.has(inner.type)) inner = inner.expression;
|
|
2189
|
+
return inner;
|
|
2190
|
+
}
|
|
2191
|
+
function isNode(value) {
|
|
2192
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
2193
|
+
}
|
|
2194
|
+
const CHILD_KEYS = {
|
|
2195
|
+
js: /* @__PURE__ */ new Map(),
|
|
2196
|
+
ts: /* @__PURE__ */ new Map()
|
|
2197
|
+
};
|
|
2198
|
+
function childKeys(node, cache) {
|
|
2199
|
+
let keys = cache.get(node.type);
|
|
2200
|
+
if (!keys) {
|
|
2201
|
+
const record = node;
|
|
2202
|
+
keys = Object.keys(record).filter((key) => {
|
|
2203
|
+
if (SKIPPED_KEYS.has(key)) return false;
|
|
2204
|
+
const value = record[key];
|
|
2205
|
+
return value === null || value === void 0 || typeof value === "object";
|
|
2206
|
+
});
|
|
2207
|
+
cache.set(node.type, keys);
|
|
2208
|
+
}
|
|
2209
|
+
return keys;
|
|
2210
|
+
}
|
|
2211
|
+
/** Depth-first, `node` first; children are skipped when `enter` returns false. */
|
|
2212
|
+
function walk(mod, node, enter) {
|
|
2213
|
+
const cache = CHILD_KEYS[mod.lang === "ts" || mod.lang === "tsx" ? "ts" : "js"];
|
|
2214
|
+
const visit = (current) => {
|
|
2215
|
+
if (!enter(current)) return;
|
|
2216
|
+
const record = current;
|
|
2217
|
+
for (const key of childKeys(current, cache)) {
|
|
2218
|
+
const value = record[key];
|
|
2219
|
+
if (Array.isArray(value)) {
|
|
2220
|
+
for (const item of value) if (isNode(item)) visit(item);
|
|
2221
|
+
} else if (isNode(value)) visit(value);
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
visit(node);
|
|
2225
|
+
}
|
|
2226
|
+
/** Identifiers a parameter pattern declares. */
|
|
2227
|
+
function bindingIds(pattern, out = []) {
|
|
2228
|
+
switch (pattern.type) {
|
|
2229
|
+
case "Identifier":
|
|
2230
|
+
out.push(pattern);
|
|
2231
|
+
break;
|
|
2232
|
+
case "AssignmentPattern":
|
|
2233
|
+
bindingIds(pattern.left, out);
|
|
2234
|
+
break;
|
|
2235
|
+
case "RestElement":
|
|
2236
|
+
bindingIds(pattern.argument, out);
|
|
2237
|
+
break;
|
|
2238
|
+
case "ObjectPattern":
|
|
2239
|
+
for (const p of pattern.properties) bindingIds(p.type === "RestElement" ? p : p.value, out);
|
|
2240
|
+
break;
|
|
2241
|
+
case "ArrayPattern":
|
|
2242
|
+
for (const element of pattern.elements) if (element) bindingIds(element, out);
|
|
2243
|
+
break;
|
|
2244
|
+
case "TSParameterProperty": bindingIds(pattern.parameter, out);
|
|
2245
|
+
}
|
|
2246
|
+
return out;
|
|
2247
|
+
}
|
|
2248
|
+
/** Spreads inside a style literal, where a compiled style would be flattened into keys. */
|
|
2249
|
+
function collectSpreads(node, out) {
|
|
2250
|
+
const inner = unwrap(node);
|
|
2251
|
+
if (inner.type === "ObjectExpression") for (const property of inner.properties) if (property.type === "SpreadElement") out.push(property);
|
|
2252
|
+
else collectSpreads(property.value, out);
|
|
2253
|
+
else if (inner.type === "ConditionalExpression") {
|
|
2254
|
+
collectSpreads(inner.consequent, out);
|
|
2255
|
+
collectSpreads(inner.alternate, out);
|
|
2256
|
+
} else if (inner.type === "LogicalExpression") collectSpreads(inner.right, out);
|
|
2257
|
+
}
|
|
2258
|
+
function containsCompiled(value) {
|
|
2259
|
+
if (value instanceof CompiledStyle) return true;
|
|
2260
|
+
if (Array.isArray(value)) return value.some(containsCompiled);
|
|
2261
|
+
if (!isPlainObject(value)) return false;
|
|
2262
|
+
if (value.__vxSpecial === "style") return true;
|
|
2263
|
+
if (Object.hasOwn(value, "__vxSpecial")) return false;
|
|
2264
|
+
return Object.values(value).some(containsCompiled);
|
|
2265
|
+
}
|
|
2266
|
+
/** The class string a value adds to `sx()`: falsy values add nothing. Undefined when unknown. */
|
|
2267
|
+
function classesOf(value) {
|
|
2268
|
+
if (value === false || value === null || value === void 0 || value === 0 || value === "") return "";
|
|
2269
|
+
if (typeof value === "string") return splitClasses(value).map((cls) => "!" + cls).join(" ");
|
|
2270
|
+
if (value instanceof CompiledStyle) return value.classes;
|
|
2271
|
+
if (value instanceof MarkerValue) return value.className;
|
|
2272
|
+
if (Array.isArray(value)) {
|
|
2273
|
+
const parts = value.map(classesOf);
|
|
2274
|
+
return parts.every((part) => part !== void 0) ? mergeClassNames(parts).join(" ") : void 0;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
function classString(atoms) {
|
|
2278
|
+
return atoms.map((atom) => atom.className).join(" ");
|
|
2279
|
+
}
|
|
2280
|
+
/** Runs `fn`; a VicssError (other than a pending import) means "not known". */
|
|
2281
|
+
function attempt(fn) {
|
|
2282
|
+
try {
|
|
2283
|
+
return { result: fn() };
|
|
2284
|
+
} catch (error) {
|
|
2285
|
+
if (error instanceof PendingImport || !(error instanceof VicssError)) throw error;
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
function withPlaceholders(value, found) {
|
|
2290
|
+
if (isDynamic(value)) {
|
|
2291
|
+
found.push(value);
|
|
2292
|
+
return `${PLACEHOLDER}${found.length - 1}`;
|
|
2293
|
+
}
|
|
2294
|
+
if (Array.isArray(value)) return value.map((item) => withPlaceholders(item, found));
|
|
2295
|
+
if (!isPlainObject(value) || Object.hasOwn(value, "__vxSpecial")) return value;
|
|
2296
|
+
const out = {};
|
|
2297
|
+
for (const [key, item] of Object.entries(value)) out[key] = withPlaceholders(item, found);
|
|
2298
|
+
return out;
|
|
2299
|
+
}
|
|
2300
|
+
/** Whether a runtime value sits in `value`, at any depth. */
|
|
2301
|
+
function containsDynamic(value) {
|
|
2302
|
+
if (isDynamic(value)) return true;
|
|
2303
|
+
if (Array.isArray(value)) return value.some(containsDynamic);
|
|
2304
|
+
return isPlainObject(value) && Object.values(value).some(containsDynamic);
|
|
2305
|
+
}
|
|
2306
|
+
function hasPlaceholder(value) {
|
|
2307
|
+
if (typeof value === "string") return value.startsWith(PLACEHOLDER);
|
|
2308
|
+
if (Array.isArray(value)) return value.some(hasPlaceholder);
|
|
2309
|
+
return isPlainObject(value) && Object.values(value).some(hasPlaceholder);
|
|
2310
|
+
}
|
|
2311
|
+
function placeholderIndex(value) {
|
|
2312
|
+
if (!value.startsWith(PLACEHOLDER)) return void 0;
|
|
2313
|
+
const index = value.slice(3);
|
|
2314
|
+
return /^\d+$/.test(index) ? Number(index) : void 0;
|
|
2315
|
+
}
|
|
2316
|
+
let byCss;
|
|
2317
|
+
function propertyByCss(css) {
|
|
2318
|
+
byCss ??= new Map([...PROPERTIES.values()].map((info) => [info.css, info]));
|
|
2319
|
+
return byCss.get(css);
|
|
2320
|
+
}
|
|
2321
|
+
/** The text a runtime value hashes by: a parameter by its name, without its type annotation. */
|
|
2322
|
+
function sourceText(mod, node) {
|
|
2323
|
+
return node.type === "Identifier" ? node.name : mod.code.slice(node.start, node.end);
|
|
2324
|
+
}
|
|
2325
|
+
/** The property written last under `key` in the object `node` is, or is bound to in this module. */
|
|
2326
|
+
function findProperty(mod, node, key) {
|
|
2327
|
+
let target = unwrap(node);
|
|
2328
|
+
if (target.type === "Identifier") {
|
|
2329
|
+
const binding = mod.scopes.bindingOf(target);
|
|
2330
|
+
if (binding?.kind !== "const" || !binding.init || binding.path) return void 0;
|
|
2331
|
+
target = unwrap(binding.init);
|
|
2332
|
+
}
|
|
2333
|
+
if (target.type !== "ObjectExpression") return void 0;
|
|
2334
|
+
for (let i = target.properties.length - 1; i >= 0; i--) {
|
|
2335
|
+
const property = target.properties[i];
|
|
2336
|
+
if (property.type === "Property" && staticKey(property) === key) return property;
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
function staticKey(property) {
|
|
2340
|
+
const { key } = property;
|
|
2341
|
+
if (key.type === "Identifier" && !property.computed) return key.name;
|
|
2342
|
+
if (key.type === "Literal" && (typeof key.value === "string" || typeof key.value === "number")) return String(key.value);
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* A token-typed parameter's custom property: a string transform, so no
|
|
2346
|
+
* token table ships to the browser (PLAN §4.6). A value that names no
|
|
2347
|
+
* token is written as it is (D33), which `var()`'s fallback does with no
|
|
2348
|
+
* table either: no token declares the property it reads. A value that
|
|
2349
|
+
* cannot be part of a property's name is the value itself.
|
|
2350
|
+
*/
|
|
2351
|
+
function tokenExpression(name, param) {
|
|
2352
|
+
const read = `((v) => /^[\\w.-]+$/.test(v) ? "var(${param.prefix}" + v.replace(/\\./g, "-") + ", " + v + ")" : v)(String(${name}))`;
|
|
2353
|
+
return param.nullable ? `(${name} == null ? undefined : ${read})` : read;
|
|
2354
|
+
}
|
|
2355
|
+
/** The name an import specifier takes from its module (`Token` in `{ Token as T }`). */
|
|
2356
|
+
function importedName(specifier) {
|
|
2357
|
+
const { imported } = specifier;
|
|
2358
|
+
return imported.type === "Identifier" ? imported.name : String(imported.value);
|
|
2359
|
+
}
|
|
2360
|
+
/** The key a destructuring property reads, when it is written out. */
|
|
2361
|
+
function patternKey(property) {
|
|
2362
|
+
const { key } = property;
|
|
2363
|
+
if (key.type === "Identifier" && !property.computed) return key.name;
|
|
2364
|
+
if (key.type === "Literal" && (typeof key.value === "string" || typeof key.value === "number")) return String(key.value);
|
|
2365
|
+
}
|
|
2366
|
+
function excerpt(code, node) {
|
|
2367
|
+
const text = code.slice(node.start, node.end).replace(/\s+/g, " ");
|
|
2368
|
+
return text.length > 60 ? `${text.slice(0, 59)}…` : text;
|
|
2369
|
+
}
|
|
2370
|
+
/** An editor over one range of the source, for code copied into generated text. */
|
|
2371
|
+
var Splice = class {
|
|
2372
|
+
#code;
|
|
2373
|
+
#start;
|
|
2374
|
+
#end;
|
|
2375
|
+
#edits = [];
|
|
2376
|
+
constructor(code, start, end) {
|
|
2377
|
+
this.#code = code;
|
|
2378
|
+
this.#start = start;
|
|
2379
|
+
this.#end = end;
|
|
2380
|
+
}
|
|
2381
|
+
overwrite(start, end, content) {
|
|
2382
|
+
this.#edits.push({
|
|
2383
|
+
start,
|
|
2384
|
+
end,
|
|
2385
|
+
content
|
|
2386
|
+
});
|
|
2387
|
+
return this;
|
|
2388
|
+
}
|
|
2389
|
+
remove(start, end) {
|
|
2390
|
+
return start === end ? this : this.overwrite(start, end, "");
|
|
2391
|
+
}
|
|
2392
|
+
/** Whether an edit overlaps `[start, end)`. */
|
|
2393
|
+
touches(start, end) {
|
|
2394
|
+
return this.#edits.some((edit) => edit.start < end && start < edit.end);
|
|
2395
|
+
}
|
|
2396
|
+
toString() {
|
|
2397
|
+
let out = "";
|
|
2398
|
+
let pos = this.#start;
|
|
2399
|
+
for (const edit of [...this.#edits].sort((a, b) => a.start - b.start)) {
|
|
2400
|
+
out += this.#code.slice(pos, edit.start) + edit.content;
|
|
2401
|
+
pos = edit.end;
|
|
2402
|
+
}
|
|
2403
|
+
return out + this.#code.slice(pos, this.#end);
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
//#endregion
|
|
2407
|
+
//#region src/vite/framework.ts
|
|
2408
|
+
/**
|
|
2409
|
+
* Which framework a module belongs to (PLAN §5.12), which decides the shape
|
|
2410
|
+
* `sx()` returns, and the framework files (`.vue`, `.svelte`, `.astro`) the
|
|
2411
|
+
* plugin compiles after their own compiler and pre-scans by their scripts
|
|
2412
|
+
* (§5.4). Plain data in, plain data out: the scan's workers use it too.
|
|
2413
|
+
*/
|
|
2414
|
+
const FRAMEWORKS = [
|
|
2415
|
+
"react",
|
|
2416
|
+
"preact",
|
|
2417
|
+
"solid",
|
|
2418
|
+
"vue",
|
|
2419
|
+
"svelte",
|
|
2420
|
+
"astro",
|
|
2421
|
+
"vanilla"
|
|
2422
|
+
];
|
|
2423
|
+
/** Root dependencies that name a project's framework, the first found winning. */
|
|
2424
|
+
const DEPENDENCIES = [
|
|
2425
|
+
["vue", "vue"],
|
|
2426
|
+
["svelte", "svelte"],
|
|
2427
|
+
["solid-js", "solid"],
|
|
2428
|
+
["preact", "preact"],
|
|
2429
|
+
["react", "react"],
|
|
2430
|
+
["astro", "astro"]
|
|
2431
|
+
];
|
|
2432
|
+
/** Extensions whose files a framework compiler turns into JS first; the patterns below list them. */
|
|
2433
|
+
const SFC_EXTENSIONS = [
|
|
2434
|
+
"vue",
|
|
2435
|
+
"svelte",
|
|
2436
|
+
"astro"
|
|
2437
|
+
];
|
|
2438
|
+
const SFC_EXTENSION = `(?:${SFC_EXTENSIONS.join("|")})`;
|
|
2439
|
+
/** `X.vue`, `X.svelte`, `X.astro`, with or without a query. */
|
|
2440
|
+
const SFC_FILE = new RegExp(`\\.${SFC_EXTENSION}(?:[?#]|$)`);
|
|
2441
|
+
/**
|
|
2442
|
+
* The modules of a framework file that hold its compiled scripts: the file
|
|
2443
|
+
* itself, or a script sub-module (`X.vue?vue&type=script&setup=true&lang.ts`,
|
|
2444
|
+
* `X.astro?astro&type=script&index=0&lang.ts`). Styles, templates and other
|
|
2445
|
+
* sub-modules are left alone.
|
|
2446
|
+
*/
|
|
2447
|
+
const SFC_SCRIPT_ID = new RegExp(`\\.${SFC_EXTENSION}(?:$|\\?(?:[^#]*&)?type=script(?:[&#]|$))`);
|
|
2448
|
+
function isSfc(id) {
|
|
2449
|
+
return SFC_FILE.test(id);
|
|
2450
|
+
}
|
|
2451
|
+
/** The framework of a module, from its id, its code and the project's settings. */
|
|
2452
|
+
function frameworkOf(id, code, settings) {
|
|
2453
|
+
const file = id.replace(/[?#].*$/, "");
|
|
2454
|
+
const ext = /\.([a-z]+)$/i.exec(file)?.[1]?.toLowerCase();
|
|
2455
|
+
if (SFC_EXTENSIONS.includes(ext ?? "")) return ext;
|
|
2456
|
+
if (ext === "jsx" || ext === "tsx") {
|
|
2457
|
+
const pragma = /@jsxImportSource\s+([^\s*]+)/.exec(code)?.[1];
|
|
2458
|
+
return jsxFramework(pragma ?? settings.jsxImportSource ?? "react");
|
|
2459
|
+
}
|
|
2460
|
+
return settings.scripts;
|
|
2461
|
+
}
|
|
2462
|
+
/** `preact`, `solid-js`, `vue` (and their subpaths) by name; anything else takes React's props. */
|
|
2463
|
+
function jsxFramework(source) {
|
|
2464
|
+
const name = source.startsWith("@") ? source.split("/", 2).join("/") : source.split("/")[0];
|
|
2465
|
+
if (name === "preact") return "preact";
|
|
2466
|
+
if (name === "solid-js") return "solid";
|
|
2467
|
+
if (name === "vue") return "vue";
|
|
2468
|
+
return "react";
|
|
2469
|
+
}
|
|
2470
|
+
/** The framework the root `package.json` depends on, if one of the known ones. */
|
|
2471
|
+
function detectFramework(root) {
|
|
2472
|
+
const pkg = readJson(path.join(root, "package.json"));
|
|
2473
|
+
if (!pkg) return void 0;
|
|
2474
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(deps(pkg.dependencies)), ...Object.keys(deps(pkg.devDependencies))]);
|
|
2475
|
+
return DEPENDENCIES.find(([name]) => names.has(name))?.[1];
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* The project's JSX import source: the Vite config's (`oxc.jsx.importSource`,
|
|
2479
|
+
* or esbuild's `jsxImportSource`), else the root tsconfig's, following
|
|
2480
|
+
* `extends` and, for a solution file, its `references`.
|
|
2481
|
+
*/
|
|
2482
|
+
function configuredJsxSource(config, root) {
|
|
2483
|
+
const jsx = record(record(config.oxc)?.jsx);
|
|
2484
|
+
const esbuild = record(config.esbuild);
|
|
2485
|
+
return string(jsx?.importSource) ?? string(esbuild?.jsxImportSource) ?? tsconfigJsxSource(path.join(root, "tsconfig.json"), /* @__PURE__ */ new Set());
|
|
2486
|
+
}
|
|
2487
|
+
function tsconfigJsxSource(file, seen) {
|
|
2488
|
+
if (seen.has(file)) return void 0;
|
|
2489
|
+
seen.add(file);
|
|
2490
|
+
const tsconfig = readJson(file, true);
|
|
2491
|
+
if (!tsconfig) return void 0;
|
|
2492
|
+
const own = string(record(tsconfig.compilerOptions)?.jsxImportSource);
|
|
2493
|
+
if (own) return own;
|
|
2494
|
+
const dir = path.dirname(file);
|
|
2495
|
+
const parents = Array.isArray(tsconfig.extends) ? tsconfig.extends : [tsconfig.extends];
|
|
2496
|
+
for (const parent of parents.toReversed()) {
|
|
2497
|
+
const resolved = typeof parent === "string" ? resolveExtends(parent, dir) : void 0;
|
|
2498
|
+
const found = resolved && tsconfigJsxSource(resolved, seen);
|
|
2499
|
+
if (found) return found;
|
|
2500
|
+
}
|
|
2501
|
+
for (const reference of Array.isArray(tsconfig.references) ? tsconfig.references : []) {
|
|
2502
|
+
const target = string(record(reference)?.path);
|
|
2503
|
+
if (!target) continue;
|
|
2504
|
+
const full = path.resolve(dir, target);
|
|
2505
|
+
const found = tsconfigJsxSource(full.endsWith(".json") ? full : path.join(full, "tsconfig.json"), seen);
|
|
2506
|
+
if (found) return found;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
function resolveExtends(specifier, dir) {
|
|
2510
|
+
if (specifier.startsWith(".") || path.isAbsolute(specifier)) {
|
|
2511
|
+
const full = path.resolve(dir, specifier);
|
|
2512
|
+
return existsSync(full) ? full : `${full}.json`;
|
|
2513
|
+
}
|
|
2514
|
+
try {
|
|
2515
|
+
return createRequire(path.join(dir, "tsconfig.json")).resolve(specifier.endsWith(".json") ? specifier : `${specifier}.json`);
|
|
2516
|
+
} catch {
|
|
2517
|
+
try {
|
|
2518
|
+
return createRequire(path.join(dir, "tsconfig.json")).resolve(specifier);
|
|
2519
|
+
} catch {
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* An opening `<script>` tag and the element's text. Attribute values are
|
|
2526
|
+
* matched whole, so a `>` inside quotes (Vue's `generic="T extends
|
|
2527
|
+
* Record<K, V>"`, Svelte's `generics`) does not end the tag.
|
|
2528
|
+
*/
|
|
2529
|
+
const SCRIPT_ELEMENT = /<script\b((?:\s+[^\s"'>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]+))?)*)\s*>([\s\S]*?)<\/script\s*>/dgi;
|
|
2530
|
+
const ATTRIBUTE = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
|
|
2531
|
+
/**
|
|
2532
|
+
* The scripts of a `.vue` (`<script>` and `<script setup>`, one module as
|
|
2533
|
+
* Vue merges them), `.svelte` (instance and module scripts, one module) or
|
|
2534
|
+
* `.astro` file (the frontmatter, and each `<script>` Astro bundles, each a
|
|
2535
|
+
* module of its own). A script with `src` is a file of its own, scanned as
|
|
2536
|
+
* one if `include` has it.
|
|
2537
|
+
*/
|
|
2538
|
+
function extractScripts(id, code) {
|
|
2539
|
+
const ext = /\.([a-z]+)(?:[?#].*)?$/i.exec(id)?.[1]?.toLowerCase();
|
|
2540
|
+
const markup = code.replace(/<!--[\s\S]*?-->/g, blank);
|
|
2541
|
+
const blocks = [];
|
|
2542
|
+
for (const match of markup.matchAll(SCRIPT_ELEMENT)) {
|
|
2543
|
+
const [start, end] = match.indices[2];
|
|
2544
|
+
blocks.push({
|
|
2545
|
+
start,
|
|
2546
|
+
end,
|
|
2547
|
+
attributes: attributesOf(match[1])
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
if (ext === "astro") {
|
|
2551
|
+
const units = [];
|
|
2552
|
+
const fence = /^(\s*)---[^\S\r\n]*\r?\n([\s\S]*?)\r?\n---/.exec(code);
|
|
2553
|
+
if (fence) {
|
|
2554
|
+
const start = fence[0].indexOf("\n", fence[1].length) + 1;
|
|
2555
|
+
units.push({
|
|
2556
|
+
code: keep(code, [[start, start + fence[2].length]]),
|
|
2557
|
+
lang: "ts",
|
|
2558
|
+
functionBody: true
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
const after = fence ? fence.index + fence[0].length : 0;
|
|
2562
|
+
for (const block of blocks) {
|
|
2563
|
+
if (block.start < after || block.attributes.size > 0) continue;
|
|
2564
|
+
units.push({
|
|
2565
|
+
code: keep(code, [[block.start, block.end]]),
|
|
2566
|
+
lang: "ts"
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
return units;
|
|
2570
|
+
}
|
|
2571
|
+
const scripts = blocks.filter(({ attributes }) => {
|
|
2572
|
+
const type = attributes.get("type");
|
|
2573
|
+
return !attributes.has("src") && (type === void 0 || type === "module" || /^(?:text|application)\/(?:java|type)script$/.test(type));
|
|
2574
|
+
});
|
|
2575
|
+
if (scripts.length === 0) return [];
|
|
2576
|
+
const lang = scripts.map((block) => block.attributes.get("lang")).some((l) => l === "tsx" || l === "jsx") ? "tsx" : "ts";
|
|
2577
|
+
return [{
|
|
2578
|
+
code: keep(code, scripts.map((block) => [block.start, block.end])),
|
|
2579
|
+
lang
|
|
2580
|
+
}];
|
|
2581
|
+
}
|
|
2582
|
+
/** A tag's attributes by lower-cased name, `""` for a bare one; the first of a name wins. */
|
|
2583
|
+
function attributesOf(text) {
|
|
2584
|
+
const out = /* @__PURE__ */ new Map();
|
|
2585
|
+
for (const match of text.matchAll(ATTRIBUTE)) {
|
|
2586
|
+
const name = match[1].toLowerCase();
|
|
2587
|
+
if (!out.has(name)) out.set(name, match[2] ?? match[3] ?? match[4] ?? "");
|
|
2588
|
+
}
|
|
2589
|
+
return out;
|
|
2590
|
+
}
|
|
2591
|
+
/** `code` with everything outside `ranges` blanked, line breaks kept. */
|
|
2592
|
+
function keep(code, ranges) {
|
|
2593
|
+
let out = "";
|
|
2594
|
+
let at = 0;
|
|
2595
|
+
for (const [start, end] of ranges) {
|
|
2596
|
+
out += blank(code.slice(at, start)) + code.slice(start, end);
|
|
2597
|
+
at = end;
|
|
2598
|
+
}
|
|
2599
|
+
return out + blank(code.slice(at));
|
|
2600
|
+
}
|
|
2601
|
+
const blank = (text) => text.replace(/[^\r\n]/g, " ");
|
|
2602
|
+
function readJson(file, comments = false) {
|
|
2603
|
+
let text;
|
|
2604
|
+
try {
|
|
2605
|
+
text = readFileSync(file, "utf8");
|
|
2606
|
+
} catch {
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
try {
|
|
2610
|
+
return record(JSON.parse(comments ? stripJsonComments(text) : text));
|
|
2611
|
+
} catch {
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
/** JSONC as tsconfig files use it: comments and trailing commas. */
|
|
2616
|
+
function stripJsonComments(text) {
|
|
2617
|
+
let out = "";
|
|
2618
|
+
for (let i = 0; i < text.length; i++) {
|
|
2619
|
+
const c = text[i];
|
|
2620
|
+
if (c === "\"") {
|
|
2621
|
+
const end = /"(?:[^"\\]|\\.)*"/y;
|
|
2622
|
+
end.lastIndex = i;
|
|
2623
|
+
const literal = end.exec(text)?.[0] ?? text.slice(i);
|
|
2624
|
+
out += literal;
|
|
2625
|
+
i += literal.length - 1;
|
|
2626
|
+
} else if (text.startsWith("//", i)) {
|
|
2627
|
+
const next = text.indexOf("\n", i);
|
|
2628
|
+
i = next === -1 ? text.length : next - 1;
|
|
2629
|
+
} else if (text.startsWith("/*", i)) {
|
|
2630
|
+
const close = text.indexOf("*/", i + 2);
|
|
2631
|
+
i = close === -1 ? text.length : close + 1;
|
|
2632
|
+
} else out += c;
|
|
2633
|
+
}
|
|
2634
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
2635
|
+
}
|
|
2636
|
+
function record(value) {
|
|
2637
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
2638
|
+
}
|
|
2639
|
+
const string = (value) => typeof value === "string" && value ? value : void 0;
|
|
2640
|
+
const deps = (value) => record(value) ?? {};
|
|
2641
|
+
//#endregion
|
|
2642
|
+
//#region src/vite/compile.ts
|
|
2643
|
+
/**
|
|
2644
|
+
* The plugin's side of compiling one module: imported constants are found
|
|
2645
|
+
* through Vite's resolver, read from disk and reported as dependencies, so
|
|
2646
|
+
* tsconfig paths, aliases and workspaces work as they do for the app.
|
|
2647
|
+
*/
|
|
2648
|
+
const REACT = {
|
|
2649
|
+
scripts: "react",
|
|
2650
|
+
jsxImportSource: void 0
|
|
2651
|
+
};
|
|
2652
|
+
/** The file the compiler reads for an import, or "" when it reads none. */
|
|
2653
|
+
async function resolveFile(resolve, source, importer) {
|
|
2654
|
+
const resolved = await resolve(source, importer);
|
|
2655
|
+
if (!resolved || resolved.external || resolved.id.startsWith("\0")) return "";
|
|
2656
|
+
return resolved.id.replace(/[?#].*$/, "");
|
|
2657
|
+
}
|
|
2658
|
+
/** Whether every recorded import resolves to the same file through `resolve`. */
|
|
2659
|
+
async function resolvesAlike(resolutions, resolve) {
|
|
2660
|
+
for (const [request, file] of Object.entries(resolutions)) {
|
|
2661
|
+
const at = request.indexOf("\0");
|
|
2662
|
+
if (await resolve(request.slice(at + 1), request.slice(0, at)) !== file) return false;
|
|
2663
|
+
}
|
|
2664
|
+
return true;
|
|
2665
|
+
}
|
|
2666
|
+
/** `this.resolve` without our own `resolveId`, as a module loader for the compiler. */
|
|
2667
|
+
function moduleLoader(resolve, resolutions = {}) {
|
|
2668
|
+
return async (source, importer) => {
|
|
2669
|
+
const id = await resolveFile(resolve, source, importer);
|
|
2670
|
+
resolutions[`${importer}\0${source}`] = id;
|
|
2671
|
+
if (!id) return null;
|
|
2672
|
+
try {
|
|
2673
|
+
return {
|
|
2674
|
+
id,
|
|
2675
|
+
code: readFileSync(id, "utf8")
|
|
2676
|
+
};
|
|
2677
|
+
} catch {
|
|
2678
|
+
return null;
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Compiles one module. `script` is set for a script the pre-scan extracted
|
|
2684
|
+
* from a framework file, whose id names the file rather than how it parses;
|
|
2685
|
+
* `sourceLine`, in dev, for a framework's output, whose lines its compiler
|
|
2686
|
+
* moved (`CompileOptions.sourceLine`).
|
|
2687
|
+
*/
|
|
2688
|
+
async function compile(ctx, id, code, resolve, script, sourceLine) {
|
|
2689
|
+
const resolutions = {};
|
|
2690
|
+
const warnings = [];
|
|
2691
|
+
return {
|
|
2692
|
+
...await compileModule(code, {
|
|
2693
|
+
system: ctx.system,
|
|
2694
|
+
id,
|
|
2695
|
+
importSources: ctx.importSources,
|
|
2696
|
+
loadModule: moduleLoader(resolve, resolutions),
|
|
2697
|
+
framework: frameworkOf(id, code, ctx.frameworks ?? REACT),
|
|
2698
|
+
...script ? {
|
|
2699
|
+
lang: script.lang,
|
|
2700
|
+
functionBody: script.functionBody === true
|
|
2701
|
+
} : {},
|
|
2702
|
+
sourceMap: ctx.sourceMap,
|
|
2703
|
+
cache: ctx.parsed,
|
|
2704
|
+
dev: ctx.dev === true,
|
|
2705
|
+
placeholders: ctx.placeholders === true,
|
|
2706
|
+
keepLines: ctx.keepLines === true,
|
|
2707
|
+
...ctx.root === void 0 ? {} : { root: ctx.root },
|
|
2708
|
+
...ctx.package ? { package: ctx.package } : {},
|
|
2709
|
+
...sourceLine ? { sourceLine } : {},
|
|
2710
|
+
warn: (message) => warnings.push(message)
|
|
2711
|
+
}),
|
|
2712
|
+
resolutions,
|
|
2713
|
+
warnings
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
/**
|
|
2717
|
+
* Parsed modules, most recently used last. A design system's barrel is read
|
|
2718
|
+
* by most styled modules, so it stays; the ASTs of the modules themselves
|
|
2719
|
+
* (about 20 times their source size) are not all kept for the server's life.
|
|
2720
|
+
*/
|
|
2721
|
+
function createParseCache(limit = 256) {
|
|
2722
|
+
return { parsed: new LruMap(limit) };
|
|
2723
|
+
}
|
|
2724
|
+
var LruMap = class extends Map {
|
|
2725
|
+
#limit;
|
|
2726
|
+
constructor(limit) {
|
|
2727
|
+
super();
|
|
2728
|
+
this.#limit = limit;
|
|
2729
|
+
}
|
|
2730
|
+
get(key) {
|
|
2731
|
+
const value = super.get(key);
|
|
2732
|
+
if (value !== void 0) {
|
|
2733
|
+
super.delete(key);
|
|
2734
|
+
super.set(key, value);
|
|
2735
|
+
}
|
|
2736
|
+
return value;
|
|
2737
|
+
}
|
|
2738
|
+
set(key, value) {
|
|
2739
|
+
super.delete(key);
|
|
2740
|
+
super.set(key, value);
|
|
2741
|
+
if (this.size > this.#limit) super.delete(this.keys().next().value);
|
|
2742
|
+
return this;
|
|
2743
|
+
}
|
|
2744
|
+
};
|
|
2745
|
+
//#endregion
|
|
2746
|
+
//#region src/vite/pool.ts
|
|
2747
|
+
/**
|
|
2748
|
+
* The pre-scan's worker threads (PLAN D17). A cold scan is CPU-bound: every
|
|
2749
|
+
* module is parsed, evaluated and normalised. With more than `POOL_MIN`
|
|
2750
|
+
* modules to compile, worker threads run the same compiler on the files'
|
|
2751
|
+
* contents next to the main thread. The main thread keeps what must stay
|
|
2752
|
+
* one: resolution through Vite's plugins, the cache and the order results
|
|
2753
|
+
* are combined in. Only plain data crosses: jobs, resolution requests and
|
|
2754
|
+
* results, and errors as their fields.
|
|
2755
|
+
*/
|
|
2756
|
+
/** Jobs a worker holds at once, so one waiting for a resolution leaves it busy. */
|
|
2757
|
+
const IN_FLIGHT = 16;
|
|
2758
|
+
/**
|
|
2759
|
+
* A config's hash with its functions counted rather than read: the main
|
|
2760
|
+
* thread loads a TypeScript config through Vite, which prints a function's
|
|
2761
|
+
* source otherwise than Node's type stripping does in a worker.
|
|
2762
|
+
*/
|
|
2763
|
+
function configShape(config) {
|
|
2764
|
+
return hash(stableStringify(config, () => "\0function"), 8);
|
|
2765
|
+
}
|
|
2766
|
+
function toErrorData(error) {
|
|
2767
|
+
if (error instanceof VicssError) return {
|
|
2768
|
+
name: error.name,
|
|
2769
|
+
message: error.message,
|
|
2770
|
+
code: error.code,
|
|
2771
|
+
...error.hint === void 0 ? {} : { hint: error.hint },
|
|
2772
|
+
...error.loc === void 0 ? {} : { loc: error.loc }
|
|
2773
|
+
};
|
|
2774
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
2775
|
+
return {
|
|
2776
|
+
name: e.name,
|
|
2777
|
+
message: e.message,
|
|
2778
|
+
...e.stack ? { stack: e.stack } : {}
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
function fromErrorData(data) {
|
|
2782
|
+
if (data.name === "VicssError" && data.code !== void 0) return new VicssError(data.code, data.message, {
|
|
2783
|
+
...data.loc ? { loc: data.loc } : {},
|
|
2784
|
+
...data.hint === void 0 ? {} : { hint: data.hint }
|
|
2785
|
+
});
|
|
2786
|
+
const error = new Error(data.message);
|
|
2787
|
+
error.name = data.name;
|
|
2788
|
+
if (data.stack) error.stack = data.stack;
|
|
2789
|
+
return error;
|
|
2790
|
+
}
|
|
2791
|
+
/** A job a worker took and could not finish: the thread died. The caller runs it itself. */
|
|
2792
|
+
var WorkerLost = class extends Error {};
|
|
2793
|
+
/**
|
|
2794
|
+
* Threads for this many compiles: one per 100 modules, leaving a core to the
|
|
2795
|
+
* main thread (which compiles too) and one to Rolldown's own threads. On
|
|
2796
|
+
* four cores, three workers were no faster than two.
|
|
2797
|
+
*/
|
|
2798
|
+
function poolSize(jobs) {
|
|
2799
|
+
return Math.max(0, Math.min(availableParallelism() - 2, Math.ceil(jobs / 100), 7));
|
|
2800
|
+
}
|
|
2801
|
+
var CompilePool = class CompilePool {
|
|
2802
|
+
workers;
|
|
2803
|
+
#threads;
|
|
2804
|
+
#use;
|
|
2805
|
+
#used = false;
|
|
2806
|
+
constructor(workers, threads, use) {
|
|
2807
|
+
this.workers = workers;
|
|
2808
|
+
this.#threads = threads;
|
|
2809
|
+
this.#use = use;
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Starts `size` threads, which load the compiler and wait for `use()`; a
|
|
2813
|
+
* build spawns them before its config is loaded, which takes as long as
|
|
2814
|
+
* their start. Unreferenced until used, so a config resolved for a build
|
|
2815
|
+
* that never runs leaves nothing alive. Undefined when threads cannot start.
|
|
2816
|
+
*/
|
|
2817
|
+
static spawn(size) {
|
|
2818
|
+
if (size < 1) return void 0;
|
|
2819
|
+
const threads = [];
|
|
2820
|
+
try {
|
|
2821
|
+
for (let i = 0; i < size; i++) threads.push(new Worker(WORKER_URL));
|
|
2822
|
+
} catch {
|
|
2823
|
+
for (const thread of threads) thread.terminate();
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
for (const thread of threads) thread.unref();
|
|
2827
|
+
let current;
|
|
2828
|
+
let warned = false;
|
|
2829
|
+
const warn = (message) => {
|
|
2830
|
+
if (warned) return;
|
|
2831
|
+
warned = true;
|
|
2832
|
+
current?.warn?.(message);
|
|
2833
|
+
};
|
|
2834
|
+
const resolve = (source, importer) => current ? current.resolve(source, importer) : Promise.resolve("");
|
|
2835
|
+
const workers = threads.map((thread) => connect(thread, resolve, warn));
|
|
2836
|
+
return new CompilePool(workers, threads, (use) => {
|
|
2837
|
+
current = use;
|
|
2838
|
+
for (const thread of threads) {
|
|
2839
|
+
thread.ref();
|
|
2840
|
+
try {
|
|
2841
|
+
thread.postMessage({
|
|
2842
|
+
type: "setup",
|
|
2843
|
+
setup: use.setup
|
|
2844
|
+
});
|
|
2845
|
+
} catch (error) {
|
|
2846
|
+
warn(`the scan runs on one thread: ${error instanceof Error ? error.message : String(error)}`);
|
|
2847
|
+
thread.terminate();
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
/** Sends the scan's setup; the workers report ready once they built the compiler. */
|
|
2853
|
+
use(use) {
|
|
2854
|
+
if (this.#used) throw new Error("vicss: a scan pool serves one scan");
|
|
2855
|
+
this.#used = true;
|
|
2856
|
+
this.#use(use);
|
|
2857
|
+
return this;
|
|
2858
|
+
}
|
|
2859
|
+
/** Ends the threads; a pool is closed once, whoever closes it. */
|
|
2860
|
+
async close() {
|
|
2861
|
+
await Promise.all(this.#threads.map((thread) => thread.terminate()));
|
|
2862
|
+
}
|
|
2863
|
+
};
|
|
2864
|
+
/** `src/vite/worker.ts` next to this file, or `dist/worker.js` next to the bundled plugin. */
|
|
2865
|
+
const WORKER_URL = new URL(import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.js", import.meta.url);
|
|
2866
|
+
function connect(thread, resolve, warn) {
|
|
2867
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
2868
|
+
let next = 0;
|
|
2869
|
+
let alive = true;
|
|
2870
|
+
let settle;
|
|
2871
|
+
const ready = new Promise((resolve) => settle = resolve);
|
|
2872
|
+
const lost = (why) => {
|
|
2873
|
+
if (!alive) return;
|
|
2874
|
+
alive = false;
|
|
2875
|
+
settle(false);
|
|
2876
|
+
for (const job of jobs.values()) job.reject(new WorkerLost(why));
|
|
2877
|
+
jobs.clear();
|
|
2878
|
+
};
|
|
2879
|
+
thread.on("message", (message) => {
|
|
2880
|
+
switch (message.type) {
|
|
2881
|
+
case "ready":
|
|
2882
|
+
settle(true);
|
|
2883
|
+
return;
|
|
2884
|
+
case "unavailable":
|
|
2885
|
+
warn(`a scan worker could not start, so the main thread compiles more: ${message.message}`);
|
|
2886
|
+
lost(message.message);
|
|
2887
|
+
thread.terminate();
|
|
2888
|
+
return;
|
|
2889
|
+
case "resolve":
|
|
2890
|
+
resolve(message.source, message.importer).then((file) => post({
|
|
2891
|
+
type: "resolved",
|
|
2892
|
+
request: message.request,
|
|
2893
|
+
file
|
|
2894
|
+
}), () => post({
|
|
2895
|
+
type: "resolved",
|
|
2896
|
+
request: message.request,
|
|
2897
|
+
file: ""
|
|
2898
|
+
}));
|
|
2899
|
+
return;
|
|
2900
|
+
case "done":
|
|
2901
|
+
case "failed": {
|
|
2902
|
+
const job = jobs.get(message.job);
|
|
2903
|
+
jobs.delete(message.job);
|
|
2904
|
+
if (message.type === "done") job?.resolve(message.result);
|
|
2905
|
+
else job?.reject(fromErrorData(message.error));
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
});
|
|
2910
|
+
thread.on("error", (error) => {
|
|
2911
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
2912
|
+
warn(`a scan worker stopped, so the main thread compiles more: ${why}`);
|
|
2913
|
+
lost(why);
|
|
2914
|
+
});
|
|
2915
|
+
thread.on("exit", () => lost("the worker exited"));
|
|
2916
|
+
const post = (message) => {
|
|
2917
|
+
if (alive) thread.postMessage(message);
|
|
2918
|
+
};
|
|
2919
|
+
return {
|
|
2920
|
+
ready,
|
|
2921
|
+
capacity: IN_FLIGHT,
|
|
2922
|
+
compile(job) {
|
|
2923
|
+
if (!alive) return Promise.reject(new WorkerLost("the worker exited"));
|
|
2924
|
+
const id = next++;
|
|
2925
|
+
return new Promise((resolve, reject) => {
|
|
2926
|
+
jobs.set(id, {
|
|
2927
|
+
resolve,
|
|
2928
|
+
reject
|
|
2929
|
+
});
|
|
2930
|
+
post({
|
|
2931
|
+
type: "compile",
|
|
2932
|
+
job: id,
|
|
2933
|
+
id: job.id,
|
|
2934
|
+
code: job.code,
|
|
2935
|
+
...job.script ? { script: job.script } : {}
|
|
2936
|
+
});
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
//#endregion
|
|
2942
|
+
export { originalLines as S, isSfc as _, toErrorData as a, setParser as b, moduleLoader as c, FRAMEWORKS as d, SFC_SCRIPT_ID as f, frameworkOf as g, extractScripts as h, poolSize as i, resolveFile as l, detectFramework as m, WorkerLost as n, compile as o, configuredJsxSource as p, configShape as r, createParseCache as s, CompilePool as t, resolvesAlike as u, compileModule as v, SourceEdits as x, createModuleCache as y };
|