@bamboocss/vite 1.45.5 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2487 @@
1
+ const require_chunk = require("./chunk.cjs");
2
+ let _bamboocss_shared = require("@bamboocss/shared");
3
+ let node_path = require("node:path");
4
+ let magic_string = require("magic-string");
5
+ magic_string = require_chunk.__toESM(magic_string);
6
+ let _bamboocss_config_ts_path = require("@bamboocss/config/ts-path");
7
+ let _bamboocss_extractor = require("@bamboocss/extractor");
8
+ let ts_morph = require("ts-morph");
9
+ //#region src/fold-analysis.ts
10
+ /**
11
+ * Nodes that *compose* a value out of their children rather than computing one.
12
+ *
13
+ * The boundary of the walk below, and the whole of its precision. Climbing through these
14
+ * keeps a value's provenance: `'red.300'` inside `{ color: 'red.300' }` inside a default is
15
+ * still the default's. Anything else — a call, a function body, a JSX element — produces its
16
+ * value by being evaluated, so what is written inside it is an argument to that evaluation
17
+ * and not the enclosing default's value.
18
+ *
19
+ * Without the boundary, `({ cls = css({ color: 'red.300' }) }) => cls` is rejected: the call's
20
+ * own literal argument is syntactically inside a default, so an unbounded walk calls it one.
21
+ * That is correct code, and rejecting it fails the build.
22
+ */
23
+ const composesValue = (node) => ts_morph.Node.isObjectLiteralExpression(node) || ts_morph.Node.isArrayLiteralExpression(node) || ts_morph.Node.isPropertyAssignment(node) || ts_morph.Node.isShorthandPropertyAssignment(node) || ts_morph.Node.isSpreadAssignment(node) || ts_morph.Node.isSpreadElement(node) || ts_morph.Node.isAsExpression(node) || ts_morph.Node.isParenthesizedExpression(node) || ts_morph.Node.isNonNullExpression(node) || ts_morph.Node.isTypeAssertion(node) || ts_morph.Node.isSatisfiesExpression(node);
24
+ /** Is `inner` written within `outer`? Positions rather than a walk, so it is O(1). */
25
+ const contains = (outer, inner) => outer.getSourceFile() === inner.getSourceFile() && outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
26
+ /**
27
+ * Did this value come from the `= …` of a destructuring binding?
28
+ *
29
+ * `const { tone = 'red.300' } = source` boxes as the literal `'red.300'`: the extractor's
30
+ * `maybeDefinitionValue` tests for an initializer first and returns the boxed default, never
31
+ * reaching the branch that would read `source`. So the default is reported as the value whether
32
+ * or not it is the one that applies.
33
+ *
34
+ * For extraction that is merely optimistic, and deliberately so: a CLI or PostCSS build ships a
35
+ * runtime `css()`, where the default genuinely does apply when the caller omits the key, and it
36
+ * needs a rule behind it. Folding is where the same resolution turns into a wrong answer,
37
+ * because the call is *replaced* by that value.
38
+ *
39
+ * Stops at the first non-composing parent, so a call written inside a default keeps its own
40
+ * provenance, and checks that the binding element was reached through its initializer, so
41
+ * `{ tone = X }`'s name node is not mistaken for its default.
42
+ */
43
+ const isBindingElementDefault = (node) => {
44
+ if (node && ts_morph.Node.isCallExpression(node)) return false;
45
+ let current = node;
46
+ while (current) {
47
+ const parent = current.getParent();
48
+ if (!parent) return false;
49
+ if (ts_morph.Node.isBindingElement(parent)) return parent.getInitializer() === current;
50
+ if (!composesValue(parent)) return false;
51
+ current = parent;
52
+ }
53
+ return false;
54
+ };
55
+ /**
56
+ * The same question asked of a whole box, including how it was resolved.
57
+ *
58
+ * The node a box reports is not always the one its value came from — an empty `{}` default
59
+ * boxes against the call rather than against the `{}` — so the resolution stack is consulted
60
+ * too. A binding element reaches that stack by having been resolved *through*, which is the
61
+ * signal the extractor itself reads when one of these is a conditional's test.
62
+ *
63
+ * Only the entries that are binding elements are examined. Walking up from every other entry
64
+ * was tried and never once changed a verdict across the default spellings or the sandbox's own
65
+ * modules, while accounting for most of the parent hops this does — the stack carries nodes
66
+ * that were never resolved through, including a call's own arguments, so walking from them is
67
+ * both the expensive half and the one that reaches conclusions it has no basis for.
68
+ *
69
+ * A binding element without an initializer carries no default to mistrust: `const { tone } =
70
+ * source` either resolves from `source` or does not resolve at all.
71
+ */
72
+ const isFromBindingDefault = (node) => {
73
+ const own = node.getNode?.();
74
+ if (isBindingElementDefault(own)) return true;
75
+ for (const entry of node.getStack?.() ?? []) {
76
+ if (!ts_morph.Node.isBindingElement(entry) || !entry.getInitializer()) continue;
77
+ if (own && contains(entry, own)) continue;
78
+ return true;
79
+ }
80
+ return false;
81
+ };
82
+ /**
83
+ * Statically resolvable means: every box in the tree carries a known value.
84
+ *
85
+ * `unresolvable` is the extractor saying it could not evaluate a node.
86
+ * `conditional` is a ternary — two possible values, so there is no single string to
87
+ * fold to. `box.fallback` produces an object with no `type` at all, which is likewise
88
+ * not something we can trust.
89
+ */
90
+ const isStaticBox = (node, seen = /* @__PURE__ */ new Set()) => {
91
+ if (!node) return false;
92
+ if (seen.has(node)) return true;
93
+ seen.add(node);
94
+ if (_bamboocss_extractor.box.isUnresolvable(node) || _bamboocss_extractor.box.isConditional(node)) return false;
95
+ if (!("type" in node) || node.type == null) return false;
96
+ if (isFromBindingDefault(node)) return false;
97
+ if (_bamboocss_extractor.box.isLiteral(node) && node.value === void 0) {
98
+ const source = node.getNode?.();
99
+ return Boolean(source && ts_morph.Node.isIdentifier(source) && source.getText() === "undefined");
100
+ }
101
+ if (_bamboocss_extractor.box.isMap(node)) {
102
+ for (const child of node.value.values()) if (!isStaticBox(child, seen)) return false;
103
+ return true;
104
+ }
105
+ if (_bamboocss_extractor.box.isArray(node)) {
106
+ for (const child of node.value) if (!isStaticBox(child, seen)) return false;
107
+ return true;
108
+ }
109
+ return true;
110
+ };
111
+ /**
112
+ * Strip the wrappers the extractor strips before it builds a box, so the source node
113
+ * compared against a box is the same node the box was built from.
114
+ *
115
+ * A local copy of the extractor's `unwrapExpression`, which is not part of its public
116
+ * surface. Recognising fewer wrappers than it does is not a cosmetic difference: an
117
+ * unrecognised one leaves an object literal wrapped, and the object checks below skip it.
118
+ */
119
+ const unwrapExpression = (node) => ts_morph.Node.isAsExpression(node) || ts_morph.Node.isParenthesizedExpression(node) || ts_morph.Node.isNonNullExpression(node) || ts_morph.Node.isTypeAssertion(node) || ts_morph.Node.isSatisfiesExpression(node) ? unwrapExpression(node.getExpression()) : node;
120
+ /**
121
+ * Mirrors the parser's evaluator environment, so re-boxing an operand here gets the same
122
+ * answer the extraction did. Left to its default, ts-evaluator presets to `NODE` and
123
+ * would resolve expressions the parser cannot see — making this check *more* permissive
124
+ * than the extraction it is auditing, which is the one thing it must never be.
125
+ */
126
+ const REBOXED = { getEvaluateOptions: () => ({ environment: { preset: "ECMA" } }) };
127
+ const rebox = (node) => (0, _bamboocss_extractor.maybeBoxNode)(unwrapExpression(node), [], REBOXED);
128
+ /**
129
+ * Did this operand resolve to a value the program will actually produce?
130
+ *
131
+ * Producing a box is not enough when the operand is itself a choice: `a || b || c` parses
132
+ * as `(a || b) || c`, so the outer operator is handed whatever the inner one answered —
133
+ * including an arm the extractor invented. Asking only "is there a box" reads that
134
+ * invention as an ordinary literal.
135
+ */
136
+ const resolvesExactly = (node) => {
137
+ const inner = unwrapExpression(node);
138
+ if (!rebox(inner)) return false;
139
+ return ts_morph.Node.isConditionalExpression(inner) || isCollapsedBinary(inner) ? decidedAtBuildTime(inner) : true;
140
+ };
141
+ /**
142
+ * Is this operand's value written here, rather than named?
143
+ *
144
+ * A box records what the extractor resolved a name *through* — a `let`'s initializer, a
145
+ * parameter's default — none of which is what the operand holds when the call runs.
146
+ * `let m = '1'; m = undefined` still boxes as `'1'`, and `({ c = 'red.300' })` still boxes
147
+ * as `'red.300'` for a caller that passed something else. Only a value written at the call
148
+ * site is what it appears to be, so only that can be judged truthy or nullish here.
149
+ */
150
+ const isWrittenHere = (node) => {
151
+ const inner = unwrapExpression(node);
152
+ return ts_morph.Node.isStringLiteral(inner) || ts_morph.Node.isNumericLiteral(inner) || ts_morph.Node.isNoSubstitutionTemplateLiteral(inner) || ts_morph.Node.isObjectLiteralExpression(inner) || ts_morph.Node.isArrayLiteralExpression(inner) || ts_morph.Node.isTrueLiteral(inner) || ts_morph.Node.isFalseLiteral(inner) || inner.getKind() === ts_morph.SyntaxKind.NullKeyword || ts_morph.Node.isPrefixUnaryExpression(inner) && ts_morph.Node.isNumericLiteral(inner.getOperand());
153
+ };
154
+ /** An inline value's truthiness, which its box carries directly. */
155
+ const isTruthy = (boxNode) => _bamboocss_extractor.box.isLiteral(boxNode) ? Boolean(boxNode.value) : _bamboocss_extractor.box.isMap(boxNode) || _bamboocss_extractor.box.isArray(boxNode) || _bamboocss_extractor.box.isObject(boxNode);
156
+ /**
157
+ * Did the extractor *decide* this choice, or guess at it?
158
+ *
159
+ * `a ? b : c`, `a || b` and `a && b` are asked "what styles could this produce", and when
160
+ * one arm does not evaluate the extractor answers with the other rather than refusing
161
+ * (`maybe-box-node.ts`, `whenTrueValue && !whenFalseValue`). That is right for generating
162
+ * CSS — emit rules for whatever might be used — and wrong for rewriting source, where the
163
+ * arm it kept becomes the only one that runs.
164
+ *
165
+ * For a ternary the tell is the arms: it guessed exactly when one produced a box and the
166
+ * other did not. For a short-circuit the answer is always the left operand, so what has to
167
+ * be established is that the left is the side that wins.
168
+ */
169
+ const decidedAtBuildTime = (node) => {
170
+ if (ts_morph.Node.isConditionalExpression(node)) return resolvesExactly(node.getWhenTrue()) && resolvesExactly(node.getWhenFalse());
171
+ const operator = node.getOperatorToken().getKind();
172
+ if (!SHORT_CIRCUIT.includes(operator)) return false;
173
+ if (!isWrittenHere(node.getLeft())) return false;
174
+ const left = rebox(node.getLeft());
175
+ if (!left) return false;
176
+ if (operator === ts_morph.SyntaxKind.BarBarToken) return isTruthy(left);
177
+ if (operator === ts_morph.SyntaxKind.QuestionQuestionToken) return !_bamboocss_extractor.box.isLiteral(left) || left.value != null;
178
+ return isTruthy(left) ? resolvesExactly(node.getRight()) : true;
179
+ };
180
+ const SHORT_CIRCUIT = [
181
+ ts_morph.SyntaxKind.AmpersandAmpersandToken,
182
+ ts_morph.SyntaxKind.BarBarToken,
183
+ ts_morph.SyntaxKind.QuestionQuestionToken
184
+ ];
185
+ /**
186
+ * Every binary form the extractor collapses to one operand — the short-circuits plus the
187
+ * comparisons, which `isLogicalSyntax` sends down the same path even though their value is
188
+ * a boolean rather than either side.
189
+ *
190
+ * Hoisted rather than built per call: `accountsForSource` asks this for every property of
191
+ * every candidate, and rebuilding a thirteen-element list each time is not free.
192
+ */
193
+ const COLLAPSED_BINARY = new Set([
194
+ ...SHORT_CIRCUIT,
195
+ ts_morph.SyntaxKind.EqualsEqualsToken,
196
+ ts_morph.SyntaxKind.EqualsEqualsEqualsToken,
197
+ ts_morph.SyntaxKind.ExclamationEqualsToken,
198
+ ts_morph.SyntaxKind.ExclamationEqualsEqualsToken,
199
+ ts_morph.SyntaxKind.GreaterThanToken,
200
+ ts_morph.SyntaxKind.GreaterThanEqualsToken,
201
+ ts_morph.SyntaxKind.LessThanToken,
202
+ ts_morph.SyntaxKind.LessThanEqualsToken,
203
+ ts_morph.SyntaxKind.InKeyword,
204
+ ts_morph.SyntaxKind.InstanceOfKeyword
205
+ ]);
206
+ const isCollapsedBinary = (node) => ts_morph.Node.isBinaryExpression(node) && COLLAPSED_BINARY.has(node.getOperatorToken().getKind());
207
+ /**
208
+ * Does the extracted box account for every property the source declares?
209
+ *
210
+ * `isStaticBox` is not sufficient on its own. The extractor *omits* what it cannot
211
+ * evaluate rather than marking it unresolvable, so `css({ color: 'red.300', ...rest })`
212
+ * yields a perfectly static-looking map holding only `color`. Folding that produces
213
+ * `"c_red.300"` and silently drops everything `rest` contributed.
214
+ *
215
+ * So the source is the authority on what the call contains, and anything the box does
216
+ * not account for disqualifies the fold:
217
+ *
218
+ * - a declared property missing from the map (its value did not evaluate)
219
+ * - a computed key, which we cannot match against the map by name
220
+ * - a spread, unless it is an inline object literal
221
+ *
222
+ * Spreads are the conservative case. `{ ...base }` where `base` is a static local
223
+ * object *is* resolved by the extractor, but a resolved spread and an unresolved one
224
+ * are indistinguishable once flattened into the map — both just contribute keys, or
225
+ * fail to. Rather than guess and erase evaluation the compiler cannot reproduce, the call
226
+ * is rejected.
227
+ */
228
+ /**
229
+ * What a property's value is written as. A shorthand names it, so the name *is* the
230
+ * expression — reading an initializer that is not there reports the property as having no
231
+ * source, and everything hidden behind the name goes unchecked.
232
+ */
233
+ const valueOf = (property) => ts_morph.Node.isPropertyAssignment(property) ? property.getInitializer() : ts_morph.Node.isShorthandPropertyAssignment(property) ? property.getNameNode() : void 0;
234
+ const accountsForSource = (node, boxNode) => {
235
+ if (!node) return true;
236
+ const unwrapped = unwrapExpression(node);
237
+ if ((ts_morph.Node.isConditionalExpression(unwrapped) || isCollapsedBinary(unwrapped)) && !_bamboocss_extractor.box.isConditional(boxNode) && !decidedAtBuildTime(unwrapped)) return false;
238
+ if (ts_morph.Node.isArrayLiteralExpression(unwrapped)) {
239
+ if (!_bamboocss_extractor.box.isArray(boxNode)) return false;
240
+ const elements = unwrapped.getElements();
241
+ if (elements.length !== boxNode.value.length) return false;
242
+ return elements.every((element, index) => accountsForSource(element, boxNode.value[index]));
243
+ }
244
+ if (!ts_morph.Node.isObjectLiteralExpression(unwrapped)) {
245
+ const origin = _bamboocss_extractor.box.isMap(boxNode) ? boxNode.getNode() : void 0;
246
+ return !origin || origin === unwrapped || !ts_morph.Node.isObjectLiteralExpression(origin) ? true : accountsForSource(origin, boxNode);
247
+ }
248
+ if (!_bamboocss_extractor.box.isMap(boxNode)) return false;
249
+ for (const property of unwrapped.getProperties()) {
250
+ if (ts_morph.Node.isSpreadAssignment(property)) {
251
+ const expression = unwrapExpression(property.getExpression());
252
+ if (ts_morph.Node.isObjectLiteralExpression(expression)) continue;
253
+ const walked = boxNode.resolvedSpreads?.find((entry) => entry.node === expression);
254
+ if (!walked) return false;
255
+ if (!accountsForSource(walked.box.getNode(), walked.box)) return false;
256
+ continue;
257
+ }
258
+ if (ts_morph.Node.isMethodDeclaration(property) || ts_morph.Node.isGetAccessorDeclaration(property) || ts_morph.Node.isSetAccessorDeclaration(property)) return false;
259
+ if (!ts_morph.Node.isPropertyAssignment(property) && !ts_morph.Node.isShorthandPropertyAssignment(property)) return false;
260
+ const nameNode = property.getNameNode();
261
+ if (ts_morph.Node.isComputedPropertyName(nameNode)) return false;
262
+ const key = ts_morph.Node.isStringLiteral(nameNode) || ts_morph.Node.isNumericLiteral(nameNode) ? String(nameNode.getLiteralValue()) : nameNode.getText();
263
+ const value = valueOf(property);
264
+ if (value && ts_morph.Node.isIdentifier(value) && value.getText() === "undefined") continue;
265
+ if (!boxNode.value.has(key)) return false;
266
+ if (!accountsForSource(value, boxNode.value.get(key))) return false;
267
+ }
268
+ return true;
269
+ };
270
+ /**
271
+ * Memo keyed on a file, thrown away when its text is replaced.
272
+ *
273
+ * A plain `WeakMap<SourceFile, …>` is wrong here: ts-morph reuses the wrapper when a path
274
+ * is re-added with new text — which is what a watch rebuild does — so it would answer for
275
+ * the previous revision. Comparing against the text it was computed from costs a
276
+ * reference check while the file is unchanged, since `getFullText()` hands back the same
277
+ * string instance.
278
+ */
279
+ const byText = (cache, sourceFile, compute) => {
280
+ const text = sourceFile.getFullText();
281
+ const hit = cache.get(sourceFile);
282
+ if (hit && hit.text === text) return hit.value;
283
+ const value = compute();
284
+ cache.set(sourceFile, {
285
+ text,
286
+ value
287
+ });
288
+ return value;
289
+ };
290
+ /**
291
+ * Every name declared at module scope, which is what an added import could collide with.
292
+ *
293
+ * This replaced `sourceFile.getLocals()`. That was precise, but it goes through the
294
+ * compiler's symbol table, and reaching for it binds the program — including every
295
+ * `.d.ts` the module's imports pull in. It cost ~8ms on a ten-line file and grew with the
296
+ * project, which was invisible while only a partial split reached it and became the
297
+ * dominant cost once open-ended values started lowering too.
298
+ *
299
+ * A syntactic walk answers the same question: a binding in a nested *function* cannot
300
+ * collide with a module-scope import, and one that shadows it *at the call site* is what
301
+ * `isShadowed` is for. Memoized against the file's text rather than the file, since
302
+ * ts-morph reuses the wrapper across a re-add and a plain `WeakMap` would answer for the
303
+ * previous revision. Uncached it is re-walked per candidate, which is quadratic in a
304
+ * module of many elements.
305
+ */
306
+ const moduleScopeCache = /* @__PURE__ */ new WeakMap();
307
+ const declaredAtModuleScope = (sourceFile) => byText(moduleScopeCache, sourceFile, () => collectModuleScopeNames(sourceFile));
308
+ const collectModuleScopeNames = (sourceFile) => {
309
+ const names = /* @__PURE__ */ new Set();
310
+ const addBinding = (node) => {
311
+ if (!node) return;
312
+ if (ts_morph.Node.isObjectBindingPattern(node) || ts_morph.Node.isArrayBindingPattern(node)) {
313
+ for (const element of node.getElements()) if (ts_morph.Node.isBindingElement(element)) addBinding(element.getNameNode());
314
+ return;
315
+ }
316
+ if (ts_morph.Node.isIdentifier(node)) names.add(node.getText());
317
+ };
318
+ const addDeclarations = (list) => {
319
+ if (ts_morph.Node.isVariableStatement(list)) {
320
+ for (const declaration of list.getDeclarations()) addBinding(declaration.getNameNode());
321
+ return;
322
+ }
323
+ if (ts_morph.Node.isVariableDeclarationList(list)) for (const declaration of list.getDeclarations()) addBinding(declaration.getNameNode());
324
+ };
325
+ const isVar = (node) => (ts_morph.Node.isVariableStatement(node) || ts_morph.Node.isVariableDeclarationList(node)) && node.getDeclarationKind() === ts_morph.VariableDeclarationKind.Var;
326
+ /**
327
+ * `var` is scoped to the enclosing *function*, not the enclosing block, so one written
328
+ * inside any statement at the top level still binds at module scope. Walking only the
329
+ * top-level statements missed every one of them, and each emitted a duplicate binding.
330
+ *
331
+ * Only statement containers are followed. A function or class body opens a new variable
332
+ * scope, so a `var` inside one cannot collide with a module-level import.
333
+ */
334
+ const addHoistedVars = (node) => {
335
+ if (isVar(node)) {
336
+ addDeclarations(node);
337
+ return;
338
+ }
339
+ if (ts_morph.Node.isBlock(node)) {
340
+ for (const statement of node.getStatements()) addHoistedVars(statement);
341
+ return;
342
+ }
343
+ if (ts_morph.Node.isIfStatement(node)) {
344
+ addHoistedVars(node.getThenStatement());
345
+ const otherwise = node.getElseStatement();
346
+ if (otherwise) addHoistedVars(otherwise);
347
+ return;
348
+ }
349
+ if (ts_morph.Node.isForStatement(node)) {
350
+ const initializer = node.getInitializer();
351
+ if (initializer) addHoistedVars(initializer);
352
+ addHoistedVars(node.getStatement());
353
+ return;
354
+ }
355
+ if (ts_morph.Node.isForInStatement(node) || ts_morph.Node.isForOfStatement(node)) {
356
+ addHoistedVars(node.getInitializer());
357
+ addHoistedVars(node.getStatement());
358
+ return;
359
+ }
360
+ if (ts_morph.Node.isWhileStatement(node) || ts_morph.Node.isDoStatement(node) || ts_morph.Node.isWithStatement(node)) {
361
+ addHoistedVars(node.getStatement());
362
+ return;
363
+ }
364
+ if (ts_morph.Node.isLabeledStatement(node)) {
365
+ addHoistedVars(node.getStatement());
366
+ return;
367
+ }
368
+ if (ts_morph.Node.isTryStatement(node)) {
369
+ addHoistedVars(node.getTryBlock());
370
+ const caught = node.getCatchClause();
371
+ if (caught) addHoistedVars(caught.getBlock());
372
+ const finally_ = node.getFinallyBlock();
373
+ if (finally_) addHoistedVars(finally_);
374
+ return;
375
+ }
376
+ if (ts_morph.Node.isSwitchStatement(node)) for (const clause of node.getCaseBlock().getClauses()) for (const statement of clause.getStatements()) addHoistedVars(statement);
377
+ };
378
+ for (const statement of sourceFile.getStatements()) {
379
+ if (ts_morph.Node.isVariableStatement(statement)) {
380
+ addDeclarations(statement);
381
+ continue;
382
+ }
383
+ if (ts_morph.Node.isImportDeclaration(statement)) {
384
+ addBinding(statement.getDefaultImport());
385
+ addBinding(statement.getNamespaceImport());
386
+ for (const named of statement.getNamedImports()) addBinding(named.getAliasNode() ?? named.getNameNode());
387
+ continue;
388
+ }
389
+ if (ts_morph.Node.isImportEqualsDeclaration(statement)) {
390
+ addBinding(statement.getNameNode());
391
+ continue;
392
+ }
393
+ if (ts_morph.Node.isFunctionDeclaration(statement) || ts_morph.Node.isClassDeclaration(statement) || ts_morph.Node.isEnumDeclaration(statement) || ts_morph.Node.isModuleDeclaration(statement) || ts_morph.Node.isTypeAliasDeclaration(statement) || ts_morph.Node.isInterfaceDeclaration(statement)) {
394
+ addBinding(statement.getNameNode());
395
+ continue;
396
+ }
397
+ addHoistedVars(statement);
398
+ }
399
+ return names;
400
+ };
401
+ /**
402
+ * Every identifier in the module, grouped by the name it spells.
403
+ *
404
+ * Built once per pass and handed to each lookup, because walking the whole tree per binding
405
+ * made that O(bindings x identifiers): a module declaring ten recipes walked its identifiers
406
+ * ten times.
407
+ *
408
+ * ## Why this is a raw walk rather than `getDescendantsOfKind`
409
+ *
410
+ * `SyntaxKind.Identifier` sorts *below* `SyntaxKind.FirstNode`, which is what ts-morph tests to
411
+ * decide whether it may search the parse tree. For a kind below that line it falls back to
412
+ * materialising the whole **token** tree — every brace, comma and keyword becomes a ts-morph node
413
+ * on the way to collecting the identifiers. On 55 KB of real tsx that measured 22ms against
414
+ * 0.22ms for the same collection over compiler nodes, and the node cache does not help: a second
415
+ * call cost the same 22ms.
416
+ *
417
+ * Wrapping is what costs, so only the buckets a caller actually reads are wrapped, on the first
418
+ * read and cached after. Nothing enumerates this index — both callers ask for one name — so the
419
+ * rest is never built. `_getNodeFromCompilerNode` is ts-morph's own memoized wrapper factory, so
420
+ * a node handed back here is the very object `getDescendantsOfKind` would have returned, which
421
+ * `localReferencesTo` depends on: it compares against the declaration by identity.
422
+ *
423
+ * JSDoc is walked explicitly. `ts.forEachChild` does not descend into it, while the token path
424
+ * this replaces does, so a name mentioned only in a `@type` annotation was previously found and
425
+ * would otherwise stop being — a silent narrowing of what counts as a surviving reference.
426
+ * Keyed on `escapedText` because that is what ts-morph's `Identifier.getText()` returns: the name
427
+ * as the compiler resolves it, so `\u0062adge` and `badge` share a bucket exactly as before.
428
+ *
429
+ * Deliberately *not* memoized across passes, unlike the module-scope names beside it. That
430
+ * cache holds strings, which outlive anything; this one holds nodes, and a node does not
431
+ * survive its source file being replaced — `addSourceFile` overwrites, which forgets every
432
+ * node previously taken from it. Keying on the source text does not help, because identical
433
+ * text re-parsed is a fresh tree: the cache hits and returns nodes that throw
434
+ * `Attempted to get information from a node that was removed or forgotten` on the next read.
435
+ */
436
+ const identifierIndex = (sourceFile) => {
437
+ const compilerNodes = /* @__PURE__ */ new Map();
438
+ const collect = (node) => {
439
+ if (node.kind === ts_morph.ts.SyntaxKind.Identifier) {
440
+ const name = String(node.escapedText);
441
+ const known = compilerNodes.get(name);
442
+ if (known) known.push(node);
443
+ else compilerNodes.set(name, [node]);
444
+ }
445
+ const jsDoc = node.jsDoc;
446
+ if (jsDoc) for (const doc of jsDoc) collect(doc);
447
+ ts_morph.ts.forEachChild(node, collect);
448
+ };
449
+ ts_morph.ts.forEachChild(sourceFile.compilerNode, collect);
450
+ const wrapped = /* @__PURE__ */ new Map();
451
+ const wrap = sourceFile;
452
+ return { get: (name) => {
453
+ const known = wrapped.get(name);
454
+ if (known) return known;
455
+ const nodes = (compilerNodes.get(name) ?? []).map((node) => wrap._getNodeFromCompilerNode(node));
456
+ wrapped.set(name, nodes);
457
+ return nodes;
458
+ } };
459
+ };
460
+ const localReferencesTo = (index, name, declaration) => {
461
+ const references = [];
462
+ for (const identifier of index.get(name)) {
463
+ if (identifier === declaration) continue;
464
+ const parent = identifier.getParent();
465
+ if (!parent) continue;
466
+ if (ts_morph.Node.isPropertyAccessExpression(parent) && parent.getNameNode() === identifier) continue;
467
+ if (ts_morph.Node.isPropertyAssignment(parent) && parent.getNameNode() === identifier) continue;
468
+ if (ts_morph.Node.isBindingElement(parent) && parent.getPropertyNameNode() === identifier) continue;
469
+ if ((ts_morph.Node.isJsxOpeningElement(parent) || ts_morph.Node.isJsxSelfClosingElement(parent) || ts_morph.Node.isJsxClosingElement(parent)) && parent.getTagNameNode() === identifier) continue;
470
+ if (ts_morph.Node.isJsxAttribute(parent) && parent.getNameNode() === identifier) continue;
471
+ if ((ts_morph.Node.isMethodDeclaration(parent) || ts_morph.Node.isPropertyDeclaration(parent) || ts_morph.Node.isGetAccessorDeclaration(parent) || ts_morph.Node.isSetAccessorDeclaration(parent) || ts_morph.Node.isMethodSignature(parent) || ts_morph.Node.isPropertySignature(parent) || ts_morph.Node.isEnumMember(parent)) && parent.getNameNode() === identifier) continue;
472
+ if ((ts_morph.Node.isVariableDeclaration(parent) || ts_morph.Node.isParameterDeclaration(parent) || ts_morph.Node.isFunctionDeclaration(parent) || ts_morph.Node.isClassDeclaration(parent) || ts_morph.Node.isBindingElement(parent)) && parent.getNameNode() === identifier) continue;
473
+ if (ts_morph.Node.isImportSpecifier(parent) || ts_morph.Node.isExportSpecifier(parent)) {
474
+ if (parent.getNameNode() !== identifier) continue;
475
+ if (ts_morph.Node.isExportSpecifier(parent)) {
476
+ references.push(identifier);
477
+ continue;
478
+ }
479
+ continue;
480
+ }
481
+ references.push(identifier);
482
+ }
483
+ return references;
484
+ };
485
+ //#endregion
486
+ //#region src/fold-recipe.ts
487
+ /**
488
+ * Binding name → the config it was declared with.
489
+ *
490
+ * Built from the definitions the parser already recorded, walking each one to the declaration
491
+ * that names it. The parser records a definition under the name it was *imported* as (`cva`),
492
+ * and a call under the name the file *bound* (`badge`); this is what joins the two.
493
+ *
494
+ * Slot and ordinary recipes share one representation.
495
+ */
496
+ const collectRecipeConfigs = (parserResult) => {
497
+ const configs = /* @__PURE__ */ new Map();
498
+ const definitions = [...parserResult.cva, ...parserResult.sva];
499
+ for (const definition of definitions) {
500
+ const node = definition.box?.getNode?.();
501
+ if (!node) continue;
502
+ const nameNode = ((ts_morph.Node.isCallExpression(node) ? node : node.getFirstAncestorByKind(ts_morph.SyntaxKind.CallExpression))?.getFirstAncestorByKind(ts_morph.SyntaxKind.VariableDeclaration))?.getNameNode();
503
+ if (!nameNode || !ts_morph.Node.isIdentifier(nameNode)) continue;
504
+ if (definition.data?.length !== 1) {
505
+ configs.set(nameNode.getText(), AMBIGUOUS);
506
+ continue;
507
+ }
508
+ const config = definition.data[0];
509
+ if (!config || typeof config !== "object") continue;
510
+ if (configs.has(nameNode.getText())) {
511
+ configs.set(nameNode.getText(), AMBIGUOUS);
512
+ continue;
513
+ }
514
+ configs.set(nameNode.getText(), {
515
+ config,
516
+ box: definition.box
517
+ });
518
+ }
519
+ return configs;
520
+ };
521
+ /** Pick a complete precompiled StyleSet for one or more runtime recipe axes. */
522
+ const RECIPE_MAP_HELPER = "cvaMap";
523
+ /** Guard the exact compiler against accidentally materialising an enormous Cartesian product. */
524
+ const DEFAULT_MAX_RECIPE_STATES = 65536;
525
+ /** What `recipe.splitVariantProps` calls, reached directly once the call is lowered. */
526
+ const SPLIT_PROPS_HELPER = "splitProps";
527
+ /** Marker for a binding the fold must never resolve — declared twice, or unresolvable. */
528
+ const AMBIGUOUS = Object.freeze({
529
+ config: {},
530
+ box: void 0
531
+ });
532
+ /** `.size`, or `["x-large"]` when the variant is not a valid identifier. */
533
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
534
+ const propertyAccess = (key) => IDENTIFIER.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
535
+ const LITERAL_KINDS = new Set([
536
+ ts_morph.SyntaxKind.StringLiteral,
537
+ ts_morph.SyntaxKind.NoSubstitutionTemplateLiteral,
538
+ ts_morph.SyntaxKind.NumericLiteral,
539
+ ts_morph.SyntaxKind.TrueKeyword,
540
+ ts_morph.SyntaxKind.FalseKeyword
541
+ ]);
542
+ /**
543
+ * The value a literal node denotes, or `undefined` for anything else.
544
+ *
545
+ * Read off the node rather than from the extractor's resolved data, because that data is lossy
546
+ * in the direction that matters: a property it could not resolve is *dropped*, so `badge({ tone })`
547
+ * and `badge({})` are identical there. Folding the first as if it were the second emits a class
548
+ * string missing the variant — the element renders, wrongly, with no report.
549
+ */
550
+ const literalValue = (node) => {
551
+ if (!node || !LITERAL_KINDS.has(node.getKind())) return void 0;
552
+ if (ts_morph.Node.isStringLiteral(node) || ts_morph.Node.isNoSubstitutionTemplateLiteral(node)) return node.getLiteralValue();
553
+ if (ts_morph.Node.isNumericLiteral(node)) return node.getLiteralValue();
554
+ if (node.getKind() === ts_morph.SyntaxKind.TrueKeyword) return true;
555
+ if (node.getKind() === ts_morph.SyntaxKind.FalseKeyword) return false;
556
+ };
557
+ /**
558
+ * The property name a key node denotes.
559
+ *
560
+ * Read off the node rather than unquoted from its text. `{ '\\u0074one': 'a' }` names the
561
+ * variant `tone`, and stripping the surrounding quotes leaves the escape uninterpreted — so
562
+ * the variant did not match, its class was dropped, and the element rendered without it. A
563
+ * numeric key normalises the same way: `{ 0x10: 'a' }` is the key `16`.
564
+ */
565
+ const propertyKey = (nameNode) => {
566
+ if (ts_morph.Node.isIdentifier(nameNode)) return nameNode.getText();
567
+ if (ts_morph.Node.isStringLiteral(nameNode) || ts_morph.Node.isNoSubstitutionTemplateLiteral(nameNode)) return nameNode.getLiteralValue();
568
+ if (ts_morph.Node.isNumericLiteral(nameNode)) return String(nameNode.getLiteralValue());
569
+ };
570
+ /**
571
+ * Make a generated compile helper callable at this call site, by whatever name the file gives it.
572
+ *
573
+ * Unlike `cx`, an inline recipe's callee is a local binding, so
574
+ * there is nothing to match — the host here is any import of the generated css module, which
575
+ * a file defining a recipe necessarily has, since `cva` came from it.
576
+ */
577
+ const ensureRecipeHelperImport = (imported, call, isBambooCssModule, isGeneratedCssModule, isShadowed, newImportModule, helperModuleFromSubpath) => {
578
+ const sourceFile = call.getSourceFile();
579
+ let host;
580
+ let subpathModule;
581
+ for (const declaration of sourceFile.getImportDeclarations()) {
582
+ const mod = declaration.getModuleSpecifierValue();
583
+ if (declaration.isTypeOnly()) continue;
584
+ for (const named of declaration.getNamedImports()) {
585
+ if (named.isTypeOnly()) continue;
586
+ if (named.getNameNode().getText() === imported) {
587
+ if (!isBambooCssModule(mod)) return void 0;
588
+ const local = (named.getAliasNode() ?? named.getNameNode()).getText();
589
+ return isShadowed(call, local) ? void 0 : { name: local };
590
+ }
591
+ }
592
+ if (!host && isGeneratedCssModule(mod) && declaration.getNamedImports().length > 0) host = declaration;
593
+ if (!subpathModule) subpathModule = helperModuleFromSubpath?.(mod);
594
+ }
595
+ const fallbackModule = newImportModule ?? subpathModule;
596
+ if (!host && !fallbackModule) return void 0;
597
+ if (declaredAtModuleScope(sourceFile).has(imported)) return void 0;
598
+ if (isShadowed(call, imported)) return void 0;
599
+ if (!host) {
600
+ const anchor = sourceFile.getImportDeclarations().at(-1);
601
+ if (!anchor) return void 0;
602
+ return {
603
+ name: imported,
604
+ insert: {
605
+ pos: anchor.getEnd(),
606
+ names: [imported],
607
+ module: fallbackModule
608
+ }
609
+ };
610
+ }
611
+ const last = host.getNamedImports().at(-1);
612
+ if (!last) return void 0;
613
+ return {
614
+ name: imported,
615
+ insert: {
616
+ pos: last.getEnd(),
617
+ names: [imported]
618
+ }
619
+ };
620
+ };
621
+ /**
622
+ * Lower one invocation, or say why not.
623
+ *
624
+ * Every property written at the call site has to be a literal. A selection is not additive —
625
+ * an unresolved variant does not merely omit a class, it can change which of several the
626
+ * recipe applies — so a partially-known selection is not foldable at all.
627
+ */
628
+ const lowerRecipeCall = (call, entry, styleCompiler, isInert, resolvedSelection, slot, maxRecipeStates = DEFAULT_MAX_RECIPE_STATES) => {
629
+ if (!entry || entry === AMBIGUOUS) return {
630
+ kind: "decline",
631
+ reason: "unknown-recipe"
632
+ };
633
+ const { config } = entry;
634
+ if (config.slots !== void 0) {
635
+ if (!Array.isArray(config.slots) || slot !== void 0 && !config.slots.includes(slot)) return {
636
+ kind: "decline",
637
+ reason: "unsupported-shape"
638
+ };
639
+ } else if (slot) return {
640
+ kind: "decline",
641
+ reason: "unsupported-shape"
642
+ };
643
+ if (!config.base && !config.variants && !config.className) return {
644
+ kind: "decline",
645
+ reason: "unknown-recipe"
646
+ };
647
+ if (!ts_morph.Node.isCallExpression(call)) return {
648
+ kind: "decline",
649
+ reason: "unsupported-shape"
650
+ };
651
+ const args = call.getArguments();
652
+ if (args.length > 1) return {
653
+ kind: "decline",
654
+ reason: "unsupported-shape"
655
+ };
656
+ const selection = {};
657
+ /** Variant → the source expression selecting it, for axes that stay runtime decisions. */
658
+ const dynamicAxes = /* @__PURE__ */ new Map();
659
+ /**
660
+ * Variants whose expression could run something, in the order the source evaluates them.
661
+ *
662
+ * The text is kept, not just the key: a later property writing the same key replaces the
663
+ * entry in `dynamicAxes`, and the expression recorded here would then never be emitted.
664
+ */
665
+ const effectful = [];
666
+ if (args.length === 1) {
667
+ const arg = args[0];
668
+ if (!arg) return {
669
+ kind: "decline",
670
+ reason: "dynamic"
671
+ };
672
+ /**
673
+ * `input(variantProps)` — a selection the build cannot see inside.
674
+ *
675
+ * The compiled recipe contract accepts scalar declared variant values. A conditional
676
+ * object is not a finite selection value; responsiveness belongs inside a variant's style
677
+ * declaration, where the compiler can materialize its conditions ahead of time.
678
+ *
679
+ * The complete StyleSets are knowable: the config declares every scalar value each axis
680
+ * accepts. This is the shape a wrapper component takes, where variants are its public API
681
+ * and therefore cannot be literals by definition.
682
+ *
683
+ * An identifier only. Each variant reads the binding again, and re-reading anything else —
684
+ * a call, a property access — would evaluate it once per axis instead of once.
685
+ */
686
+ if (ts_morph.Node.isIdentifier(arg)) {
687
+ const binding = arg.getText();
688
+ for (const key of Object.keys(config.variants ?? {})) dynamicAxes.set(key, `${binding}${propertyAccess(key)}`);
689
+ } else if (!ts_morph.Node.isObjectLiteralExpression(arg)) return {
690
+ kind: "decline",
691
+ reason: "dynamic"
692
+ };
693
+ else for (const property of arg.getProperties()) {
694
+ if (ts_morph.Node.isSpreadAssignment(property)) return {
695
+ kind: "decline",
696
+ reason: "dynamic"
697
+ };
698
+ if (ts_morph.Node.isShorthandPropertyAssignment(property)) {
699
+ dynamicAxes.set(property.getName(), property.getName());
700
+ delete selection[property.getName()];
701
+ continue;
702
+ }
703
+ if (!ts_morph.Node.isPropertyAssignment(property)) return {
704
+ kind: "decline",
705
+ reason: "dynamic"
706
+ };
707
+ const nameNode = property.getNameNode();
708
+ if (ts_morph.Node.isComputedPropertyName(nameNode)) return {
709
+ kind: "decline",
710
+ reason: "dynamic"
711
+ };
712
+ const key = propertyKey(nameNode);
713
+ if (key === void 0) return {
714
+ kind: "decline",
715
+ reason: "dynamic"
716
+ };
717
+ const initializer = property.getInitializer();
718
+ if (initializer && !isInert(initializer)) {
719
+ if (!Object.hasOwn(config.variants ?? {}, key)) return {
720
+ kind: "decline",
721
+ reason: "dynamic"
722
+ };
723
+ effectful.push({
724
+ key,
725
+ text: initializer.getText()
726
+ });
727
+ dynamicAxes.set(key, initializer.getText());
728
+ delete selection[key];
729
+ continue;
730
+ }
731
+ const literal = literalValue(initializer);
732
+ if (literal !== void 0) {
733
+ selection[key] = literal;
734
+ dynamicAxes.delete(key);
735
+ continue;
736
+ }
737
+ if (!resolvedSelection || !Object.hasOwn(resolvedSelection, key)) {
738
+ if (!initializer) return {
739
+ kind: "decline",
740
+ reason: "dynamic"
741
+ };
742
+ dynamicAxes.set(key, initializer.getText());
743
+ delete selection[key];
744
+ continue;
745
+ }
746
+ const value = resolvedSelection[key];
747
+ if (value !== null && typeof value === "object") return {
748
+ kind: "decline",
749
+ reason: "dynamic"
750
+ };
751
+ selection[key] = value;
752
+ dynamicAxes.delete(key);
753
+ }
754
+ }
755
+ /**
756
+ * Every expression that could run something has to reach the output carrying its own text.
757
+ *
758
+ * A later property writing the same key replaces it in `dynamicAxes` — `badge({ tone: a(),
759
+ * tone: 'b' })` is last-wins for the *value*, but `a()` still runs, and emitting only the
760
+ * literal would delete it. Duplicate keys are a type error in TypeScript; the fold does not
761
+ * typecheck and does transform `.js`, so this is reachable.
762
+ */
763
+ const everyEffectSurvives = () => effectful.every(({ key, text }) => dynamicAxes.get(key) === text);
764
+ const compiledSelection = (selected) => {
765
+ if (Array.isArray(config.slots) && slot === void 0) {
766
+ const slots = {};
767
+ const classNames = /* @__PURE__ */ new Set();
768
+ for (const slotName of config.slots) {
769
+ const styles = styleCompiler.resolveRecipe(config, selected, slotName);
770
+ if (!styles) return void 0;
771
+ const className = styleCompiler.className(styles);
772
+ slots[slotName] = className;
773
+ for (const token of className.split(" ")) if (token) classNames.add(token);
774
+ }
775
+ return {
776
+ value: slots,
777
+ classNames: [...classNames]
778
+ };
779
+ }
780
+ const styles = styleCompiler.resolveRecipe(config, selected, slot);
781
+ if (!styles) return void 0;
782
+ const className = styleCompiler.className(styles);
783
+ return {
784
+ value: className,
785
+ classNames: className.split(" ").filter(Boolean),
786
+ styles
787
+ };
788
+ };
789
+ if (dynamicAxes.size === 0) {
790
+ if (!everyEffectSurvives()) return {
791
+ kind: "decline",
792
+ reason: "dynamic"
793
+ };
794
+ const compiled = compiledSelection(selection);
795
+ if (!compiled) return {
796
+ kind: "decline",
797
+ reason: "dynamic"
798
+ };
799
+ if (typeof compiled.value === "string") return {
800
+ kind: "class",
801
+ className: compiled.value,
802
+ styles: compiled.styles
803
+ };
804
+ return {
805
+ kind: "slots",
806
+ expression: JSON.stringify(compiled.value),
807
+ classNames: compiled.classNames,
808
+ dynamic: false
809
+ };
810
+ }
811
+ if (!everyEffectSurvives()) return {
812
+ kind: "decline",
813
+ reason: "dynamic"
814
+ };
815
+ if (effectful.length > 1) {
816
+ const variantOrder = Object.keys(config.variants ?? {});
817
+ const keys = effectful.map((entry) => entry.key);
818
+ if ([...keys].sort((a, b) => variantOrder.indexOf(a) - variantOrder.indexOf(b)).join("\0") !== keys.join("\0")) return {
819
+ kind: "decline",
820
+ reason: "dynamic"
821
+ };
822
+ }
823
+ for (const key of [...dynamicAxes.keys()]) if (!Object.hasOwn(config.variants ?? {}, key)) dynamicAxes.delete(key);
824
+ if (dynamicAxes.size === 0) {
825
+ const compiled = compiledSelection(selection);
826
+ if (!compiled) return {
827
+ kind: "decline",
828
+ reason: "dynamic"
829
+ };
830
+ if (typeof compiled.value === "string") return {
831
+ kind: "class",
832
+ className: compiled.value,
833
+ styles: compiled.styles
834
+ };
835
+ return {
836
+ kind: "slots",
837
+ expression: JSON.stringify(compiled.value),
838
+ classNames: compiled.classNames,
839
+ dynamic: false
840
+ };
841
+ }
842
+ /**
843
+ * Compile the finite recipe state space into a reduced decision table.
844
+ *
845
+ * Each leaf is a *complete* final StyleSet. This matters for declarations overridden by
846
+ * variants and compounds: selecting independent per-axis atoms would put both values in
847
+ * the utility layer and let stylesheet order, rather than the recipe's merge order, pick
848
+ * the winner. Complete leaves retain the same precedence while sharing their atoms with
849
+ * every `css()` and recipe in the build.
850
+ *
851
+ * `undefined` is its own edge because it restores a default variant. `null` and any
852
+ * undeclared value take the miss edge and explicitly suppress that default. Declared
853
+ * values use string keys, matching JavaScript's property-key coercion in the recipe
854
+ * runtime. A flat alternating key/value array avoids the special `__proto__` semantics
855
+ * of an object literal.
856
+ */
857
+ const axes = Object.keys(config.variants ?? {}).filter((key) => dynamicAxes.has(key));
858
+ const stateCount = axes.reduce((product, axis) => product * (Object.keys(config.variants?.[axis] ?? {}).length + 2), 1);
859
+ if (stateCount > maxRecipeStates) throw new Error(`Static recipe compilation would inspect ${stateCount.toLocaleString("en-US")} selections across ${axes.length} runtime variant axes, above maxRecipeStates=${maxRecipeStates.toLocaleString("en-US")}. Make one or more axes statically known, split the recipe, or raise the limit explicitly.`);
860
+ const expressions = axes.map((axis) => dynamicAxes.get(axis));
861
+ const wholeSlots = Array.isArray(config.slots) && slot === void 0;
862
+ return {
863
+ kind: "dynamic-style",
864
+ map: {
865
+ outputKind: wholeSlots ? "slots" : "class",
866
+ compile(before = [], after = []) {
867
+ const nodes = [];
868
+ const nodeByShape = /* @__PURE__ */ new Map();
869
+ const leaves = [];
870
+ const leafByShape = /* @__PURE__ */ new Map();
871
+ const emittedClasses = /* @__PURE__ */ new Set();
872
+ const leaf = (dynamicSelection) => {
873
+ const selected = {
874
+ ...selection,
875
+ ...dynamicSelection
876
+ };
877
+ if (wholeSlots) {
878
+ const compiled = compiledSelection(selected);
879
+ if (!compiled || typeof compiled.value === "string") return internLeaf("");
880
+ for (const token of compiled.classNames) emittedClasses.add(token);
881
+ return internLeaf(compiled.value);
882
+ }
883
+ const styles = styleCompiler.resolveRecipe(config, selected, slot);
884
+ if (!styles) return internLeaf("");
885
+ const className = styleCompiler.className(styleCompiler.compose(...before, styles, ...after));
886
+ for (const token of className.split(" ")) if (token) emittedClasses.add(token);
887
+ return internLeaf(className);
888
+ };
889
+ function internLeaf(value) {
890
+ const shape = JSON.stringify(value);
891
+ const known = leafByShape.get(shape);
892
+ if (known !== void 0) return ~known;
893
+ const id = leaves.length;
894
+ leaves.push(value);
895
+ leafByShape.set(shape, id);
896
+ return ~id;
897
+ }
898
+ const buildNode = (index, dynamicSelection) => {
899
+ if (index === axes.length) return leaf(dynamicSelection);
900
+ const axis = axes[index];
901
+ const values = Object.keys(config.variants?.[axis] ?? {});
902
+ const miss = buildNode(index + 1, {
903
+ ...dynamicSelection,
904
+ [axis]: null
905
+ });
906
+ const absentSelection = { ...dynamicSelection };
907
+ delete absentSelection[axis];
908
+ const absent = buildNode(index + 1, absentSelection);
909
+ const byValue = [];
910
+ for (const value of values) byValue.push(value, buildNode(index + 1, {
911
+ ...dynamicSelection,
912
+ [axis]: value
913
+ }));
914
+ const refs = [
915
+ miss,
916
+ absent,
917
+ ...byValue.filter((_, valueIndex) => valueIndex % 2 === 1)
918
+ ];
919
+ if (refs.every((ref) => ref === refs[0])) return refs[0];
920
+ const node = [
921
+ miss,
922
+ absent,
923
+ byValue
924
+ ];
925
+ const shape = JSON.stringify(node);
926
+ const known = nodeByShape.get(shape);
927
+ if (known !== void 0) return known;
928
+ const id = nodes.length;
929
+ nodes.push(node);
930
+ nodeByShape.set(shape, id);
931
+ return id;
932
+ };
933
+ const root = buildNode(0, {});
934
+ const staticLeaf = root < 0 ? leaves[~root] : void 0;
935
+ return {
936
+ expression: root < 0 && effectful.length === 0 ? JSON.stringify(staticLeaf) : `${RECIPE_MAP_HELPER}([${expressions.join(", ")}], ${JSON.stringify(nodes)}, ${JSON.stringify(leaves)}, ${root})`,
937
+ classNames: [...emittedClasses],
938
+ staticClasses: typeof staticLeaf === "string" ? staticLeaf : "",
939
+ outputKind: wholeSlots ? "slots" : "class",
940
+ usesHelper: !(root < 0 && effectful.length === 0)
941
+ };
942
+ }
943
+ }
944
+ };
945
+ };
946
+ //#endregion
947
+ //#region src/runtime-css.ts
948
+ /** The shape `createCss` and `createMergeCss` both take, derived from a resolved context. */
949
+ const createCssContext = (ctx) => ({
950
+ hash: Boolean(ctx.hash.className),
951
+ conditions: {
952
+ shift: ctx.conditions.shift,
953
+ finalize: ctx.conditions.finalize
954
+ },
955
+ utility: {
956
+ prefix: ctx.utility.prefix,
957
+ hasShorthand: ctx.utility.hasShorthand,
958
+ resolveShorthand: ctx.utility.resolveShorthand.bind(ctx.utility),
959
+ transform: ctx.utility.transform.bind(ctx.utility),
960
+ toHash: ctx.utility.toHash.bind(ctx.utility)
961
+ }
962
+ });
963
+ const createRuntimeCss = (ctx) => {
964
+ const cssContext = createCssContext(ctx);
965
+ const cssFn = (0, _bamboocss_shared.createCssUncached)(cssContext);
966
+ const { mergeCssUncached } = (0, _bamboocss_shared.createMergeCss)(cssContext);
967
+ return (0, _bamboocss_shared.memo)((...styles) => cssFn(mergeCssUncached(...styles)));
968
+ };
969
+ /**
970
+ * The map is every token in the project, so it is built once per context and shared by
971
+ * every module in the build — not once per `foldSource`, which would price a whole token
972
+ * table into each of the overwhelming majority of modules that call `token()` zero times.
973
+ * Keyed weakly so a context that goes out of scope takes its table with it.
974
+ *
975
+ * Both halves of the generated entry are stored, because `token()` and `token.value()` read
976
+ * different ones and building a second table would pay the same per-project cost twice.
977
+ */
978
+ const tokenValues = /* @__PURE__ */ new WeakMap();
979
+ const tokenValuesFor = (ctx) => {
980
+ let values = tokenValues.get(ctx);
981
+ if (values) return values;
982
+ values = /* @__PURE__ */ new Map();
983
+ for (const token of ctx.tokens.allTokens) {
984
+ const { varRef, isVirtual, condition } = token.extensions;
985
+ values.set(token.name, {
986
+ value: ctx.tokens.view.get(token.name) ?? (isVirtual || condition !== "base" ? varRef : token.value),
987
+ variable: ctx.tokens.view.getVar(token.name) ?? varRef
988
+ });
989
+ }
990
+ tokenValues.set(ctx, values);
991
+ return values;
992
+ };
993
+ const createRuntimeTokenValue = (ctx) => (path) => {
994
+ const value = tokenValuesFor(ctx).get(path)?.value;
995
+ return typeof value === "string" ? value : void 0;
996
+ };
997
+ /**
998
+ * The generated runtime's `token()`, rebuilt in-process.
999
+ *
1000
+ * Reads the `variable` half of the same entry `token.value()` reads the `value` half of.
1001
+ * That half is `varRef` for every token regardless of condition, so unlike
1002
+ * `createRuntimeTokenValue` there is no split to get wrong and no non-string case to
1003
+ * decline: a `var()` reference is a string or the token does not exist. Which is what makes
1004
+ * the default form the trivially foldable one.
1005
+ */
1006
+ const createRuntimeToken = (ctx) => (path) => tokenValuesFor(ctx).get(path)?.variable || void 0;
1007
+ //#endregion
1008
+ //#region src/fold.ts
1009
+ /**
1010
+ * `cva`/`sva` return a function, so their definitions are compile-time declarations rather
1011
+ * than class-producing calls; once their uses are lowered, the factory calls are erased.
1012
+ * `token` also resolves to no class, but it does resolve to a literal, so it compiles through
1013
+ * its own path rather than being declined outright. A static `viewTransition` bag resolves to
1014
+ * its extracted class and uses the ordinary class candidate path.
1015
+ *
1016
+ * Recipe invocations compile through `fold-recipe`. Inline calls
1017
+ * are recorded under the name the file bound; config calls arrive as `recipe`. Routing both
1018
+ * through one exact finite-state lowering keeps their selection contract identical.
1019
+ */
1020
+ const FOLDABLE_TYPES = new Set([
1021
+ "css",
1022
+ "pattern",
1023
+ "viewTransition"
1024
+ ]);
1025
+ /**
1026
+ * The skip reasons that leave a `css()`-family call in the output.
1027
+ *
1028
+ * `overlapping` is handled by the enclosing fold, and `not-imported` is somebody else's
1029
+ * function of the same name — neither leaves a call of ours.
1030
+ */
1031
+ const SURVIVES_TO_RUNTIME = new Set([
1032
+ "dynamic",
1033
+ "runtime-binding",
1034
+ "raw-call",
1035
+ "unsupported-kind",
1036
+ "no-call-expression",
1037
+ "unresolved-token",
1038
+ "compile-failed"
1039
+ ]);
1040
+ /**
1041
+ * A call of a recipe the file bound itself: `const badge = cva(...)`, then `badge({ tone })`.
1042
+ *
1043
+ * Folded when the whole selection resolves, reported under this reason when it does not.
1044
+ * Deliberately all-or-nothing: an unresolved variant does not merely omit its own class, so a
1045
+ * partially-known selection is not foldable at all.
1046
+ *
1047
+ * Visible at all because it used to not be. The parser matched calls by imported name, so a
1048
+ * local binding was never recorded, and an unfoldable invocation looked identical to code
1049
+ * nothing had parsed.
1050
+ */
1051
+ const RECIPE_CALL_TYPE = "cva-call";
1052
+ /**
1053
+ * An identifier that actually reads the binding.
1054
+ *
1055
+ * `getDescendantsOfKind(Identifier)` yields every name in the file, and most of them bind or
1056
+ * label rather than read: a JSX tag (`<button/>` against a recipe called `button`), an object
1057
+ * key, a property name, a declaration. Counting those failed builds on modules that had
1058
+ * folded completely — and `button`, `input`, `label`, `select`, `table`, `dialog` and `form`
1059
+ * are all ordinary recipe names as well as intrinsic elements.
1060
+ *
1061
+ * A type position is excluded for a different reason: it is erased, and with it the import.
1062
+ */
1063
+ const isValueReference = (identifier) => {
1064
+ const parent = identifier.getParent();
1065
+ if (!parent) return false;
1066
+ if (ts_morph.Node.isImportSpecifier(parent) || ts_morph.Node.isExportSpecifier(parent)) return false;
1067
+ if (ts_morph.Node.isImportClause(parent) || ts_morph.Node.isNamespaceImport(parent)) return false;
1068
+ if (ts_morph.Node.isPropertyAccessExpression(parent) && parent.getNameNode() === identifier) return false;
1069
+ if (ts_morph.Node.isQualifiedName(parent) && parent.getRight() === identifier) return false;
1070
+ if (ts_morph.Node.isPropertyAssignment(parent) && parent.getNameNode() === identifier) return false;
1071
+ if (ts_morph.Node.isMethodDeclaration(parent) || ts_morph.Node.isPropertyDeclaration(parent) || ts_morph.Node.isGetAccessorDeclaration(parent) || ts_morph.Node.isSetAccessorDeclaration(parent) || ts_morph.Node.isMethodSignature(parent) || ts_morph.Node.isPropertySignature(parent) || ts_morph.Node.isEnumMember(parent)) {
1072
+ if (parent.getNameNode() === identifier) return false;
1073
+ }
1074
+ if (ts_morph.Node.isLabeledStatement(parent) || ts_morph.Node.isBreakStatement(parent) || ts_morph.Node.isContinueStatement(parent)) return false;
1075
+ if (ts_morph.Node.isJsxOpeningElement(parent) || ts_morph.Node.isJsxSelfClosingElement(parent) || ts_morph.Node.isJsxClosingElement(parent)) {
1076
+ if (parent.getTagNameNode() === identifier) return identifier.getText()[0] === identifier.getText()[0]?.toUpperCase();
1077
+ }
1078
+ if (ts_morph.Node.isJsxAttribute(parent)) return false;
1079
+ if (ts_morph.Node.isBindingElement(parent) && parent.getPropertyNameNode() === identifier) return false;
1080
+ if ((ts_morph.Node.isVariableDeclaration(parent) || ts_morph.Node.isParameterDeclaration(parent) || ts_morph.Node.isBindingElement(parent) || ts_morph.Node.isFunctionDeclaration(parent) || ts_morph.Node.isClassDeclaration(parent)) && parent.getNameNode() === identifier) return false;
1081
+ return !identifier.getFirstAncestor((ancestor) => ts_morph.Node.isTypeNode(ancestor) || ts_morph.Node.isTypeAliasDeclaration(ancestor) || ts_morph.Node.isInterfaceDeclaration(ancestor));
1082
+ };
1083
+ /**
1084
+ * Imports a surviving reference to is not a failure.
1085
+ *
1086
+ * These are what the compiler itself writes; all live in `cx` and pull no engine, so a
1087
+ * reference to one is the fold having worked.
1088
+ */
1089
+ const PERMITTED_BINDINGS = new Set([
1090
+ "cx",
1091
+ RECIPE_MAP_HELPER,
1092
+ SPLIT_PROPS_HELPER
1093
+ ]);
1094
+ /**
1095
+ * Whether a module's text could hold a `splitVariantProps` property access.
1096
+ *
1097
+ * A necessary condition, deliberately not a sufficient one: the name inside a string or a
1098
+ * comment opens the walk, which costs what the walk always cost. What it must never do is
1099
+ * close on a module that has one, and an identifier may be spelled with unicode escapes —
1100
+ * `badge.splitVariantProps(p)` reads as the name to the compiler and contains none of it
1101
+ * as text. Both escape forms start `\u`, so one test covers every spelling.
1102
+ */
1103
+ const mayNameSplitVariantProps = (text) => text.includes("splitVariantProps") || text.includes("\\u");
1104
+ /**
1105
+ * The pieces `trim` reduces a module specifier by, hoisted because a regex literal
1106
+ * constructs a new object every time it is evaluated and `trim` runs per specifier per
1107
+ * import declaration per module.
1108
+ */
1109
+ const LEADING_RELATIVE = /^(?:\.\.?\/)+/;
1110
+ const TRAILING_SLASH = /\/$/;
1111
+ const MODULE_EXTENSION = /\.[mc]?[jt]sx?$/;
1112
+ const TRAILING_INDEX = /\/index$/;
1113
+ /**
1114
+ * An argument that cannot run anything when it is evaluated.
1115
+ *
1116
+ * `token()` takes one argument, but javascript evaluates every argument a call site passes
1117
+ * before the call — so a fold that drops an extra one also drops whatever evaluating it would
1118
+ * have done. `token('x', compute())` is pathological and no longer type-checks, and the fold's
1119
+ * contract is behaviour preservation regardless: a literal is the cheap way to prove it, since
1120
+ * it means no call, no property read, no getter.
1121
+ */
1122
+ const isInertArgument = (node) => ts_morph.Node.isStringLiteral(node) || ts_morph.Node.isNumericLiteral(node) || ts_morph.Node.isNoSubstitutionTemplateLiteral(node) || node.getKind() === ts_morph.SyntaxKind.TrueKeyword || node.getKind() === ts_morph.SyntaxKind.FalseKeyword || node.getKind() === ts_morph.SyntaxKind.NullKeyword || ts_morph.Node.isIdentifier(node) && node.getText() === "undefined";
1123
+ /**
1124
+ * An expression whose evaluation cannot do anything observable, so deleting it preserves
1125
+ * behaviour.
1126
+ *
1127
+ * `isInertArgument` covers the leaves; this walks the object and array literals a recipe
1128
+ * call is actually written with. A spread runs the source's getters, a computed key runs an
1129
+ * expression, a getter or method definition is a function — none of those are safe to
1130
+ * delete, so they are declined rather than enumerated.
1131
+ */
1132
+ const isInertExpression = (node) => {
1133
+ if (isInertArgument(node)) return true;
1134
+ if (ts_morph.Node.isIdentifier(node)) return true;
1135
+ if (ts_morph.Node.isAsExpression(node) || ts_morph.Node.isSatisfiesExpression(node) || ts_morph.Node.isNonNullExpression(node) || ts_morph.Node.isTypeAssertion(node) || ts_morph.Node.isParenthesizedExpression(node)) return isInertExpression(node.getExpression());
1136
+ if (ts_morph.Node.isArrowFunction(node) || ts_morph.Node.isFunctionExpression(node)) return true;
1137
+ if (ts_morph.Node.isRegularExpressionLiteral(node) || ts_morph.Node.isBigIntLiteral(node)) return true;
1138
+ if (ts_morph.Node.isPrefixUnaryExpression(node)) {
1139
+ const operator = node.getOperatorToken();
1140
+ return (operator === ts_morph.SyntaxKind.MinusToken || operator === ts_morph.SyntaxKind.PlusToken || operator === ts_morph.SyntaxKind.ExclamationToken || operator === ts_morph.SyntaxKind.TildeToken) && isInertExpression(node.getOperand());
1141
+ }
1142
+ if (ts_morph.Node.isBinaryExpression(node)) {
1143
+ const operator = node.getOperatorToken().getKind();
1144
+ return (operator === ts_morph.SyntaxKind.QuestionQuestionToken || operator === ts_morph.SyntaxKind.BarBarToken || operator === ts_morph.SyntaxKind.AmpersandAmpersandToken) && isInertExpression(node.getLeft()) && isInertExpression(node.getRight());
1145
+ }
1146
+ if (ts_morph.Node.isObjectLiteralExpression(node)) return node.getProperties().every((property) => {
1147
+ if (ts_morph.Node.isShorthandPropertyAssignment(property)) return true;
1148
+ if (!ts_morph.Node.isPropertyAssignment(property)) return false;
1149
+ if (ts_morph.Node.isComputedPropertyName(property.getNameNode())) return false;
1150
+ const initializer = property.getInitializer();
1151
+ return initializer !== void 0 && isInertExpression(initializer);
1152
+ });
1153
+ if (ts_morph.Node.isArrayLiteralExpression(node)) return node.getElements().every(isInertExpression);
1154
+ return false;
1155
+ };
1156
+ /**
1157
+ * Source files a box tree reaches, other than the one being folded.
1158
+ *
1159
+ * When the extractor resolves an imported identifier it boxes the *declaration's*
1160
+ * node, which lives in the defining module. Walking the tree and reading each node's
1161
+ * source file therefore recovers exactly the files a fold depended on — narrower and
1162
+ * more accurate than treating every import of the module as a dependency.
1163
+ */
1164
+ const collectSourceFiles = (node, ctx, seen = /* @__PURE__ */ new Set()) => {
1165
+ if (!node || seen.has(node)) return;
1166
+ seen.add(node);
1167
+ ctx.record(node.getNode?.());
1168
+ if (_bamboocss_extractor.box.isMap(node)) {
1169
+ for (const child of node.value.values()) collectSourceFiles(child, ctx, seen);
1170
+ return;
1171
+ }
1172
+ if (_bamboocss_extractor.box.isArray(node)) for (const child of node.value) collectSourceFiles(child, ctx, seen);
1173
+ };
1174
+ const createDependencyScan = (ownFile) => {
1175
+ const results = /* @__PURE__ */ new Set();
1176
+ const paths = /* @__PURE__ */ new Map();
1177
+ return {
1178
+ results,
1179
+ record(node) {
1180
+ if (!node) return;
1181
+ const sourceFile = node.getSourceFile();
1182
+ if (sourceFile === ownFile) return;
1183
+ let path = paths.get(sourceFile);
1184
+ if (path === void 0) {
1185
+ path = sourceFile.getFilePath();
1186
+ paths.set(sourceFile, path);
1187
+ }
1188
+ if (path) results.add(path);
1189
+ }
1190
+ };
1191
+ };
1192
+ /**
1193
+ * The call expression to replace.
1194
+ *
1195
+ * `extractCallExpressionArguments` boxes the argument list against the call node and
1196
+ * pushes `[callNode, argNode]` onto each argument's stack, so the call is reachable
1197
+ * from either shape the parser stores: the argument array (multi-arg) or the first
1198
+ * argument's map (single-arg).
1199
+ */
1200
+ const findCallExpression = (node) => {
1201
+ const own = node.getNode?.();
1202
+ if (own && ts_morph.Node.isCallExpression(own)) return own;
1203
+ const stack = node.getStack?.() ?? [];
1204
+ for (const entry of stack) if (ts_morph.Node.isCallExpression(entry)) return entry;
1205
+ let current = own;
1206
+ for (let depth = 0; current && depth < 3; depth++) {
1207
+ if (ts_morph.Node.isCallExpression(current)) return current;
1208
+ current = current.getParent();
1209
+ }
1210
+ };
1211
+ /**
1212
+ * `css.raw(...)` must keep returning a style object — folding it to a class string
1213
+ * breaks every caller composing those styles. The file matcher strips `.raw` when it
1214
+ * normalizes function names, so the parser result cannot tell us; the callee text can.
1215
+ */
1216
+ const isRawCall = (call) => {
1217
+ if (!ts_morph.Node.isCallExpression(call)) return false;
1218
+ const callee = call.getExpression().getText();
1219
+ return callee === "raw" || callee.endsWith(".raw");
1220
+ };
1221
+ /** The identifier a callee is rooted at: `css` for `css(…)`, `panda` for `panda.css(…)`. */
1222
+ const calleeRootName = (call) => {
1223
+ if (!ts_morph.Node.isCallExpression(call)) return void 0;
1224
+ let current = call.getExpression();
1225
+ while (ts_morph.Node.isPropertyAccessExpression(current)) current = current.getExpression();
1226
+ return ts_morph.Node.isIdentifier(current) ? current.getText() : void 0;
1227
+ };
1228
+ /**
1229
+ * Local names a module binds to an import of bamboo's own generated system.
1230
+ *
1231
+ * The parser matches by name and asks neither question this does — deliberately, since
1232
+ * for CSS extraction the worst case is a few unused rules. A transform cannot be that
1233
+ * relaxed, and it needs both halves:
1234
+ *
1235
+ * - imported at all, or a user's `const css = (s) => JSON.stringify(s)` gets rewritten
1236
+ * - imported *from bamboo*, or `import { css } from '@emotion/css'` does, which is the
1237
+ * likelier accident of the two since a migrating project has both in the tree
1238
+ *
1239
+ * Answered together and once per file, because the scan is the expensive part and both
1240
+ * answers fall out of the same pass. Per call site instead of per file, this scan
1241
+ * measured +74% on the largest sandbox module.
1242
+ */
1243
+ const bambooImportedNames = (sourceFile, ctx) => {
1244
+ const names = /* @__PURE__ */ new Set();
1245
+ for (const declaration of sourceFile.getImportDeclarations()) {
1246
+ const mod = declaration.getModuleSpecifierValue();
1247
+ for (const named of declaration.getNamedImports()) {
1248
+ const name = named.getNameNode().getText();
1249
+ const alias = named.getAliasNode()?.getText() ?? name;
1250
+ if (ctx.imports.match({
1251
+ mod,
1252
+ name,
1253
+ alias
1254
+ })) names.add(alias);
1255
+ }
1256
+ const namespace = declaration.getNamespaceImport();
1257
+ if (namespace) {
1258
+ const alias = namespace.getText();
1259
+ if (ctx.imports.match({
1260
+ mod,
1261
+ name: alias,
1262
+ alias,
1263
+ kind: "namespace"
1264
+ })) names.add(alias);
1265
+ }
1266
+ }
1267
+ return names;
1268
+ };
1269
+ /**
1270
+ * Is the callee the imported binding, or a local one that shadows it?
1271
+ *
1272
+ * A block-scoped binding of the same name is legal alongside the import, and it is
1273
+ * the one the call actually reaches. Walking ancestors is the precise answer; the
1274
+ * cost is kept off the common path by only inspecting the two node kinds that can
1275
+ * introduce a binding. Ancestors of a call in JSX are overwhelmingly elements and
1276
+ * attributes, which match neither and cost nothing.
1277
+ */
1278
+ const isShadowed = (call, name) => {
1279
+ for (let node = call.getParent(); node; node = node.getParent()) {
1280
+ if (ts_morph.Node.isSourceFile(node)) return false;
1281
+ if (bindsName(node, name)) return true;
1282
+ }
1283
+ return false;
1284
+ };
1285
+ /**
1286
+ * Does a binding name introduce `name`?
1287
+ *
1288
+ * A plain identifier check is not enough: destructuring is the likeliest way a
1289
+ * same-named local reaches a call, since `({ css }) => css(…)` is what a component
1290
+ * taking a `css` prop looks like. Nested and rest elements bind too, so the pattern
1291
+ * is walked rather than inspected at the top level.
1292
+ */
1293
+ const bindingIntroduces = (nameNode, name) => {
1294
+ if (!nameNode) return false;
1295
+ if (ts_morph.Node.isIdentifier(nameNode)) return nameNode.getText() === name;
1296
+ if (ts_morph.Node.isObjectBindingPattern(nameNode) || ts_morph.Node.isArrayBindingPattern(nameNode)) return nameNode.getElements().some((element) => ts_morph.Node.isBindingElement(element) && bindingIntroduces(element.getNameNode(), name));
1297
+ return false;
1298
+ };
1299
+ const declarationsBind = (list, name) => ts_morph.Node.isVariableDeclarationList(list) && list.getDeclarations().some((declaration) => bindingIntroduces(declaration.getNameNode(), name));
1300
+ const bindsName = (scope, name) => {
1301
+ if (ts_morph.Node.isBlock(scope)) return scope.getStatements().some((statement) => statementBinds(statement, name));
1302
+ if (ts_morph.Node.isFunctionDeclaration(scope) || ts_morph.Node.isArrowFunction(scope) || ts_morph.Node.isFunctionExpression(scope) || ts_morph.Node.isMethodDeclaration(scope)) return scope.getParameters().some((parameter) => bindingIntroduces(parameter.getNameNode(), name));
1303
+ if (ts_morph.Node.isCatchClause(scope)) return bindingIntroduces(scope.getVariableDeclaration()?.getNameNode(), name);
1304
+ if (ts_morph.Node.isForStatement(scope) || ts_morph.Node.isForOfStatement(scope) || ts_morph.Node.isForInStatement(scope)) return declarationsBind(scope.getInitializer(), name);
1305
+ return false;
1306
+ };
1307
+ const statementBinds = (statement, name) => {
1308
+ if (ts_morph.Node.isVariableStatement(statement)) return statement.getDeclarations().some((declaration) => bindingIntroduces(declaration.getNameNode(), name));
1309
+ if (ts_morph.Node.isFunctionDeclaration(statement) || ts_morph.Node.isClassDeclaration(statement)) return statement.getNameNode()?.getText() === name;
1310
+ return false;
1311
+ };
1312
+ const hasStyles = (data) => data.length > 0 && data.every((entry) => entry != null && typeof entry === "object");
1313
+ /**
1314
+ * Pair each source argument with the box the parser stored for it, and require the
1315
+ * box to account for all of it. The parser keeps either the whole argument array
1316
+ * (multi-arg calls) or just the first argument's map (single-arg calls).
1317
+ */
1318
+ const argumentsAccountedFor = (call, boxNode) => {
1319
+ if (!ts_morph.Node.isCallExpression(call)) return false;
1320
+ const args = call.getArguments();
1321
+ if (args.length === 0) return true;
1322
+ if (_bamboocss_extractor.box.isArray(boxNode) && boxNode.getNode() === call) {
1323
+ if (boxNode.value.length !== args.length) return false;
1324
+ return args.every((arg, index) => accountsForSource(arg, boxNode.value[index]));
1325
+ }
1326
+ if (args.length !== 1) return false;
1327
+ return accountsForSource(args[0], boxNode);
1328
+ };
1329
+ const foldSource = (options) => {
1330
+ const { ctx, code, parserResult, runtimeCss = createRuntimeCss(ctx), styleCompiler, maxRecipeStates, parseModule, recipeConfigCache, reportSurvivors, sourceFile: ownSourceFile } = options;
1331
+ const runtimeToken = createRuntimeToken(ctx);
1332
+ const runtimeTokenValue = createRuntimeTokenValue(ctx);
1333
+ /**
1334
+ * Does this specifier name a module that exports the css API, exactly?
1335
+ *
1336
+ * `ImportMap.match` is substring-based, which is right for deciding whether a call is
1337
+ * bamboo's and wrong for deciding whether a module can be imported *from*:
1338
+ * `styled-system/css/css` matches while exporting no `cx`. So the comparison is
1339
+ * equality, not containment.
1340
+ *
1341
+ * A tsconfig path alias is resolved first, the same way `ImportMap.match` does. Without
1342
+ * that, `@site/styled-system/css` — the spelling this repo's own website uses — fails
1343
+ * the check and silently loses helper lowering, which is indistinguishable in the
1344
+ * diagnostics from a genuinely dynamic call.
1345
+ */
1346
+ const cssModules = ctx.imports.matchers.css?.mods ?? [];
1347
+ /**
1348
+ * The generated css module, the only one whose exports are known.
1349
+ *
1350
+ * A configured `importMap.css` points at the user's own wrapper, and a wrapper that
1351
+ * re-exports `css` need not re-export `cx` — adding one there imports a binding that
1352
+ * may not exist. Reusing a `cx` the user already imported from it stays fine, since
1353
+ * that binding demonstrably resolves; only *adding* one is restricted.
1354
+ */
1355
+ const generatedCssModule = [ctx.imports.outdir, "css"].join("/");
1356
+ const pathMappings = ctx.conf.tsOptions?.pathMappings;
1357
+ /**
1358
+ * The spelling reduced to the module it names.
1359
+ *
1360
+ * The extension and `/index` are stripped because bamboo's own output makes a file
1361
+ * import them: `outExtension: 'js'` under NodeNext resolution is written
1362
+ * `styled-system/css/index.js`, which is neither equal to `styled-system/css` nor a
1363
+ * tail of it. Extraction admitted such a file anyway — `ImportMap.match` is
1364
+ * substring-based — so the call was folded while the *insert* was refused, and the
1365
+ * result was reported as `dynamic`: the same silent downgrade the alias case above
1366
+ * describes, reached through the extension instead.
1367
+ *
1368
+ * This does not weaken the equality the comment above insists on. `styled-system/css/css`
1369
+ * still names neither, because only a trailing `/index` is a module's own directory.
1370
+ *
1371
+ * `.d.ts` is deliberately not stripped. A declaration file exports no runtime binding, so
1372
+ * matching one would authorise inserting an import that resolves to nothing — and a value
1373
+ * import cannot name one anyway, which is what makes leaving it out free.
1374
+ */
1375
+ const trim = (value) => value.replaceAll("\\", "/").replace(LEADING_RELATIVE, "").replace(TRAILING_SLASH, "").replace(MODULE_EXTENSION, "").replace(TRAILING_INDEX, "");
1376
+ const matchesModule = (mod, entries) => {
1377
+ const candidates = [mod];
1378
+ if (pathMappings) {
1379
+ const resolved = (0, _bamboocss_config_ts_path.resolveTsPathPattern)(pathMappings, mod);
1380
+ if (resolved) candidates.push(resolved);
1381
+ }
1382
+ return candidates.some((candidate) => {
1383
+ const normalized = trim(candidate);
1384
+ return entries.some((entry) => {
1385
+ const target = trim(entry);
1386
+ return normalized === target || normalized.endsWith(`/${target}`);
1387
+ });
1388
+ });
1389
+ };
1390
+ const isBambooCssModule = (mod) => matchesModule(mod, cssModules);
1391
+ const isGeneratedCssModule = (mod) => matchesModule(mod, [generatedCssModule]);
1392
+ /**
1393
+ * Where the compiler's helpers can be imported from, for a file that imports the generated
1394
+ * css module by *subpath* rather than through its barrel.
1395
+ *
1396
+ * `styled-system/css/cva.js` is a real spelling, and it cannot host the helper: that module
1397
+ * exports `cva`, not `cvaMap`. Matching only the barrel meant no host was found and every
1398
+ * runtime recipe selection in the file declined — a failure whose reported reason said
1399
+ * nothing about import spelling, and whose suggested remedies all pointed elsewhere.
1400
+ *
1401
+ * The sibling `cx` module is what actually exports them. The prefix is verified against the
1402
+ * configured output before anything is derived, so an unrelated `foo/css/bar.js` is left
1403
+ * alone, and the caller's extension is preserved rather than guessed at.
1404
+ */
1405
+ const helperModuleFromSubpath = (mod) => {
1406
+ const normalized = mod.replaceAll("\\", "/");
1407
+ const at = normalized.lastIndexOf("/css/");
1408
+ if (at < 0) return void 0;
1409
+ const prefix = normalized.slice(0, at + 5 - 1);
1410
+ if (!isGeneratedCssModule(prefix)) return void 0;
1411
+ const rest = normalized.slice(at + 5);
1412
+ if (!rest || rest.includes("/")) return void 0;
1413
+ const dot = rest.lastIndexOf(".");
1414
+ const extension = dot > 0 ? rest.slice(dot) : "";
1415
+ if (rest === `cx${extension}`) return void 0;
1416
+ return `${prefix}/cx${extension}`;
1417
+ };
1418
+ /**
1419
+ * The generated css entry as spelled beside an imported config recipe.
1420
+ *
1421
+ * A decision table needs only `cvaMap`, but a module importing a config recipe often has
1422
+ * no css import to extend. Preserve a relative/aliased styled-system spelling by replacing
1423
+ * its `/recipes` suffix; falling back to the configured generated entry covers bare imports.
1424
+ */
1425
+ const configRecipeCssSpecifier = (call, binding) => {
1426
+ for (const declaration of call.getSourceFile().getImportDeclarations()) {
1427
+ if (declaration.isTypeOnly()) continue;
1428
+ if (!declaration.getNamedImports().some((named) => {
1429
+ if (named.isTypeOnly()) return false;
1430
+ return (named.getAliasNode() ?? named.getNameNode()).getText() === binding;
1431
+ })) continue;
1432
+ const mod = declaration.getModuleSpecifierValue().replaceAll("\\", "/");
1433
+ const at = mod.lastIndexOf("/recipes");
1434
+ if (at >= 0) return `${mod.slice(0, at)}/css`;
1435
+ }
1436
+ return generatedCssModule;
1437
+ };
1438
+ /**
1439
+ * How *this* module would have to spell the css module, learnt from one that already does.
1440
+ *
1441
+ * A file calling an imported recipe need not import the css module at all, so when the
1442
+ * lowering needs a decision-table helper there is no spelling in the file to copy. The declaring module
1443
+ * necessarily has one — `cva` came from it — and that is the spelling reused here.
1444
+ *
1445
+ * A bare or aliased specifier resolves identically from any file, so it is taken as
1446
+ * written. A relative one is re-based: resolved against the module that wrote it, then
1447
+ * expressed from the module being folded.
1448
+ */
1449
+ const cssModuleSpecifierFrom = (declaring) => {
1450
+ for (const declaration of declaring.getImportDeclarations()) {
1451
+ if (declaration.isTypeOnly()) continue;
1452
+ const mod = declaration.getModuleSpecifierValue();
1453
+ if (isGeneratedCssModule(mod)) return mod;
1454
+ }
1455
+ };
1456
+ /**
1457
+ * That spelling, said from the module being folded.
1458
+ *
1459
+ * A bare or aliased specifier resolves identically from any file, so it is taken as
1460
+ * written. A relative one is re-based: resolved against the module that wrote it, then
1461
+ * expressed from the module being folded. Pure path arithmetic, so it holds a string
1462
+ * rather than a node — a cached node does not survive the next `addSourceFile`, which
1463
+ * ts-morph implements by forgetting the file's whole tree.
1464
+ */
1465
+ const rebaseSpecifier = (specifier, declaringPath, consumingPath) => {
1466
+ if (!specifier.startsWith(".")) return specifier;
1467
+ const absolute = (0, node_path.resolve)((0, node_path.dirname)(declaringPath), specifier);
1468
+ const rebased = (0, node_path.relative)((0, node_path.dirname)(consumingPath), absolute).replaceAll("\\", "/");
1469
+ if (!rebased) return void 0;
1470
+ return rebased.startsWith(".") ? rebased : `./${rebased}`;
1471
+ };
1472
+ /**
1473
+ * Configs of one foreign module, parsed once however many of its recipes are called.
1474
+ *
1475
+ * Falls back to a per-call map when the caller supplies none, so the fold stays correct
1476
+ * standalone — only repeated, which is what the shared cache exists to avoid.
1477
+ */
1478
+ const configsByModule = recipeConfigCache ?? /* @__PURE__ */ new Map();
1479
+ /** The specifier each imported recipe's module used for the css module, when it needs one. */
1480
+ const helperModules = /* @__PURE__ */ new Map();
1481
+ /** Declaring modules a fold read, recorded as paths because their nodes do not persist. */
1482
+ const foreignDependencies = /* @__PURE__ */ new Set();
1483
+ /** Resolutions for this module's own call sites, keyed by the name the call site writes. */
1484
+ const importedRecipes = /* @__PURE__ */ new Map();
1485
+ /**
1486
+ * The config of a recipe this module imports.
1487
+ *
1488
+ * The binding is followed with ts-morph's symbol aliasing rather than by re-reading import
1489
+ * declarations, because that is what already understands the shapes these are reached
1490
+ * through: `export { badge } from './styles'`, `export * from './styles'`, and an alias at
1491
+ * either end. Each hop is an alias symbol, so following them to a non-alias lands on the
1492
+ * declaration wherever it lives.
1493
+ *
1494
+ * The selected declarations do not depend on which module the call is in. A recipe lowered
1495
+ * here therefore reaches the same globally shared atoms as a call in its declaring module.
1496
+ */
1497
+ const resolveImportedRecipe = (call, name, origin) => {
1498
+ if (importedRecipes.has(name)) return importedRecipes.get(name);
1499
+ const resolve = () => {
1500
+ if (!parseModule) return void 0;
1501
+ const consuming = call.getSourceFile();
1502
+ if (origin.filePath === consuming.getFilePath()) return void 0;
1503
+ let foreign = configsByModule.get(origin.filePath);
1504
+ if (!foreign) {
1505
+ const result = parseModule(origin.filePath);
1506
+ if (!result) return void 0;
1507
+ const collected = collectRecipeConfigs(result);
1508
+ const declaring = [...collected.values()].find((entry) => entry.box)?.box?.getNode?.()?.getSourceFile();
1509
+ const configs = /* @__PURE__ */ new Map();
1510
+ for (const [key, entry] of collected) configs.set(key, entry === AMBIGUOUS ? entry : {
1511
+ config: entry.config,
1512
+ box: void 0
1513
+ });
1514
+ foreign = {
1515
+ configs,
1516
+ cssSpecifier: declaring ? cssModuleSpecifierFrom(declaring) : void 0
1517
+ };
1518
+ configsByModule.set(origin.filePath, foreign);
1519
+ }
1520
+ const entry = foreign.configs.get(origin.name);
1521
+ if (!entry || entry === AMBIGUOUS) return void 0;
1522
+ foreignDependencies.add(origin.filePath);
1523
+ helperModules.set(name, foreign.cssSpecifier ? rebaseSpecifier(foreign.cssSpecifier, origin.filePath, consuming.getFilePath()) : void 0);
1524
+ return entry;
1525
+ };
1526
+ const resolved = resolve();
1527
+ importedRecipes.set(name, resolved);
1528
+ return resolved;
1529
+ };
1530
+ const folded = [];
1531
+ const skipped = [];
1532
+ const candidates = [];
1533
+ const seenRanges = /* @__PURE__ */ new Set();
1534
+ const recipeConfigs = collectRecipeConfigs(parserResult);
1535
+ const recipeDefinitions = [];
1536
+ for (const [name, entry] of recipeConfigs) {
1537
+ if (entry === AMBIGUOUS) continue;
1538
+ const definition = entry.box?.getNode?.();
1539
+ if (!definition) continue;
1540
+ const call = ts_morph.Node.isCallExpression(definition) ? definition : definition.getFirstAncestorByKind(ts_morph.SyntaxKind.CallExpression);
1541
+ if (!call || code.slice(call.getStart(), call.getEnd()) !== call.getText()) continue;
1542
+ recipeDefinitions.push({
1543
+ name,
1544
+ call
1545
+ });
1546
+ }
1547
+ /** Ranges already reported as declined, so one call is never counted twice. */
1548
+ const reportedRanges = /* @__PURE__ */ new Set();
1549
+ const importCache = /* @__PURE__ */ new Map();
1550
+ const importsFor = (sourceFile) => {
1551
+ let names = importCache.get(sourceFile);
1552
+ if (!names) {
1553
+ names = bambooImportedNames(sourceFile, ctx);
1554
+ importCache.set(sourceFile, names);
1555
+ }
1556
+ return names;
1557
+ };
1558
+ for (const item of parserResult.toArray()) {
1559
+ const type = item.type ?? "";
1560
+ const name = item.name ?? type;
1561
+ if (!item.box) continue;
1562
+ const call = findCallExpression(item.box);
1563
+ if (type === "token" || type === "tokenValue") {
1564
+ if (!call) {
1565
+ skipped.push({
1566
+ name,
1567
+ reason: "no-call-expression",
1568
+ start: 0,
1569
+ end: 0
1570
+ });
1571
+ continue;
1572
+ }
1573
+ const start = call.getStart();
1574
+ const end = call.getEnd();
1575
+ if (code.slice(start, end) !== call.getText()) {
1576
+ skipped.push({
1577
+ name,
1578
+ reason: "no-call-expression",
1579
+ start: 0,
1580
+ end: 0
1581
+ });
1582
+ continue;
1583
+ }
1584
+ const rangeKey = `${start}:${end}`;
1585
+ if (seenRanges.has(rangeKey)) continue;
1586
+ seenRanges.add(rangeKey);
1587
+ const rootName = calleeRootName(call);
1588
+ if (!rootName || !importsFor(call.getSourceFile()).has(rootName) || isShadowed(call, rootName)) {
1589
+ skipped.push({
1590
+ name,
1591
+ reason: "not-imported",
1592
+ start,
1593
+ end
1594
+ });
1595
+ continue;
1596
+ }
1597
+ const callee = ts_morph.Node.isCallExpression(call) ? call.getExpression() : void 0;
1598
+ const propertyName = ts_morph.Node.isPropertyAccessExpression(callee) ? callee.getNameNode().getText() : void 0;
1599
+ const wantsValue = type === "tokenValue";
1600
+ if (wantsValue !== (propertyName === "value")) {
1601
+ skipped.push({
1602
+ name,
1603
+ reason: "unsupported-kind",
1604
+ start,
1605
+ end
1606
+ });
1607
+ continue;
1608
+ }
1609
+ if (!wantsValue && propertyName !== void 0 && !ctx.imports.matchers.tokens.match(propertyName)) {
1610
+ skipped.push({
1611
+ name,
1612
+ reason: "unsupported-kind",
1613
+ start,
1614
+ end
1615
+ });
1616
+ continue;
1617
+ }
1618
+ if (!isStaticBox(item.box) || item.data.length !== 1) {
1619
+ skipped.push({
1620
+ name,
1621
+ reason: "dynamic",
1622
+ start,
1623
+ end
1624
+ });
1625
+ continue;
1626
+ }
1627
+ const path = item.data[0];
1628
+ if (typeof path !== "string") {
1629
+ skipped.push({
1630
+ name,
1631
+ reason: "dynamic",
1632
+ start,
1633
+ end
1634
+ });
1635
+ continue;
1636
+ }
1637
+ if (!(ts_morph.Node.isCallExpression(call) ? call.getArguments().slice(1) : []).every(isInertArgument)) {
1638
+ skipped.push({
1639
+ name,
1640
+ reason: "dynamic",
1641
+ start,
1642
+ end
1643
+ });
1644
+ continue;
1645
+ }
1646
+ const value = wantsValue ? runtimeTokenValue(path) : runtimeToken(path);
1647
+ if (!value) {
1648
+ skipped.push({
1649
+ name,
1650
+ reason: "unresolved-token",
1651
+ start,
1652
+ end
1653
+ });
1654
+ continue;
1655
+ }
1656
+ candidates.push({
1657
+ item,
1658
+ call,
1659
+ node: call,
1660
+ start,
1661
+ end,
1662
+ value
1663
+ });
1664
+ continue;
1665
+ }
1666
+ if (!FOLDABLE_TYPES.has(type)) {
1667
+ if (call && (type === RECIPE_CALL_TYPE || type === "recipe") && !isShadowed(call, name)) {
1668
+ const start = call.getStart();
1669
+ const end = call.getEnd();
1670
+ const rangeKey = `${start}:${end}`;
1671
+ if (!reportedRanges.has(rangeKey)) {
1672
+ reportedRanges.add(rangeKey);
1673
+ if (code.slice(start, end) !== call.getText()) {
1674
+ skipped.push({
1675
+ name,
1676
+ reason: "no-call-expression",
1677
+ start: 0,
1678
+ end: 0
1679
+ });
1680
+ continue;
1681
+ }
1682
+ if (isRawCall(call)) {
1683
+ skipped.push({
1684
+ name,
1685
+ reason: "raw-call",
1686
+ start,
1687
+ end
1688
+ });
1689
+ continue;
1690
+ }
1691
+ if (!recipeConfigs.has(name)) {
1692
+ if (type === "recipe") {
1693
+ const config = ctx.recipes.getConfig(name);
1694
+ if (config) {
1695
+ recipeConfigs.set(name, {
1696
+ config,
1697
+ box: void 0
1698
+ });
1699
+ helperModules.set(name, configRecipeCssSpecifier(call, name));
1700
+ }
1701
+ } else if (item.origin) {
1702
+ const imported = resolveImportedRecipe(call, name, item.origin);
1703
+ if (imported) recipeConfigs.set(name, imported);
1704
+ }
1705
+ }
1706
+ const resolvedSelection = item.data?.length === 1 ? item.data[0] : void 0;
1707
+ const entry = recipeConfigs.get(name);
1708
+ let inlineSlot;
1709
+ let inlineEnd = end;
1710
+ if (Array.isArray(entry?.config.slots)) {
1711
+ const parent = call.getParent();
1712
+ if (ts_morph.Node.isPropertyAccessExpression(parent) && parent.getExpression() === call) {
1713
+ const accessed = parent.getName();
1714
+ if (entry.config.slots.includes(accessed)) {
1715
+ inlineSlot = accessed;
1716
+ inlineEnd = parent.getEnd();
1717
+ }
1718
+ } else if (ts_morph.Node.isElementAccessExpression(parent) && parent.getExpression() === call) {
1719
+ const argument = parent.getArgumentExpression();
1720
+ const accessed = argument && (ts_morph.Node.isStringLiteral(argument) || ts_morph.Node.isNoSubstitutionTemplateLiteral(argument)) ? argument.getLiteralValue() : void 0;
1721
+ if (typeof accessed === "string" && entry.config.slots.includes(accessed)) {
1722
+ inlineSlot = accessed;
1723
+ inlineEnd = parent.getEnd();
1724
+ }
1725
+ }
1726
+ }
1727
+ const lowered = lowerRecipeCall(call, entry, styleCompiler, isInertExpression, resolvedSelection, inlineSlot, maxRecipeStates);
1728
+ if (lowered.kind === "dynamic-style") {
1729
+ const helper = ensureRecipeHelperImport(RECIPE_MAP_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed, helperModules.get(name), helperModuleFromSubpath);
1730
+ if (helper) {
1731
+ candidates.push({
1732
+ item,
1733
+ call,
1734
+ node: call,
1735
+ start,
1736
+ end: inlineEnd,
1737
+ className: "",
1738
+ classNames: [],
1739
+ styleMap: lowered.map,
1740
+ mapHelperName: helper.name,
1741
+ insert: helper.insert,
1742
+ configBox: entry?.box,
1743
+ outputKind: lowered.map.outputKind === "slots" ? "slots" : void 0
1744
+ });
1745
+ continue;
1746
+ }
1747
+ skipped.push({
1748
+ name,
1749
+ reason: "recipe-call",
1750
+ start,
1751
+ end
1752
+ });
1753
+ continue;
1754
+ }
1755
+ if (lowered.kind === "slots") {
1756
+ const helper = lowered.helper ? ensureRecipeHelperImport(lowered.helper, call, isBambooCssModule, isGeneratedCssModule, isShadowed, helperModules.get(name), helperModuleFromSubpath) : void 0;
1757
+ if (!lowered.helper || helper) {
1758
+ const replacement = lowered.helper && helper && helper.name !== lowered.helper ? lowered.expression.replaceAll(`${lowered.helper}(`, `${helper.name}(`) : lowered.expression;
1759
+ candidates.push({
1760
+ item,
1761
+ call,
1762
+ node: call,
1763
+ start,
1764
+ end: inlineEnd,
1765
+ replacement,
1766
+ className: "",
1767
+ classNames: lowered.classNames,
1768
+ insert: helper?.insert,
1769
+ configBox: entry?.box,
1770
+ outputKind: "slots"
1771
+ });
1772
+ continue;
1773
+ }
1774
+ skipped.push({
1775
+ name,
1776
+ reason: "recipe-call",
1777
+ start,
1778
+ end
1779
+ });
1780
+ continue;
1781
+ }
1782
+ if (lowered.kind === "class") {
1783
+ candidates.push({
1784
+ item,
1785
+ call,
1786
+ node: call,
1787
+ start,
1788
+ end: inlineEnd,
1789
+ replacement: JSON.stringify(lowered.className),
1790
+ className: lowered.className,
1791
+ classNames: lowered.className.split(" ").filter(Boolean),
1792
+ styleSet: lowered.styles,
1793
+ configBox: entry?.box
1794
+ });
1795
+ continue;
1796
+ }
1797
+ skipped.push({
1798
+ name,
1799
+ reason: "recipe-call",
1800
+ start,
1801
+ end
1802
+ });
1803
+ }
1804
+ }
1805
+ continue;
1806
+ }
1807
+ if (!call) {
1808
+ skipped.push({
1809
+ name,
1810
+ reason: "no-call-expression",
1811
+ start: 0,
1812
+ end: 0
1813
+ });
1814
+ continue;
1815
+ }
1816
+ const start = call.getStart();
1817
+ const end = call.getEnd();
1818
+ const memberParent = call.getParent();
1819
+ const accessed = type === "recipe" && ts_morph.Node.isPropertyAccessExpression(memberParent) && memberParent.getExpression() === call ? memberParent : void 0;
1820
+ const accessedName = accessed?.getNameNode().getText();
1821
+ const declaredSlots = accessed ? ctx.recipes.getConfig(name)?.slots ?? [] : [];
1822
+ const memberAccess = accessedName && declaredSlots.includes(accessedName) ? accessed : void 0;
1823
+ const slot = memberAccess ? accessedName : void 0;
1824
+ const foldEnd = memberAccess ? memberAccess.getEnd() : end;
1825
+ if (code.slice(start, end) !== call.getText()) {
1826
+ skipped.push({
1827
+ name,
1828
+ reason: "no-call-expression",
1829
+ start: 0,
1830
+ end: 0
1831
+ });
1832
+ continue;
1833
+ }
1834
+ const rangeKey = `${start}:${foldEnd}`;
1835
+ if (seenRanges.has(rangeKey)) continue;
1836
+ seenRanges.add(rangeKey);
1837
+ if (isRawCall(call)) {
1838
+ skipped.push({
1839
+ name,
1840
+ reason: "raw-call",
1841
+ start,
1842
+ end
1843
+ });
1844
+ continue;
1845
+ }
1846
+ const rootName = calleeRootName(call);
1847
+ if (!rootName || !importsFor(call.getSourceFile()).has(rootName) || isShadowed(call, rootName)) {
1848
+ skipped.push({
1849
+ name,
1850
+ reason: "not-imported",
1851
+ start,
1852
+ end
1853
+ });
1854
+ continue;
1855
+ }
1856
+ if (!(ts_morph.Node.isCallExpression(call) && call.getArguments().length === 0 && item.data.length === 1) && !isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
1857
+ skipped.push({
1858
+ name,
1859
+ reason: "dynamic",
1860
+ start,
1861
+ end
1862
+ });
1863
+ continue;
1864
+ }
1865
+ candidates.push({
1866
+ item,
1867
+ call,
1868
+ node: call,
1869
+ start,
1870
+ end: foldEnd,
1871
+ slot
1872
+ });
1873
+ }
1874
+ /**
1875
+ * Resolve every fully static candidate to symbolic declarations before allocating a class.
1876
+ *
1877
+ * The normal fold can wait until the rewrite loop to compute a class string. Semantic
1878
+ * composition cannot: an enclosing `cx()` needs the declarations of its arguments so it can
1879
+ * discard overridden values before any string exists.
1880
+ */
1881
+ {
1882
+ for (const candidate of candidates) {
1883
+ if (candidate.styleSet || candidate.value !== void 0 || candidate.replacement) continue;
1884
+ const { item } = candidate;
1885
+ if (item.type === "css") {
1886
+ candidate.styleSet = styleCompiler.compose(...item.data);
1887
+ continue;
1888
+ }
1889
+ if (item.type === "pattern") {
1890
+ candidate.styleSet = styleCompiler.compose(...item.data.map((entry) => ctx.patterns.transform(item.name ?? "", entry)));
1891
+ continue;
1892
+ }
1893
+ if (item.type === "viewTransition") {
1894
+ const semantic = (0, _bamboocss_shared.viewTransitionClassName)(item.data[0], ctx.utility.prefix);
1895
+ candidate.className = styleCompiler.allocateClassString(semantic);
1896
+ candidate.classNames = [candidate.className];
1897
+ candidate.replacement = JSON.stringify(candidate.className);
1898
+ continue;
1899
+ }
1900
+ }
1901
+ const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile();
1902
+ if (sourceFile) {
1903
+ const cxBindings = /* @__PURE__ */ new Set();
1904
+ for (const declaration of sourceFile.getImportDeclarations()) {
1905
+ if (declaration.isTypeOnly() || !isBambooCssModule(declaration.getModuleSpecifierValue())) continue;
1906
+ for (const named of declaration.getNamedImports()) {
1907
+ if (named.isTypeOnly() || named.getNameNode().getText() !== "cx") continue;
1908
+ cxBindings.add((named.getAliasNode() ?? named.getNameNode()).getText());
1909
+ }
1910
+ }
1911
+ const byRange = new Map(candidates.map((candidate) => [`${candidate.start}:${candidate.end}`, candidate]));
1912
+ for (const call of cxBindings.size ? sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression) : []) {
1913
+ const callee = call.getExpression();
1914
+ if (!ts_morph.Node.isIdentifier(callee) || !cxBindings.has(callee.getText()) || isShadowed(call, callee.getText())) continue;
1915
+ const matched = [];
1916
+ const parts = [];
1917
+ const dynamic = [];
1918
+ const constantCandidates = [];
1919
+ let supported = true;
1920
+ const take = (arg) => {
1921
+ const candidate = byRange.get(`${arg.getStart()}:${arg.getEnd()}`);
1922
+ if (candidate?.styleMap?.outputKind === "class") {
1923
+ dynamic.push(candidate);
1924
+ parts.push({
1925
+ kind: "dynamic",
1926
+ candidate
1927
+ });
1928
+ return true;
1929
+ }
1930
+ if (candidate?.styleSet) {
1931
+ matched.push(candidate);
1932
+ parts.push({
1933
+ kind: "style",
1934
+ candidate
1935
+ });
1936
+ return true;
1937
+ }
1938
+ if (candidate?.item.type === "viewTransition" && candidate.replacement && candidate.className) {
1939
+ constantCandidates.push(candidate);
1940
+ parts.push({
1941
+ kind: "class",
1942
+ value: candidate.className,
1943
+ candidate
1944
+ });
1945
+ return true;
1946
+ }
1947
+ if (ts_morph.Node.isStringLiteral(arg) || ts_morph.Node.isNoSubstitutionTemplateLiteral(arg)) {
1948
+ parts.push({
1949
+ kind: "class",
1950
+ value: arg.getLiteralValue()
1951
+ });
1952
+ return true;
1953
+ }
1954
+ if (ts_morph.Node.isArrayLiteralExpression(arg)) {
1955
+ for (const element of arg.getElements()) if (ts_morph.Node.isSpreadElement(element) || !take(element)) return false;
1956
+ return true;
1957
+ }
1958
+ if (arg.getKind() === ts_morph.SyntaxKind.FalseKeyword || arg.getKind() === ts_morph.SyntaxKind.TrueKeyword || ts_morph.Node.isNumericLiteral(arg) || arg.getKind() === ts_morph.SyntaxKind.NullKeyword || ts_morph.Node.isIdentifier(arg) && arg.getText() === "undefined") return true;
1959
+ return false;
1960
+ };
1961
+ for (const arg of call.getArguments()) {
1962
+ if (take(arg)) continue;
1963
+ supported = false;
1964
+ break;
1965
+ }
1966
+ if (dynamic.length > 1) {
1967
+ skipped.push({
1968
+ name: "cx",
1969
+ reason: "dynamic",
1970
+ start: call.getStart(),
1971
+ end: call.getEnd()
1972
+ });
1973
+ continue;
1974
+ }
1975
+ if (!supported) {
1976
+ skipped.push({
1977
+ name: "cx",
1978
+ reason: "dynamic",
1979
+ start: call.getStart(),
1980
+ end: call.getEnd()
1981
+ });
1982
+ continue;
1983
+ }
1984
+ if (dynamic.length === 1 && matched.length > 0) {
1985
+ const dynamicCandidate = dynamic[0];
1986
+ const styleParts = parts.filter((part) => part.kind !== "class");
1987
+ const dynamicIndex = styleParts.findIndex((part) => part.kind === "dynamic");
1988
+ const before = styleParts.slice(0, dynamicIndex).filter((part) => part.kind === "style").map((part) => part.candidate.styleSet);
1989
+ const after = styleParts.slice(dynamicIndex + 1).filter((part) => part.kind === "style").map((part) => part.candidate.styleSet);
1990
+ const compiled = dynamicCandidate.styleMap.compile(before, after);
1991
+ const expression = compiled.usesHelper && dynamicCandidate.mapHelperName && dynamicCandidate.mapHelperName !== "cvaMap" ? compiled.expression.replaceAll(`${RECIPE_MAP_HELPER}(`, `${dynamicCandidate.mapHelperName}(`) : compiled.expression;
1992
+ const arguments_ = [];
1993
+ let wroteCompiled = false;
1994
+ for (const part of parts) {
1995
+ if (part.kind === "class") {
1996
+ if (part.value) arguments_.push(JSON.stringify(part.value));
1997
+ continue;
1998
+ }
1999
+ if (!wroteCompiled) {
2000
+ arguments_.push(expression);
2001
+ wroteCompiled = true;
2002
+ }
2003
+ }
2004
+ dynamicCandidate.subsumed = true;
2005
+ const first = styleParts[0].candidate;
2006
+ candidates.push({
2007
+ ...first,
2008
+ call,
2009
+ node: call,
2010
+ start: call.getStart(),
2011
+ end: call.getEnd(),
2012
+ displayName: "cx",
2013
+ replacement: arguments_.length === 1 ? arguments_[0] : `${callee.getText()}(${arguments_.join(", ")})`,
2014
+ className: "",
2015
+ classNames: [...compiled.classNames, ...parts.filter((part) => part.kind === "class").flatMap((part) => part.value.split(" "))].filter(Boolean),
2016
+ styleSet: void 0,
2017
+ styleMap: void 0,
2018
+ outputKind: void 0,
2019
+ insert: compiled.usesHelper ? dynamicCandidate.insert : void 0,
2020
+ sourceBoxes: styleParts.flatMap((part) => [part.candidate.item.box, part.candidate.configBox]).concat(constantCandidates.map((candidate) => candidate.item.box)).filter(Boolean)
2021
+ });
2022
+ continue;
2023
+ }
2024
+ if (matched.length === 0) {
2025
+ if (constantCandidates.length === 0) continue;
2026
+ const className = parts.filter((part) => part.kind === "class").map((part) => part.value).filter(Boolean).join(" ");
2027
+ const first = constantCandidates[0];
2028
+ candidates.push({
2029
+ ...first,
2030
+ call,
2031
+ node: call,
2032
+ start: call.getStart(),
2033
+ end: call.getEnd(),
2034
+ displayName: "cx",
2035
+ replacement: JSON.stringify(className),
2036
+ className,
2037
+ classNames: className.split(" ").filter(Boolean),
2038
+ sourceBoxes: constantCandidates.map((candidate) => candidate.item.box).filter(Boolean)
2039
+ });
2040
+ continue;
2041
+ }
2042
+ const merged = styleCompiler.compose(...matched.map((candidate) => candidate.styleSet));
2043
+ const compiled = styleCompiler.className(merged);
2044
+ const classParts = [];
2045
+ let wroteCompiled = false;
2046
+ for (const part of parts) {
2047
+ if (part.kind === "class") {
2048
+ if (part.value) classParts.push(part.value);
2049
+ continue;
2050
+ }
2051
+ if (!wroteCompiled && compiled) {
2052
+ classParts.push(compiled);
2053
+ wroteCompiled = true;
2054
+ }
2055
+ }
2056
+ const first = matched[0];
2057
+ candidates.push({
2058
+ ...first,
2059
+ call,
2060
+ node: call,
2061
+ start: call.getStart(),
2062
+ end: call.getEnd(),
2063
+ displayName: "cx",
2064
+ replacement: JSON.stringify(classParts.join(" ")),
2065
+ className: classParts.join(" "),
2066
+ classNames: classParts.flatMap((part) => part.split(" ")).filter(Boolean),
2067
+ styleSet: merged,
2068
+ sourceBoxes: [...matched.flatMap((candidate) => [candidate.item.box, candidate.configBox]), ...constantCandidates.map((candidate) => candidate.item.box)].filter(Boolean)
2069
+ });
2070
+ }
2071
+ }
2072
+ for (const candidate of candidates) {
2073
+ if (!candidate.styleMap || candidate.subsumed || candidate.replacement) continue;
2074
+ const compiled = candidate.styleMap.compile();
2075
+ candidate.replacement = compiled.usesHelper && candidate.mapHelperName && candidate.mapHelperName !== "cvaMap" ? compiled.expression.replaceAll(`${RECIPE_MAP_HELPER}(`, `${candidate.mapHelperName}(`) : compiled.expression;
2076
+ if (!compiled.usesHelper) candidate.insert = void 0;
2077
+ candidate.className = compiled.staticClasses;
2078
+ candidate.classNames = compiled.classNames;
2079
+ candidate.outputKind = compiled.outputKind === "slots" ? "slots" : void 0;
2080
+ }
2081
+ }
2082
+ /**
2083
+ * Ranges the rewrite actually replaced. Declared before the early return below, because
2084
+ * that return is now also a reporting point: a module with nothing to fold is exactly the
2085
+ * shape `reportSurvivors` exists to catch.
2086
+ */
2087
+ const applied = [];
2088
+ if (candidates.length === 0 && recipeDefinitions.length === 0) {
2089
+ if (reportSurvivors) reportRuntimeBindings();
2090
+ return {
2091
+ code,
2092
+ map: null,
2093
+ folded,
2094
+ skipped,
2095
+ dependencies: []
2096
+ };
2097
+ }
2098
+ const rewriteSourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
2099
+ if (!rewriteSourceFile) return {
2100
+ code,
2101
+ map: null,
2102
+ folded,
2103
+ skipped,
2104
+ dependencies: []
2105
+ };
2106
+ const dependencyScan = createDependencyScan(rewriteSourceFile);
2107
+ candidates.sort((a, b) => a.start - b.start || b.end - a.end);
2108
+ const magic = new magic_string.default(code);
2109
+ const insertedNames = /* @__PURE__ */ new Set();
2110
+ const applyInsert = (insert) => {
2111
+ if (!insert) return;
2112
+ const missing = insert.names.filter((name) => !insertedNames.has(name));
2113
+ if (!missing.length) return;
2114
+ magic.appendLeft(insert.pos, insert.module ? `\nimport { ${missing.join(", ")} } from '${insert.module}'` : missing.map((name) => `, ${name}`).join(""));
2115
+ for (const name of missing) insertedNames.add(name);
2116
+ };
2117
+ const collides = (edits) => edits.some(([start, end]) => applied.some(([from, to]) => start < to && from < end));
2118
+ for (const candidate of candidates) {
2119
+ const { item, start, end } = candidate;
2120
+ const name = candidate.displayName ?? item.name ?? item.type ?? "";
2121
+ const ranges = [[start, end]];
2122
+ if (collides(ranges)) {
2123
+ skipped.push({
2124
+ name,
2125
+ reason: "overlapping",
2126
+ start,
2127
+ end
2128
+ });
2129
+ continue;
2130
+ }
2131
+ if (candidate.value !== void 0) {
2132
+ magic.overwrite(start, end, JSON.stringify(candidate.value));
2133
+ applied.push(...ranges);
2134
+ folded.push({
2135
+ name,
2136
+ kind: "value",
2137
+ className: "",
2138
+ classNames: [],
2139
+ value: candidate.value,
2140
+ start,
2141
+ end
2142
+ });
2143
+ collectSourceFiles(item.box, dependencyScan);
2144
+ continue;
2145
+ }
2146
+ if (candidate.replacement) {
2147
+ magic.overwrite(start, end, candidate.replacement);
2148
+ applyInsert(candidate.insert);
2149
+ applied.push(...ranges);
2150
+ folded.push({
2151
+ name,
2152
+ kind: candidate.outputKind ?? "class",
2153
+ className: candidate.className,
2154
+ classNames: (candidate.classNames ?? [candidate.className]).filter(Boolean),
2155
+ start,
2156
+ end
2157
+ });
2158
+ collectSourceFiles(item.box, dependencyScan);
2159
+ if (candidate.configBox) collectSourceFiles(candidate.configBox, dependencyScan);
2160
+ for (const box of candidate.sourceBoxes ?? []) collectSourceFiles(box, dependencyScan);
2161
+ continue;
2162
+ }
2163
+ let className;
2164
+ try {
2165
+ if (item.type === "pattern") className = runtimeCss(...item.data.map((entry) => ctx.patterns.transform(name, entry)));
2166
+ else className = runtimeCss(...item.data);
2167
+ } catch {
2168
+ skipped.push({
2169
+ name,
2170
+ reason: "dynamic",
2171
+ start,
2172
+ end
2173
+ });
2174
+ continue;
2175
+ }
2176
+ magic.overwrite(start, end, JSON.stringify(className));
2177
+ applied.push(...ranges);
2178
+ folded.push({
2179
+ name,
2180
+ kind: "class",
2181
+ className,
2182
+ classNames: className ? [className] : [],
2183
+ start,
2184
+ end
2185
+ });
2186
+ collectSourceFiles(item.box, dependencyScan);
2187
+ }
2188
+ const recipeSourceFile = candidates[0]?.node.getSourceFile();
2189
+ if (recipeSourceFile && mayNameSplitVariantProps(recipeSourceFile.getFullText())) for (const access of recipeSourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.PropertyAccessExpression)) {
2190
+ if (access.getName() !== "splitVariantProps") continue;
2191
+ const target = access.getExpression();
2192
+ if (!ts_morph.Node.isIdentifier(target)) continue;
2193
+ if (isShadowed(access, target.getText())) continue;
2194
+ const local = recipeConfigs?.get(target.getText());
2195
+ const importedConfig = !local && importsFor(recipeSourceFile).has(target.getText()) ? ctx.recipes.getConfig(target.getText()) : void 0;
2196
+ const entry = local && local !== AMBIGUOUS ? local : importedConfig ? {
2197
+ config: importedConfig,
2198
+ box: void 0
2199
+ } : void 0;
2200
+ if (!entry) continue;
2201
+ const call = access.getParent();
2202
+ if (!ts_morph.Node.isCallExpression(call) || call.getExpression() !== access) continue;
2203
+ const args = call.getArguments();
2204
+ if (args.length !== 1) continue;
2205
+ const start = call.getStart();
2206
+ const end = call.getEnd();
2207
+ if (code.slice(start, end) !== call.getText()) continue;
2208
+ if (collides([[start, end]])) continue;
2209
+ const helper = ensureRecipeHelperImport(SPLIT_PROPS_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed, helperModules.get(target.getText()), helperModuleFromSubpath);
2210
+ if (!helper) continue;
2211
+ const keys = Object.keys(entry.config.variants ?? {});
2212
+ magic.overwrite(start, end, `${helper.name}(${args[0].getText()}, ${JSON.stringify(keys)})`);
2213
+ applyInsert(helper.insert);
2214
+ applied.push([start, end]);
2215
+ }
2216
+ for (const { name, call } of recipeDefinitions) {
2217
+ const start = call.getStart();
2218
+ const end = call.getEnd();
2219
+ if (collides([[start, end]])) continue;
2220
+ magic.overwrite(start, end, "undefined");
2221
+ applied.push([start, end]);
2222
+ folded.push({
2223
+ name,
2224
+ kind: "definition",
2225
+ className: "",
2226
+ classNames: [],
2227
+ start,
2228
+ end
2229
+ });
2230
+ }
2231
+ /**
2232
+ * Bindings from a bamboo module still referenced once every rewrite is applied.
2233
+ *
2234
+ * Deliberately not driven by `parserResult`: that is the recogniser, and the point here is
2235
+ * to catch what it did not see. A namespace import called as `s.cva(...)`, a default
2236
+ * import, a specifier that resolved to nothing — each leaves a live reference and no ledger
2237
+ * entry at all, which used to let a build silently ship the engine.
2238
+ *
2239
+ * The helpers the compiler writes are excluded because they pull no style engine. `cx` is
2240
+ * also allowed to remain when it joins an arbitrary external class; only fully analyzable
2241
+ * arguments receive Bamboo's semantic composition guarantee.
2242
+ */
2243
+ /** The specifier this module imported `binding` through, if it did. */
2244
+ function importSpecifierFor(sourceFile, binding) {
2245
+ for (const declaration of sourceFile.getImportDeclarations()) {
2246
+ if (declaration.isTypeOnly()) continue;
2247
+ for (const named of declaration.getNamedImports()) {
2248
+ if (named.isTypeOnly()) continue;
2249
+ if ((named.getAliasNode() ?? named.getNameNode()).getText() === binding) return named.getAliasNode() ?? named.getNameNode();
2250
+ }
2251
+ }
2252
+ }
2253
+ function reportRuntimeBindings() {
2254
+ const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
2255
+ if (!sourceFile) return;
2256
+ const importedRecipeBindings = new Set(parserResult.importedRecipes?.keys() ?? []);
2257
+ /**
2258
+ * Every identifier in the module, grouped by the name it spells — built at most once per
2259
+ * pass, and only when something below has a name to look up.
2260
+ *
2261
+ * One index answers for every binding, rather than a walk per binding. Built here rather
2262
+ * than cached across passes: it holds nodes, and a node does not outlive its source file
2263
+ * being replaced.
2264
+ *
2265
+ * Deferred because most modules in an app neither declare nor import a recipe, and the
2266
+ * walk is not cheap — `getDescendantsOfKind` wraps every identifier in the file in a
2267
+ * ts-morph node to answer a question those modules never ask. It was 11% of a 6,307-file
2268
+ * build, most of it spent producing an index nothing read.
2269
+ */
2270
+ let identifiers;
2271
+ const identifiersByName = () => identifiers ??= identifierIndex(sourceFile);
2272
+ for (const binding of new Set([...recipeConfigs.keys(), ...importedRecipeBindings])) {
2273
+ const entry = recipeConfigs.get(binding);
2274
+ if (entry === AMBIGUOUS) continue;
2275
+ if (!entry && !importedRecipeBindings.has(binding)) continue;
2276
+ const definition = entry?.box?.getNode?.();
2277
+ const nameNode = definition?.getSourceFile() === sourceFile ? definition?.getFirstAncestorByKind(ts_morph.SyntaxKind.VariableDeclaration)?.getNameNode() : importSpecifierFor(sourceFile, binding);
2278
+ if (!nameNode || !ts_morph.Node.isIdentifier(nameNode)) continue;
2279
+ const references = localReferencesTo(identifiersByName(), binding, nameNode);
2280
+ if (skipped.filter((item) => SURVIVES_TO_RUNTIME.has(item.reason) && item.end > item.start).some((item) => references.some((ref) => ref.getStart() >= item.start && ref.getStart() < item.end))) continue;
2281
+ const survivor = references.find((ref) => !applied.some(([from, to]) => ref.getStart() >= from && ref.getStart() < to));
2282
+ if (!survivor) continue;
2283
+ skipped.push({
2284
+ name: binding,
2285
+ reason: "runtime-binding",
2286
+ start: survivor.getStart(),
2287
+ end: survivor.getEnd()
2288
+ });
2289
+ }
2290
+ const bambooModules = [
2291
+ ...cssModules,
2292
+ ...ctx.imports.matchers.recipe?.mods ?? [],
2293
+ ...ctx.imports.matchers.pattern?.mods ?? [],
2294
+ ...ctx.imports.matchers.tokens?.mods ?? []
2295
+ ];
2296
+ const runtimeCalls = [];
2297
+ const importEquals = [];
2298
+ sourceFile.forEachDescendant((node) => {
2299
+ if (ts_morph.Node.isCallExpression(node)) runtimeCalls.push(node);
2300
+ else if (ts_morph.Node.isImportEqualsDeclaration(node)) importEquals.push(node);
2301
+ });
2302
+ for (const call of runtimeCalls) {
2303
+ const callee = call.getExpression();
2304
+ const argument = call.getArguments()[0];
2305
+ if (!argument || !ts_morph.Node.isStringLiteral(argument) && !ts_morph.Node.isNoSubstitutionTemplateLiteral(argument)) continue;
2306
+ if (!matchesModule(argument.getLiteralValue(), bambooModules)) continue;
2307
+ const isDynamicImport = callee.getKind() === ts_morph.SyntaxKind.ImportKeyword;
2308
+ const isRequire = ts_morph.Node.isIdentifier(callee) && callee.getText() === "require" && !isShadowed(call, "require");
2309
+ if (!isDynamicImport && !isRequire) continue;
2310
+ skipped.push({
2311
+ name: isDynamicImport ? "import" : "require",
2312
+ reason: "runtime-binding",
2313
+ start: call.getStart(),
2314
+ end: call.getEnd()
2315
+ });
2316
+ }
2317
+ for (const declaration of importEquals) {
2318
+ if (declaration.isTypeOnly()) continue;
2319
+ const reference = declaration.getModuleReference();
2320
+ if (!ts_morph.Node.isExternalModuleReference(reference)) continue;
2321
+ const expression = reference.getExpression();
2322
+ if (!expression || !ts_morph.Node.isStringLiteral(expression) || !matchesModule(expression.getLiteralValue(), bambooModules)) continue;
2323
+ skipped.push({
2324
+ name: declaration.getName(),
2325
+ reason: "runtime-binding",
2326
+ start: declaration.getStart(),
2327
+ end: declaration.getEnd()
2328
+ });
2329
+ }
2330
+ /** Local name -> what to call it in the report. */
2331
+ const watched = /* @__PURE__ */ new Map();
2332
+ for (const declaration of sourceFile.getImportDeclarations()) {
2333
+ if (declaration.isTypeOnly()) continue;
2334
+ if (!matchesModule(declaration.getModuleSpecifierValue(), bambooModules)) continue;
2335
+ for (const named of declaration.getNamedImports()) {
2336
+ if (named.isTypeOnly()) continue;
2337
+ const imported = named.getNameNode().getText();
2338
+ if (PERMITTED_BINDINGS.has(imported)) continue;
2339
+ watched.set((named.getAliasNode() ?? named.getNameNode()).getText(), imported);
2340
+ }
2341
+ const namespace = declaration.getNamespaceImport();
2342
+ if (namespace) watched.set(namespace.getText(), `${namespace.getText()}.*`);
2343
+ const defaultImport = declaration.getDefaultImport();
2344
+ if (defaultImport) watched.set(defaultImport.getText(), defaultImport.getText());
2345
+ }
2346
+ for (const declaration of sourceFile.getExportDeclarations()) {
2347
+ if (declaration.isTypeOnly()) continue;
2348
+ if (!matchesModule(declaration.getModuleSpecifierValue() ?? "", bambooModules)) continue;
2349
+ if (declaration.isNamespaceExport()) {
2350
+ skipped.push({
2351
+ name: declaration.getNamespaceExport()?.getName() ?? "*",
2352
+ reason: "runtime-binding",
2353
+ start: declaration.getStart(),
2354
+ end: declaration.getEnd()
2355
+ });
2356
+ continue;
2357
+ }
2358
+ for (const named of declaration.getNamedExports()) {
2359
+ if (named.isTypeOnly()) continue;
2360
+ const imported = named.getNameNode().getText();
2361
+ if (PERMITTED_BINDINGS.has(imported)) continue;
2362
+ skipped.push({
2363
+ name: imported,
2364
+ reason: "runtime-binding",
2365
+ start: named.getStart(),
2366
+ end: named.getEnd()
2367
+ });
2368
+ }
2369
+ }
2370
+ for (const declaration of sourceFile.getExportDeclarations()) {
2371
+ if (declaration.isTypeOnly() || declaration.getModuleSpecifier()) continue;
2372
+ for (const named of declaration.getNamedExports()) {
2373
+ if (named.isTypeOnly()) continue;
2374
+ const imported = watched.get(named.getNameNode().getText());
2375
+ if (imported === void 0) continue;
2376
+ skipped.push({
2377
+ name: imported,
2378
+ reason: "runtime-binding",
2379
+ start: named.getStart(),
2380
+ end: named.getEnd()
2381
+ });
2382
+ }
2383
+ }
2384
+ if (watched.size === 0) return;
2385
+ const declined = skipped.filter((entry) => SURVIVES_TO_RUNTIME.has(entry.reason) && entry.end > entry.start).map((entry) => [entry.start, entry.end]);
2386
+ const survivors = [];
2387
+ for (const [local, imported] of watched) for (const identifier of identifiersByName().get(local)) {
2388
+ const start = identifier.getStart();
2389
+ if (identifier.getFirstAncestorByKind(ts_morph.SyntaxKind.ImportDeclaration)) continue;
2390
+ if (applied.some(([from, to]) => start >= from && start < to)) continue;
2391
+ if (declined.some(([from, to]) => start >= from && start < to)) continue;
2392
+ if (!isValueReference(identifier)) continue;
2393
+ if (isShadowed(identifier, local)) continue;
2394
+ survivors.push({
2395
+ name: imported,
2396
+ reason: "runtime-binding",
2397
+ start,
2398
+ end: identifier.getEnd()
2399
+ });
2400
+ break;
2401
+ }
2402
+ survivors.sort((a, b) => a.start - b.start);
2403
+ skipped.push(...survivors);
2404
+ }
2405
+ if (reportSurvivors) reportRuntimeBindings();
2406
+ if (folded.length === 0) return {
2407
+ code,
2408
+ map: null,
2409
+ folded,
2410
+ skipped,
2411
+ dependencies: []
2412
+ };
2413
+ return {
2414
+ code: magic.toString(),
2415
+ map: magic.generateMap({
2416
+ source: options.filePath,
2417
+ hires: true,
2418
+ includeContent: true
2419
+ }),
2420
+ folded,
2421
+ skipped,
2422
+ dependencies: [...dependencyScan.results, ...foreignDependencies]
2423
+ };
2424
+ };
2425
+ //#endregion
2426
+ //#region src/style-set.ts
2427
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
2428
+ /** A compound selector matches only through variant classes the recipe actually emits. */
2429
+ const matchesCompound = (compound, selection, variants) => {
2430
+ for (const [key, expected] of Object.entries(compound)) {
2431
+ if (key === "css") continue;
2432
+ const declared = variants?.[key];
2433
+ const selected = selection[key];
2434
+ if (selected == null || !declared || !Object.hasOwn(declared, String(selected))) return false;
2435
+ if (!(Array.isArray(expected) ? expected : [expected]).some((value) => value != null && String(selected) === String(value))) return false;
2436
+ }
2437
+ return true;
2438
+ };
2439
+ /**
2440
+ * Resolve the style fragments one recipe call contributes, in emitted-rule precedence.
2441
+ *
2442
+ * This intentionally rejects conditional variant *selections*. A scalar selects a style
2443
+ * object; an object such as `{ base: 'sm', md: 'lg' }` selects several objects under
2444
+ * conditions and needs a separate lowering. Returning `undefined` rejects that call instead
2445
+ * of silently compiling only one branch.
2446
+ */
2447
+ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (className) => className) => {
2448
+ const { mergeCssUncached } = (0, _bamboocss_shared.createMergeCss)(createCssContext(ctx));
2449
+ const compose = (...styles) => mergeCssUncached(...styles);
2450
+ const resolveRecipe = (config, input = {}, slot) => {
2451
+ const slots = Array.isArray(config.slots) ? config.slots : void 0;
2452
+ if (Boolean(slots) !== Boolean(slot)) return void 0;
2453
+ if (slot && !slots?.includes(slot)) return void 0;
2454
+ const selection = {
2455
+ ...config.defaultVariants ?? {},
2456
+ ...(0, _bamboocss_shared.compact)(input)
2457
+ };
2458
+ if (Object.values(selection).some((value) => isRecord(value))) return void 0;
2459
+ const fragments = [];
2460
+ const take = (candidate) => {
2461
+ if (!isRecord(candidate)) return;
2462
+ const styles = slot ? candidate[slot] : candidate;
2463
+ if (isRecord(styles)) fragments.push(styles);
2464
+ };
2465
+ take(config.base);
2466
+ for (const variant of Object.keys(config.variants ?? {})) {
2467
+ const value = selection[variant];
2468
+ if (value == null) continue;
2469
+ take(config.variants?.[variant]?.[String(value)]);
2470
+ }
2471
+ for (const compound of config.compoundVariants ?? []) {
2472
+ if (!isRecord(compound) || !matchesCompound(compound, selection, config.variants)) continue;
2473
+ take(compound.css);
2474
+ }
2475
+ return compose(...fragments);
2476
+ };
2477
+ return {
2478
+ compose,
2479
+ resolveRecipe,
2480
+ className: (...styles) => runtimeCss(...styles),
2481
+ allocateClassString
2482
+ };
2483
+ };
2484
+ //#endregion
2485
+ exports.createRuntimeCss = createRuntimeCss;
2486
+ exports.createStaticStyleSetCompiler = createStaticStyleSetCompiler;
2487
+ exports.foldSource = foldSource;