@bamboocss/vite 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1935 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ //#region \0rolldown/runtime.js
6
+ var __create = Object.create;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
14
+ key = keys[i];
15
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
16
+ get: ((k) => from[k]).bind(null, key),
17
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
18
+ });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+ //#endregion
27
+ let _bamboocss_config_ts_path = require("@bamboocss/config/ts-path");
28
+ let _bamboocss_extractor = require("@bamboocss/extractor");
29
+ let magic_string = require("magic-string");
30
+ magic_string = __toESM(magic_string);
31
+ let ts_morph = require("ts-morph");
32
+ let _bamboocss_shared = require("@bamboocss/shared");
33
+ let node_path = require("node:path");
34
+ let _bamboocss_logger = require("@bamboocss/logger");
35
+ let _bamboocss_node = require("@bamboocss/node");
36
+ //#region src/fold-partial.ts
37
+ /**
38
+ * Statically resolvable means: every box in the tree carries a known value.
39
+ *
40
+ * `unresolvable` is the extractor saying it could not evaluate a node.
41
+ * `conditional` is a ternary — two possible values, so there is no single string to
42
+ * fold to. `box.fallback` produces an object with no `type` at all, which is likewise
43
+ * not something we can trust.
44
+ */
45
+ const isStaticBox = (node, seen = /* @__PURE__ */ new Set()) => {
46
+ if (!node) return false;
47
+ if (seen.has(node)) return true;
48
+ seen.add(node);
49
+ if (_bamboocss_extractor.box.isUnresolvable(node) || _bamboocss_extractor.box.isConditional(node)) return false;
50
+ if (!("type" in node) || node.type == null) return false;
51
+ if (_bamboocss_extractor.box.isLiteral(node) && node.value === void 0) return false;
52
+ if (_bamboocss_extractor.box.isMap(node)) {
53
+ for (const child of node.value.values()) if (!isStaticBox(child, seen)) return false;
54
+ return true;
55
+ }
56
+ if (_bamboocss_extractor.box.isArray(node)) {
57
+ for (const child of node.value) if (!isStaticBox(child, seen)) return false;
58
+ return true;
59
+ }
60
+ return true;
61
+ };
62
+ /**
63
+ * Strip the wrappers the extractor strips before it builds a box, so the source node
64
+ * compared against a box is the same node the box was built from.
65
+ *
66
+ * A local copy of the extractor's `unwrapExpression`, which is not part of its public
67
+ * surface. Recognising fewer wrappers than it does is not a cosmetic difference: an
68
+ * unrecognised one leaves an object literal wrapped, and the object checks below skip it.
69
+ */
70
+ 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;
71
+ /**
72
+ * Mirrors the parser's evaluator environment, so re-boxing an operand here gets the same
73
+ * answer the extraction did. Left to its default, ts-evaluator presets to `NODE` and
74
+ * would resolve expressions the parser cannot see — making this check *more* permissive
75
+ * than the extraction it is auditing, which is the one thing it must never be.
76
+ */
77
+ const REBOXED = { getEvaluateOptions: () => ({ environment: { preset: "ECMA" } }) };
78
+ const rebox = (node) => (0, _bamboocss_extractor.maybeBoxNode)(unwrapExpression(node), [], REBOXED);
79
+ /**
80
+ * Did this operand resolve to a value the program will actually produce?
81
+ *
82
+ * Producing a box is not enough when the operand is itself a choice: `a || b || c` parses
83
+ * as `(a || b) || c`, so the outer operator is handed whatever the inner one answered —
84
+ * including an arm the extractor invented. Asking only "is there a box" reads that
85
+ * invention as an ordinary literal.
86
+ */
87
+ const resolvesExactly = (node) => {
88
+ const inner = unwrapExpression(node);
89
+ if (!rebox(inner)) return false;
90
+ return ts_morph.Node.isConditionalExpression(inner) || isCollapsedBinary(inner) ? decidedAtBuildTime(inner) : true;
91
+ };
92
+ /**
93
+ * Is this operand's value written here, rather than named?
94
+ *
95
+ * A box records what the extractor resolved a name *through* — a `let`'s initializer, a
96
+ * parameter's default — none of which is what the operand holds when the call runs.
97
+ * `let m = '1'; m = undefined` still boxes as `'1'`, and `({ c = 'red.300' })` still boxes
98
+ * as `'red.300'` for a caller that passed something else. Only a value written at the call
99
+ * site is what it appears to be, so only that can be judged truthy or nullish here.
100
+ */
101
+ const isWrittenHere = (node) => {
102
+ const inner = unwrapExpression(node);
103
+ 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());
104
+ };
105
+ /** An inline value's truthiness, which its box carries directly. */
106
+ 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);
107
+ /**
108
+ * Did the extractor *decide* this choice, or guess at it?
109
+ *
110
+ * `a ? b : c`, `a || b` and `a && b` are asked "what styles could this produce", and when
111
+ * one arm does not evaluate the extractor answers with the other rather than refusing
112
+ * (`maybe-box-node.ts`, `whenTrueValue && !whenFalseValue`). That is right for generating
113
+ * CSS — emit rules for whatever might be used — and wrong for rewriting source, where the
114
+ * arm it kept becomes the only one that runs.
115
+ *
116
+ * For a ternary the tell is the arms: it guessed exactly when one produced a box and the
117
+ * other did not. For a short-circuit the answer is always the left operand, so what has to
118
+ * be established is that the left is the side that wins.
119
+ */
120
+ const decidedAtBuildTime = (node) => {
121
+ if (ts_morph.Node.isConditionalExpression(node)) return resolvesExactly(node.getWhenTrue()) && resolvesExactly(node.getWhenFalse());
122
+ const operator = node.getOperatorToken().getKind();
123
+ if (!SHORT_CIRCUIT.includes(operator)) return false;
124
+ if (!isWrittenHere(node.getLeft())) return false;
125
+ const left = rebox(node.getLeft());
126
+ if (!left) return false;
127
+ if (operator === ts_morph.SyntaxKind.BarBarToken) return isTruthy(left);
128
+ if (operator === ts_morph.SyntaxKind.QuestionQuestionToken) return !_bamboocss_extractor.box.isLiteral(left) || left.value != null;
129
+ return isTruthy(left) ? resolvesExactly(node.getRight()) : true;
130
+ };
131
+ const SHORT_CIRCUIT = [
132
+ ts_morph.SyntaxKind.AmpersandAmpersandToken,
133
+ ts_morph.SyntaxKind.BarBarToken,
134
+ ts_morph.SyntaxKind.QuestionQuestionToken
135
+ ];
136
+ /**
137
+ * Every binary form the extractor collapses to one operand — the short-circuits plus the
138
+ * comparisons, which `isLogicalSyntax` sends down the same path even though their value is
139
+ * a boolean rather than either side.
140
+ *
141
+ * Hoisted rather than built per call: `accountsForSource` asks this for every property of
142
+ * every candidate, and rebuilding a thirteen-element list each time is not free.
143
+ */
144
+ const COLLAPSED_BINARY = new Set([
145
+ ...SHORT_CIRCUIT,
146
+ ts_morph.SyntaxKind.EqualsEqualsToken,
147
+ ts_morph.SyntaxKind.EqualsEqualsEqualsToken,
148
+ ts_morph.SyntaxKind.ExclamationEqualsToken,
149
+ ts_morph.SyntaxKind.ExclamationEqualsEqualsToken,
150
+ ts_morph.SyntaxKind.GreaterThanToken,
151
+ ts_morph.SyntaxKind.GreaterThanEqualsToken,
152
+ ts_morph.SyntaxKind.LessThanToken,
153
+ ts_morph.SyntaxKind.LessThanEqualsToken,
154
+ ts_morph.SyntaxKind.InKeyword,
155
+ ts_morph.SyntaxKind.InstanceOfKeyword
156
+ ]);
157
+ const isCollapsedBinary = (node) => ts_morph.Node.isBinaryExpression(node) && COLLAPSED_BINARY.has(node.getOperatorToken().getKind());
158
+ /**
159
+ * Does the extracted box account for every property the source declares?
160
+ *
161
+ * `isStaticBox` is not sufficient on its own. The extractor *omits* what it cannot
162
+ * evaluate rather than marking it unresolvable, so `css({ color: 'red.300', ...rest })`
163
+ * yields a perfectly static-looking map holding only `color`. Folding that produces
164
+ * `"c_red.300"` and silently drops everything `rest` contributed.
165
+ *
166
+ * So the source is the authority on what the call contains, and anything the box does
167
+ * not account for disqualifies the fold:
168
+ *
169
+ * - a declared property missing from the map (its value did not evaluate)
170
+ * - a computed key, which we cannot match against the map by name
171
+ * - a spread, unless it is an inline object literal
172
+ *
173
+ * Spreads are the conservative case. `{ ...base }` where `base` is a static local
174
+ * object *is* resolved by the extractor, but a resolved spread and an unresolved one
175
+ * are indistinguishable once flattened into the map — both just contribute keys, or
176
+ * fail to. Rather than guess, phase 1 declines them. Partial folding is where this
177
+ * gets revisited.
178
+ */
179
+ /**
180
+ * What a property's value is written as. A shorthand names it, so the name *is* the
181
+ * expression — reading an initializer that is not there reports the property as having no
182
+ * source, and everything hidden behind the name goes unchecked.
183
+ */
184
+ const valueOf = (property) => ts_morph.Node.isPropertyAssignment(property) ? property.getInitializer() : ts_morph.Node.isShorthandPropertyAssignment(property) ? property.getNameNode() : void 0;
185
+ const accountsForSource = (node, boxNode) => {
186
+ if (!node) return true;
187
+ const unwrapped = unwrapExpression(node);
188
+ if ((ts_morph.Node.isConditionalExpression(unwrapped) || isCollapsedBinary(unwrapped)) && !_bamboocss_extractor.box.isConditional(boxNode) && !decidedAtBuildTime(unwrapped)) return false;
189
+ if (ts_morph.Node.isArrayLiteralExpression(unwrapped)) {
190
+ if (!_bamboocss_extractor.box.isArray(boxNode)) return false;
191
+ const elements = unwrapped.getElements();
192
+ if (elements.length !== boxNode.value.length) return false;
193
+ return elements.every((element, index) => accountsForSource(element, boxNode.value[index]));
194
+ }
195
+ if (!ts_morph.Node.isObjectLiteralExpression(unwrapped)) {
196
+ const origin = _bamboocss_extractor.box.isMap(boxNode) ? boxNode.getNode() : void 0;
197
+ return !origin || origin === unwrapped || !ts_morph.Node.isObjectLiteralExpression(origin) ? true : accountsForSource(origin, boxNode);
198
+ }
199
+ if (!_bamboocss_extractor.box.isMap(boxNode)) return false;
200
+ for (const property of unwrapped.getProperties()) {
201
+ if (ts_morph.Node.isSpreadAssignment(property)) {
202
+ const expression = property.getExpression();
203
+ if (!ts_morph.Node.isObjectLiteralExpression(expression)) return false;
204
+ continue;
205
+ }
206
+ if (ts_morph.Node.isMethodDeclaration(property) || ts_morph.Node.isGetAccessorDeclaration(property) || ts_morph.Node.isSetAccessorDeclaration(property)) return false;
207
+ if (!ts_morph.Node.isPropertyAssignment(property) && !ts_morph.Node.isShorthandPropertyAssignment(property)) return false;
208
+ const nameNode = property.getNameNode();
209
+ if (ts_morph.Node.isComputedPropertyName(nameNode)) return false;
210
+ const key = ts_morph.Node.isStringLiteral(nameNode) || ts_morph.Node.isNumericLiteral(nameNode) ? String(nameNode.getLiteralValue()) : nameNode.getText();
211
+ const value = valueOf(property);
212
+ if (value && ts_morph.Node.isIdentifier(value) && value.getText() === "undefined") continue;
213
+ if (!boxNode.value.has(key)) return false;
214
+ if (!accountsForSource(value, boxNode.value.get(key))) return false;
215
+ }
216
+ return true;
217
+ };
218
+ /**
219
+ * A property whose value is a ternary is not dynamic, it is *finite*: both branches are
220
+ * known, so each can be resolved now and the choice left to a ternary between two
221
+ * literals. That removes the `css()` call without needing to know which branch runs.
222
+ *
223
+ * Independent conditionals stay linear rather than multiplying, because each property
224
+ * contributes its own ternary. Two conditionals give two ternaries, not four
225
+ * combinations — which is only sound because `collides()` already rules out two
226
+ * properties resolving to the same class, so no combination can interact with another.
227
+ */
228
+ const finiteBranches = (key, value, boxNode, deps) => {
229
+ if (!_bamboocss_extractor.box.isConditional(boxNode)) return void 0;
230
+ const node = boxNode.getNode();
231
+ if (!ts_morph.Node.isConditionalExpression(node)) return void 0;
232
+ if (!value || unwrapExpression(value) !== node) return void 0;
233
+ const [whenTrue, whenFalse] = [[node.getWhenTrue(), boxNode.whenTrue], [node.getWhenFalse(), boxNode.whenFalse]].map(([source, branch]) => {
234
+ if (!deps.isStatic(branch)) return void 0;
235
+ if (!deps.isAccounted(source, branch)) return void 0;
236
+ const value = (0, _bamboocss_extractor.unbox)(branch).raw;
237
+ try {
238
+ return deps.runtimeCss({ [key]: value });
239
+ } catch {
240
+ return;
241
+ }
242
+ });
243
+ if (whenTrue === void 0 || whenFalse === void 0) return void 0;
244
+ if (!whenTrue && !whenFalse) return void 0;
245
+ return `${node.getCondition().getText()} ? ${JSON.stringify(whenTrue)} : ${JSON.stringify(whenFalse)}`;
246
+ };
247
+ /** The binding the leaf fold calls, exported by the generated css module. */
248
+ const LEAF_HELPER = "cssLeaf";
249
+ /**
250
+ * Memo keyed on a file, thrown away when its text is replaced.
251
+ *
252
+ * A plain `WeakMap<SourceFile, …>` is wrong here: ts-morph reuses the wrapper when a path
253
+ * is re-added with new text — which is what a watch rebuild does — so it would answer for
254
+ * the previous revision. Comparing against the text it was computed from costs a
255
+ * reference check while the file is unchanged, since `getFullText()` hands back the same
256
+ * string instance.
257
+ */
258
+ const byText = (cache, sourceFile, compute) => {
259
+ const text = sourceFile.getFullText();
260
+ const hit = cache.get(sourceFile);
261
+ if (hit && hit.text === text) return hit.value;
262
+ const value = compute();
263
+ cache.set(sourceFile, {
264
+ text,
265
+ value
266
+ });
267
+ return value;
268
+ };
269
+ /**
270
+ * The modules this file imports from, as specifiers.
271
+ *
272
+ * Only strings are cached. A ts-morph *node* cannot be: re-adding a path forgets the old
273
+ * nodes even when the text is identical, so the memo would hand back wrappers that throw
274
+ * on access. Strings outlive that, and answer the one question worth asking before the
275
+ * helper resolution walks the declarations — whether this file imports from bamboo at
276
+ * all. On a module of many elements that never fold, that walk was the entire cost of
277
+ * trying.
278
+ */
279
+ const specifierCache = /* @__PURE__ */ new WeakMap();
280
+ const importsAnything = (sourceFile, matches) => byText(specifierCache, sourceFile, () => sourceFile.getImportDeclarations().map((declaration) => declaration.getModuleSpecifierValue())).some(matches);
281
+ /**
282
+ * Every name declared at module scope, which is what an added import could collide with.
283
+ *
284
+ * This replaced `sourceFile.getLocals()`. That was precise, but it goes through the
285
+ * compiler's symbol table, and reaching for it binds the program — including every
286
+ * `.d.ts` the module's imports pull in. It cost ~8ms on a ten-line file and grew with the
287
+ * project, which was invisible while only a partial split reached it and became the
288
+ * dominant cost once open-ended values started lowering too.
289
+ *
290
+ * A syntactic walk answers the same question: a binding in a nested *function* cannot
291
+ * collide with a module-scope import, and one that shadows it *at the call site* is what
292
+ * `isShadowed` is for. Memoized against the file's text rather than the file, since
293
+ * ts-morph reuses the wrapper across a re-add and a plain `WeakMap` would answer for the
294
+ * previous revision. Uncached it is re-walked per candidate, which is quadratic in a
295
+ * module of many elements.
296
+ */
297
+ const moduleScopeCache = /* @__PURE__ */ new WeakMap();
298
+ const declaredAtModuleScope = (sourceFile) => byText(moduleScopeCache, sourceFile, () => collectModuleScopeNames(sourceFile));
299
+ const collectModuleScopeNames = (sourceFile) => {
300
+ const names = /* @__PURE__ */ new Set();
301
+ const addBinding = (node) => {
302
+ if (!node) return;
303
+ if (ts_morph.Node.isObjectBindingPattern(node) || ts_morph.Node.isArrayBindingPattern(node)) {
304
+ for (const element of node.getElements()) if (ts_morph.Node.isBindingElement(element)) addBinding(element.getNameNode());
305
+ return;
306
+ }
307
+ if (ts_morph.Node.isIdentifier(node)) names.add(node.getText());
308
+ };
309
+ const addDeclarations = (list) => {
310
+ if (ts_morph.Node.isVariableStatement(list)) {
311
+ for (const declaration of list.getDeclarations()) addBinding(declaration.getNameNode());
312
+ return;
313
+ }
314
+ if (ts_morph.Node.isVariableDeclarationList(list)) for (const declaration of list.getDeclarations()) addBinding(declaration.getNameNode());
315
+ };
316
+ const isVar = (node) => (ts_morph.Node.isVariableStatement(node) || ts_morph.Node.isVariableDeclarationList(node)) && node.getDeclarationKind() === ts_morph.VariableDeclarationKind.Var;
317
+ /**
318
+ * `var` is scoped to the enclosing *function*, not the enclosing block, so one written
319
+ * inside any statement at the top level still binds at module scope. Walking only the
320
+ * top-level statements missed every one of them, and each emitted a duplicate binding.
321
+ *
322
+ * Only statement containers are followed. A function or class body opens a new variable
323
+ * scope, so a `var` inside one cannot collide with a module-level import.
324
+ */
325
+ const addHoistedVars = (node) => {
326
+ if (isVar(node)) {
327
+ addDeclarations(node);
328
+ return;
329
+ }
330
+ if (ts_morph.Node.isBlock(node)) {
331
+ for (const statement of node.getStatements()) addHoistedVars(statement);
332
+ return;
333
+ }
334
+ if (ts_morph.Node.isIfStatement(node)) {
335
+ addHoistedVars(node.getThenStatement());
336
+ const otherwise = node.getElseStatement();
337
+ if (otherwise) addHoistedVars(otherwise);
338
+ return;
339
+ }
340
+ if (ts_morph.Node.isForStatement(node)) {
341
+ const initializer = node.getInitializer();
342
+ if (initializer) addHoistedVars(initializer);
343
+ addHoistedVars(node.getStatement());
344
+ return;
345
+ }
346
+ if (ts_morph.Node.isForInStatement(node) || ts_morph.Node.isForOfStatement(node)) {
347
+ addHoistedVars(node.getInitializer());
348
+ addHoistedVars(node.getStatement());
349
+ return;
350
+ }
351
+ if (ts_morph.Node.isWhileStatement(node) || ts_morph.Node.isDoStatement(node) || ts_morph.Node.isWithStatement(node)) {
352
+ addHoistedVars(node.getStatement());
353
+ return;
354
+ }
355
+ if (ts_morph.Node.isLabeledStatement(node)) {
356
+ addHoistedVars(node.getStatement());
357
+ return;
358
+ }
359
+ if (ts_morph.Node.isTryStatement(node)) {
360
+ addHoistedVars(node.getTryBlock());
361
+ const caught = node.getCatchClause();
362
+ if (caught) addHoistedVars(caught.getBlock());
363
+ const finally_ = node.getFinallyBlock();
364
+ if (finally_) addHoistedVars(finally_);
365
+ return;
366
+ }
367
+ if (ts_morph.Node.isSwitchStatement(node)) for (const clause of node.getCaseBlock().getClauses()) for (const statement of clause.getStatements()) addHoistedVars(statement);
368
+ };
369
+ for (const statement of sourceFile.getStatements()) {
370
+ if (ts_morph.Node.isVariableStatement(statement)) {
371
+ addDeclarations(statement);
372
+ continue;
373
+ }
374
+ if (ts_morph.Node.isImportDeclaration(statement)) {
375
+ addBinding(statement.getDefaultImport());
376
+ addBinding(statement.getNamespaceImport());
377
+ for (const named of statement.getNamedImports()) addBinding(named.getAliasNode() ?? named.getNameNode());
378
+ continue;
379
+ }
380
+ if (ts_morph.Node.isImportEqualsDeclaration(statement)) {
381
+ addBinding(statement.getNameNode());
382
+ continue;
383
+ }
384
+ 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)) {
385
+ addBinding(statement.getNameNode());
386
+ continue;
387
+ }
388
+ addHoistedVars(statement);
389
+ }
390
+ return names;
391
+ };
392
+ /**
393
+ * The local name an already-imported bamboo binding goes by.
394
+ *
395
+ * `ensureCxImport` answers this too, but it also decides whether a *missing* binding can
396
+ * be added, which needs `getLocals()` — the compiler's binder over the whole module. This
397
+ * runs for every candidate whether it folds or not, so it stops at what the import
398
+ * declarations already say and never forces that.
399
+ */
400
+ const findBambooBinding = (call, imported, isBambooCssModule, isShadowed) => {
401
+ if (!importsAnything(call.getSourceFile(), isBambooCssModule)) return void 0;
402
+ for (const declaration of call.getSourceFile().getImportDeclarations()) {
403
+ if (declaration.isTypeOnly() || !isBambooCssModule(declaration.getModuleSpecifierValue())) continue;
404
+ for (const named of declaration.getNamedImports()) {
405
+ if (named.isTypeOnly() || named.getNameNode().getText() !== imported) continue;
406
+ const local = (named.getAliasNode() ?? named.getNameNode()).getText();
407
+ return isShadowed(call, local) ? void 0 : local;
408
+ }
409
+ }
410
+ };
411
+ /**
412
+ * A value that survives the class pipeline unchanged, so the class built around it is the
413
+ * prefix and nothing else: no whitespace for `sanitize` to collapse, no `!` for the
414
+ * important regex, no space for `withoutSpace`, and nothing a token or condition could
415
+ * plausibly be named.
416
+ */
417
+ const LEAF_SENTINEL = "bamboo0leaf0sentinel0";
418
+ /**
419
+ * A property the extractor could not resolve is *open-ended* rather than finite — but its
420
+ * class is still `prefix + value`, and the prefix is known now.
421
+ *
422
+ * `utility.transform` is string construction over a table fixed at build time, and
423
+ * nothing consults which rules were emitted. So `css({ color: tone })` already returns
424
+ * `c_<tone>` for a value the extractor never saw, with no CSS behind it. Emitting that
425
+ * string directly cannot be less correct than the call it replaces.
426
+ *
427
+ * The prefix is read off the real implementation rather than rebuilt: resolving a
428
+ * sentinel through `runtimeCss` applies the shorthand table and the utility's class name
429
+ * in one step. It also self-gates — a hashed or grouped class does not contain the
430
+ * sentinel, so both modes decline here without this having to read the config.
431
+ *
432
+ * Shared with the element surface, which asks the same question about a JSX style prop.
433
+ *
434
+ * Top level only, like the finite lowering beside it. A nested leaf's class carries its
435
+ * condition path, which the prefix would describe correctly — but the helper's fallback
436
+ * rebuilds `{ [prop]: value }` to hand back to `css()`, and that reconstruction has to
437
+ * carry the same path or the declined shape resolves without its condition.
438
+ */
439
+ const leafPrefix = (key, ctx, runtimeCss) => {
440
+ if (ctx.isTemplateLiteralSyntax) return void 0;
441
+ if (ctx.conditions.isCondition(key)) return void 0;
442
+ let resolved;
443
+ try {
444
+ resolved = runtimeCss({ [key]: LEAF_SENTINEL });
445
+ } catch {
446
+ return;
447
+ }
448
+ if (!resolved.endsWith(LEAF_SENTINEL)) return void 0;
449
+ const prefix = resolved.slice(0, -21);
450
+ return !prefix || prefix.includes(" ") ? void 0 : prefix;
451
+ };
452
+ /**
453
+ * Written as an object or an array, this is a condition block or a responsive list: one
454
+ * class per entry rather than one class. `leafClass` declines both at runtime and falls
455
+ * back, so lowering one is not wrong — it is a guaranteed round trip through the fallback,
456
+ * which is the same reason a condition key is declined.
457
+ */
458
+ const isWrittenAsCollection = (value) => {
459
+ const inner = unwrapExpression(value);
460
+ return ts_morph.Node.isObjectLiteralExpression(inner) || ts_morph.Node.isArrayLiteralExpression(inner);
461
+ };
462
+ /** The call this surface emits in place of the property. */
463
+ const leafCall = (prefix, key, valueText, name = LEAF_HELPER) => `${name}(${JSON.stringify(prefix)}, ${JSON.stringify(key)}, ${valueText})`;
464
+ const dynamicLeaf = (key, value, deps) => {
465
+ if (!deps.allowLeaf || !value) return void 0;
466
+ if (isWrittenAsCollection(value)) return void 0;
467
+ const prefix = leafPrefix(key, deps.ctx, deps.runtimeCss);
468
+ return prefix === void 0 ? void 0 : leafCall(prefix, key, value.getText(), deps.leafName ?? "cssLeaf");
469
+ };
470
+ /**
471
+ * Properties are partitioned whole rather than recursed into. A top-level property is
472
+ * either entirely static or entirely dynamic, which keeps the reconstructed object a
473
+ * verbatim slice of the source and avoids rebuilding nested conditions by hand.
474
+ */
475
+ const planPartialFold = (argument, boxNode, styles, deps) => {
476
+ const partition = partitionObject(argument, boxNode, styles, deps);
477
+ if (!partition) return void 0;
478
+ const className = deps.runtimeCss(partition.staticStyles);
479
+ if (!className && !partition.finite.length) return void 0;
480
+ return {
481
+ className,
482
+ dynamicText: partition.dynamicText.length ? `{ ${partition.dynamicText.join(", ")} }` : void 0,
483
+ finite: partition.finite,
484
+ finiteFirst: partition.finiteFirst
485
+ };
486
+ };
487
+ /**
488
+ * Split one object level, recursing into a block that is part static and part dynamic.
489
+ *
490
+ * Without the recursion a single dynamic leaf sends its whole block to the runtime:
491
+ * `{ _hover: { color: 'red.300', bg: p } }` loses the resolved `color` even though
492
+ * nothing about it depends on `p`. That is a precision loss rather than a wrong answer,
493
+ * but it costs exactly the calls a component re-renders most.
494
+ *
495
+ * A class is identified by its condition path *and* its property, so `_hover.color` in
496
+ * one half and `_hover.bg` in the other cannot collide, and neither can `color` against
497
+ * `_hover.color`. Collision is therefore checked per level, among siblings.
498
+ *
499
+ * The static subtree is read from the extracted data rather than rebuilt: the extractor
500
+ * has already dropped the unresolvable leaves, so `styles[key]` for a mixed block is
501
+ * exactly the resolvable part. The dynamic side is taken from source text, so nothing
502
+ * depends on that pruning being complete.
503
+ */
504
+ const partitionObject = (node, boxNode, styles, deps, topLevel = true) => {
505
+ if (!_bamboocss_extractor.box.isMap(boxNode)) return void 0;
506
+ const { ctx, isAccounted, isStatic } = deps;
507
+ const staticKeys = [];
508
+ const staticStyles = {};
509
+ const seenKeys = /* @__PURE__ */ new Set();
510
+ /**
511
+ * Everything not resolved outright, in source order. Kept as one list because a ternary
512
+ * that cannot be lowered has to become a runtime property *in its original position*,
513
+ * and because whether the two kinds interleave is a property of that order.
514
+ */
515
+ const slots = [];
516
+ for (const property of node.getProperties()) {
517
+ if (!ts_morph.Node.isPropertyAssignment(property) && !ts_morph.Node.isShorthandPropertyAssignment(property)) return void 0;
518
+ const nameNode = property.getNameNode();
519
+ if (ts_morph.Node.isComputedPropertyName(nameNode)) return void 0;
520
+ const key = ts_morph.Node.isStringLiteral(nameNode) || ts_morph.Node.isNumericLiteral(nameNode) ? String(nameNode.getLiteralValue()) : nameNode.getText();
521
+ if (seenKeys.has(key)) return void 0;
522
+ seenKeys.add(key);
523
+ const value = valueOf(property);
524
+ const valueBox = boxNode.value.get(key);
525
+ if (key in styles && isStatic(valueBox) && isAccounted(value, valueBox)) {
526
+ staticKeys.push(key);
527
+ staticStyles[key] = styles[key];
528
+ continue;
529
+ }
530
+ if (topLevel) {
531
+ const branches = finiteBranches(key, value, valueBox, deps);
532
+ if (branches) {
533
+ slots.push({
534
+ key,
535
+ kind: "finite",
536
+ lowered: {
537
+ expression: branches,
538
+ emitsLiterals: true
539
+ },
540
+ text: property.getText()
541
+ });
542
+ continue;
543
+ }
544
+ const leaf = dynamicLeaf(key, value, deps);
545
+ if (leaf) {
546
+ slots.push({
547
+ key,
548
+ kind: "finite",
549
+ lowered: {
550
+ expression: leaf,
551
+ emitsLiterals: false
552
+ },
553
+ text: property.getText()
554
+ });
555
+ continue;
556
+ }
557
+ }
558
+ const nested = value && ts_morph.Node.isObjectLiteralExpression(value) && isAccounted(value, valueBox) ? partitionObject(value, valueBox, styles[key] ?? {}, deps, false) : void 0;
559
+ if (nested && Object.keys(nested.staticStyles).length && nested.dynamicText.length) {
560
+ staticKeys.push(key);
561
+ staticStyles[key] = nested.staticStyles;
562
+ slots.push({
563
+ key,
564
+ kind: "dynamic",
565
+ split: true,
566
+ text: `${property.getNameNode().getText()}: { ${nested.dynamicText.join(", ")} }`
567
+ });
568
+ continue;
569
+ }
570
+ slots.push({
571
+ key,
572
+ kind: "dynamic",
573
+ text: property.getText()
574
+ });
575
+ }
576
+ const demote = (slot) => {
577
+ slot.kind = "dynamic";
578
+ slot.lowered = void 0;
579
+ };
580
+ const contested = () => slots.filter((slot) => slot.kind === "dynamic" && !slot.split).map((slot) => slot.key);
581
+ for (;;) {
582
+ const lowered = slots.filter((slot) => slot.kind === "finite");
583
+ const dynamic = contested();
584
+ const offender = lowered.find((slot, index) => collides([slot.key], [
585
+ ...staticKeys,
586
+ ...dynamic,
587
+ ...lowered.slice(0, index).map((kept) => kept.key)
588
+ ], ctx));
589
+ if (!offender) break;
590
+ demote(offender);
591
+ }
592
+ const written = () => slots.map((slot) => slot.kind === "finite" ? "f" : "d").join("");
593
+ while (!/^f*d*$/.test(written()) && !/^d*f*$/.test(written())) demote(slots.findLast((slot) => slot.kind === "finite"));
594
+ const dynamicText = slots.filter((slot) => slot.kind === "dynamic").map((slot) => slot.text);
595
+ const finite = slots.filter((slot) => slot.kind === "finite").map((slot) => slot.lowered);
596
+ if (!staticKeys.length && !finite.length) return void 0;
597
+ if (!dynamicText.length && !finite.length) return void 0;
598
+ if (collides(staticKeys, contested(), ctx)) return void 0;
599
+ return {
600
+ staticStyles,
601
+ dynamicText,
602
+ finite,
603
+ finiteFirst: !written().startsWith("d")
604
+ };
605
+ };
606
+ /**
607
+ * Do the two halves resolve to a shared property?
608
+ *
609
+ * Compared after shorthand resolution, since that is where distinct keys become the same
610
+ * property. An unrecognised key resolves to itself, so two distinct unknown keys are read
611
+ * as distinct — which is right for atomic output, where one class is emitted per key.
612
+ */
613
+ const collides = (staticKeys, dynamicKeys, ctx) => {
614
+ if (staticKeys.includes("base") || dynamicKeys.includes("base")) return true;
615
+ const resolve = (key) => ctx.utility.hasShorthand ? ctx.utility.resolveShorthand(key) : key;
616
+ const resolvedStatic = new Set(staticKeys.map(resolve));
617
+ return dynamicKeys.some((key) => resolvedStatic.has(resolve(key)));
618
+ };
619
+ /**
620
+ * The `cx` binding to call, adding it to the import that already brings in the style
621
+ * helper when it is not there yet.
622
+ *
623
+ * Reusing that declaration rather than writing a new one avoids having to guess the
624
+ * module specifier, which varies with `importMap`, path aliases and how the project
625
+ * spells its outdir.
626
+ */
627
+ const ensureCxImport = (call, calleeRoot, isBambooCssModule, isGeneratedCssModule, isShadowed, extra = []) => {
628
+ const sourceFile = call.getSourceFile();
629
+ const wanted = ["cx", ...extra];
630
+ const resolved = {};
631
+ let host;
632
+ for (const declaration of sourceFile.getImportDeclarations()) {
633
+ const mod = declaration.getModuleSpecifierValue();
634
+ for (const named of declaration.getNamedImports()) {
635
+ const local = (named.getAliasNode() ?? named.getNameNode()).getText();
636
+ const imported = named.getNameNode().getText();
637
+ if (wanted.includes(imported) && !(imported in resolved)) {
638
+ if (declaration.isTypeOnly() || named.isTypeOnly()) return void 0;
639
+ if (!isBambooCssModule(mod)) return void 0;
640
+ if (isShadowed(call, local)) return void 0;
641
+ resolved[imported] = local;
642
+ }
643
+ if (local === calleeRoot) host = declaration;
644
+ }
645
+ }
646
+ const missing = wanted.filter((name) => !(name in resolved));
647
+ if (!missing.length) return {
648
+ name: resolved.cx,
649
+ names: resolved
650
+ };
651
+ if (!host) return void 0;
652
+ if (!isGeneratedCssModule(host.getModuleSpecifierValue())) return void 0;
653
+ const declared = declaredAtModuleScope(sourceFile);
654
+ for (const name of missing) {
655
+ if (declared.has(name) || isShadowed(call, name)) return void 0;
656
+ resolved[name] = name;
657
+ }
658
+ const last = host.getNamedImports().at(-1);
659
+ if (!last) return void 0;
660
+ return {
661
+ name: resolved.cx,
662
+ names: resolved,
663
+ insert: {
664
+ pos: last.getEnd(),
665
+ names: missing
666
+ }
667
+ };
668
+ };
669
+ /**
670
+ * The `css` and `cx` bindings a partially folded JSX element needs.
671
+ *
672
+ * Splitting an element sends its dynamic style props to a `css()` call, so unlike the
673
+ * call-site split this needs *two* bindings rather than one. Both are taken from an
674
+ * existing bamboo `css` import: writing a new import declaration would mean guessing a
675
+ * module specifier, and the spelling varies with `importMap`, path aliases and how the
676
+ * project reaches its outdir. An element in a file that does not already import `css` is
677
+ * left alone instead.
678
+ */
679
+ const resolveCssHelpers = (node, isBambooCssModule, isGeneratedCssModule, isShadowed, wantLeaf = false) => {
680
+ const sourceFile = node.getSourceFile();
681
+ if (!importsAnything(sourceFile, isBambooCssModule)) return void 0;
682
+ for (const declaration of sourceFile.getImportDeclarations()) {
683
+ const mod = declaration.getModuleSpecifierValue();
684
+ if (declaration.isTypeOnly() || !isBambooCssModule(mod)) continue;
685
+ const named = declaration.getNamedImports();
686
+ const cssImport = named.find((entry) => entry.getNameNode().getText() === "css" && !entry.isTypeOnly());
687
+ if (!cssImport) continue;
688
+ const cssName = (cssImport.getAliasNode() ?? cssImport.getNameNode()).getText();
689
+ if (isShadowed(node, cssName)) return void 0;
690
+ const wanted = wantLeaf ? ["cx", LEAF_HELPER] : ["cx"];
691
+ const resolved = {};
692
+ const missing = [];
693
+ for (const want of wanted) {
694
+ const existing = named.find((entry) => entry.getNameNode().getText() === want && !entry.isTypeOnly());
695
+ if (existing) {
696
+ const local = (existing.getAliasNode() ?? existing.getNameNode()).getText();
697
+ if (isShadowed(node, local)) return void 0;
698
+ resolved[want] = local;
699
+ continue;
700
+ }
701
+ missing.push(want);
702
+ }
703
+ if (!missing.length) return {
704
+ css: cssName,
705
+ cx: resolved.cx,
706
+ leaf: resolved[LEAF_HELPER]
707
+ };
708
+ if (!isGeneratedCssModule(mod)) return void 0;
709
+ const declared = declaredAtModuleScope(sourceFile);
710
+ for (const name of missing) {
711
+ if (isShadowed(node, name) || declared.has(name)) return void 0;
712
+ resolved[name] = name;
713
+ }
714
+ const last = named.at(-1);
715
+ if (!last) return void 0;
716
+ return {
717
+ css: cssName,
718
+ cx: resolved.cx,
719
+ leaf: resolved[LEAF_HELPER],
720
+ insert: {
721
+ pos: last.getEnd(),
722
+ names: missing
723
+ }
724
+ };
725
+ }
726
+ };
727
+ //#endregion
728
+ //#region src/fold-jsx.ts
729
+ const RANK = {
730
+ constant: 0,
731
+ reads: 1,
732
+ unknown: 2
733
+ };
734
+ const worst = (a, b) => RANK[a] >= RANK[b] ? a : b;
735
+ const purityOf = (node) => {
736
+ if (ts_morph.Node.isIdentifier(node)) return "reads";
737
+ if (ts_morph.Node.isStringLiteral(node) || ts_morph.Node.isNumericLiteral(node) || ts_morph.Node.isNoSubstitutionTemplateLiteral(node) || ts_morph.Node.isTrueLiteral(node) || ts_morph.Node.isFalseLiteral(node) || node.getKind() === ts_morph.SyntaxKind.NullKeyword) return "constant";
738
+ if (ts_morph.Node.isPrefixUnaryExpression(node) && ts_morph.Node.isNumericLiteral(node.getOperand())) return "constant";
739
+ if (ts_morph.Node.isObjectLiteralExpression(node)) return node.getProperties().reduce((acc, property) => {
740
+ if (!ts_morph.Node.isPropertyAssignment(property)) return "unknown";
741
+ if (ts_morph.Node.isComputedPropertyName(property.getNameNode())) return "unknown";
742
+ const value = property.getInitializer();
743
+ return worst(acc, value ? purityOf(value) : "unknown");
744
+ }, "constant");
745
+ if (ts_morph.Node.isArrayLiteralExpression(node)) return node.getElements().reduce((acc, element) => worst(acc, purityOf(element)), "constant");
746
+ return "unknown";
747
+ };
748
+ /**
749
+ * The same question for a whole attribute.
750
+ *
751
+ * A `JsxElement` or `JsxFragment` initializer — `title=<Tag x={f()} />` — is legal, holds
752
+ * arbitrary expressions, and is not a `JsxExpression`, so this asks what an initializer
753
+ * *is* rather than listing the kinds that carry code.
754
+ */
755
+ const attributePurity = (attribute) => {
756
+ const initializer = attribute.getInitializer();
757
+ if (!initializer) return "constant";
758
+ if (ts_morph.Node.isStringLiteral(initializer)) return "constant";
759
+ if (!ts_morph.Node.isJsxExpression(initializer)) return "unknown";
760
+ const expression = initializer.getExpression();
761
+ return expression ? purityOf(expression) : "constant";
762
+ };
763
+ /**
764
+ * Props the factory gives extra *styling* meaning to, which is why they block a fold.
765
+ *
766
+ * `unstyled` skips the recipe and `css` merges at a higher precedence than the style
767
+ * props, so neither is expressible as a class the fold can compute alongside the rest.
768
+ *
769
+ * `ref`, `key` and `children` used to be here too, and do not belong: none of them
770
+ * changes what the element is styled with. The factory takes `ref` through `forwardRef`
771
+ * and hands it straight to `createElement`, so an intrinsic tag — or whatever `as` names
772
+ * — receives the identical prop. `key` never reaches the component at all; React consumes
773
+ * it. And `children ?? combinedProps.children` mirrors `createElement`'s own rule that
774
+ * the third argument beats `props.children`. Each is a passthrough, and travels as one.
775
+ */
776
+ const RESERVED_PROPS = new Set(["unstyled", "css"]);
777
+ /**
778
+ * Props that carry no styling, and so block a fold only where the framework gives them a
779
+ * meaning the rewrite would change.
780
+ */
781
+ const MECHANICAL_PROPS = new Set([
782
+ "ref",
783
+ "key",
784
+ "children"
785
+ ]);
786
+ /**
787
+ * Where a mechanical prop may travel as an ordinary passthrough.
788
+ *
789
+ * React only, and measured rather than reasoned about. Its factory forwards the ref to
790
+ * the element it renders, so moving the ref onto that element changes nothing.
791
+ *
792
+ * Preact was in this list on the strength of `forwardRef` appearing in its factory too,
793
+ * and that inference was wrong: under this repo's compat setup an unfolded
794
+ * `<styled.div ref={r}>` binds the *component instance* and the folded `<div ref={r}>`
795
+ * binds the DOM node. A hand-written `forwardRef` behaves the same way, so it is Preact's
796
+ * own handling rather than anything the factory does. Vue diverges for the plain reason —
797
+ * a ref on a component is the instance, on an element it is the node.
798
+ *
799
+ * An allowlist rather than a denylist, because the failure is silent and the list of
800
+ * runtimes is open. Every other framework keeps the behaviour it had before.
801
+ */
802
+ const MECHANICAL_FRAMEWORKS = new Set(["react"]);
803
+ /**
804
+ * Is this a tag JSX reads as an intrinsic element?
805
+ *
806
+ * Anchored and dot-free, because both halves matter. `Section` is a variable reference,
807
+ * and `foo.bar` is a member expression — a property read off something in scope — where
808
+ * the runtime would have created an element named literally that.
809
+ */
810
+ const isIntrinsicTag = (tag) => /^[a-z][\w-]*$/.test(tag);
811
+ /**
812
+ * The tag an `as` prop names, when it names one statically.
813
+ *
814
+ * The factory destructures `{ as: Element = __base__ }` and hands `Element` to
815
+ * `createElement`, so a static `as` is simply a different tag with the same class and
816
+ * the same forwarded props — `splitProps` keys off the factory's own config, not off
817
+ * what `as` points at, so the split is unchanged.
818
+ *
819
+ * Casing is load-bearing, because JSX and `createElement` disagree about it. JSX reads a
820
+ * lowercase tag as an intrinsic element and a capitalised one as a variable, while
821
+ * `createElement` takes a string as intrinsic and anything else as a component. So the
822
+ * two forms only survive the rewrite when their casing already agrees:
823
+ *
824
+ * - `as="section"` -> `<section>`, intrinsic both ways.
825
+ * - `as={Link}` -> `<Link>`, a component reference both ways.
826
+ *
827
+ * The mismatched pair render something else entirely. `as={thing}` would fold to
828
+ * `<thing>`, a DOM element named `thing` rather than the component; `as="Section"` would
829
+ * fold to `<Section>`, a variable reference rather than the intrinsic the factory would
830
+ * have created. Both bail.
831
+ *
832
+ * A dot is the same hazard spelled differently, and it survives lowercasing. `<foo.bar>`
833
+ * is a JSX member expression — `createElement(foo.bar)`, a property read off a variable
834
+ * in scope — where the factory would have created an intrinsic element named literally
835
+ * `foo.bar`. So a dotted value bails even though its casing agrees.
836
+ */
837
+ const asTag = (attribute) => {
838
+ const initializer = attribute.getInitializer();
839
+ if (!initializer) return void 0;
840
+ if (ts_morph.Node.isStringLiteral(initializer)) {
841
+ const value = initializer.getLiteralValue();
842
+ return /^[a-z][\w-]*$/.test(value) ? value : void 0;
843
+ }
844
+ if (!ts_morph.Node.isJsxExpression(initializer)) return void 0;
845
+ const expression = initializer.getExpression();
846
+ if (!expression || !ts_morph.Node.isIdentifier(expression)) return void 0;
847
+ const name = expression.getText();
848
+ return /^[A-Z]/.test(name) ? name : void 0;
849
+ };
850
+ /**
851
+ * `normalizeHTMLProps` renames these on the way to the DOM (`htmlSize` -> `size`).
852
+ * Reproducing the rename is easy; noticing that it exists is the hard part, so they bail.
853
+ */
854
+ const HTML_PROPS = new Set([
855
+ "htmlSize",
856
+ "htmlTranslate",
857
+ "htmlWidth",
858
+ "htmlHeight"
859
+ ]);
860
+ /**
861
+ * The intrinsic tag a factory expression names, if it names one statically.
862
+ *
863
+ * Only `styled.div` and friends fold. `styled(Component)` and `styled('div')` are call
864
+ * expressions whose result is bound elsewhere, and a capitalised tag is a component
865
+ * rather than an intrinsic element.
866
+ */
867
+ const intrinsicTag = (tagName, factoryName) => {
868
+ const prefix = `${factoryName}.`;
869
+ if (!tagName.startsWith(prefix)) return void 0;
870
+ const tag = tagName.slice(prefix.length);
871
+ if (!/^[a-z][a-z0-9-]*$/.test(tag)) return void 0;
872
+ return tag;
873
+ };
874
+ /**
875
+ * Collapse a pattern element (`<Stack gap="4">`) to the tag it renders.
876
+ *
877
+ * A pattern component is a second layer on top of the factory: it splits its own props
878
+ * out, runs them through the pattern's transform, and hands the result to
879
+ * `styled.<jsxElement>`, which then does everything described above. Folding one removes
880
+ * both layers.
881
+ *
882
+ * The class is computed through `patterns.transform`, the same call the encoder makes
883
+ * when it decides what css to emit — so a folded pattern class is backed by a rule by
884
+ * construction, and the render-parity test is what confirms it also matches the runtime.
885
+ *
886
+ * Only the default `jsxStyleProps: 'all'` folds. Under `minimal` and `none` the pattern's
887
+ * styles reach the factory through the `css` prop instead of being spread, which reverses
888
+ * which side wins when a prop is set in both places.
889
+ */
890
+ const planPatternFold = (item, ctx, runtimeCss) => {
891
+ const node = item.box?.getNode?.();
892
+ if (!node || !ts_morph.Node.isJsxOpeningElement(node) && !ts_morph.Node.isJsxSelfClosingElement(node)) return { reason: "unsupported-kind" };
893
+ if (ctx.jsx.styleProps !== "all") return { reason: "unsupported-kind" };
894
+ const jsxName = node.getTagNameNode().getText();
895
+ const detail = ctx.patterns.details.find((entry) => entry.jsxName === jsxName);
896
+ if (!detail) return { reason: "unsupported-kind" };
897
+ const styles = item.data?.[0] ?? {};
898
+ const passthrough = [];
899
+ let staticClassName = "";
900
+ let sawChildrenProp = false;
901
+ let tag = detail.config.jsxElement ?? "div";
902
+ let tagFromAs = false;
903
+ for (const attribute of node.getAttributes()) {
904
+ if (!ts_morph.Node.isJsxAttribute(attribute)) return { reason: "dynamic" };
905
+ const name = attribute.getNameNode().getText();
906
+ if (name === "className") {
907
+ const initializer = attribute.getInitializer();
908
+ if (!initializer || !ts_morph.Node.isStringLiteral(initializer)) return { reason: "dynamic" };
909
+ staticClassName = initializer.getLiteralValue();
910
+ continue;
911
+ }
912
+ if (name === "as") {
913
+ const resolved = asTag(attribute);
914
+ if (!resolved) return { reason: "dynamic" };
915
+ tag = resolved;
916
+ tagFromAs = true;
917
+ continue;
918
+ }
919
+ if (RESERVED_PROPS.has(name) || HTML_PROPS.has(name)) return { reason: "dynamic" };
920
+ if (MECHANICAL_PROPS.has(name) && !MECHANICAL_FRAMEWORKS.has(ctx.jsx.framework ?? "")) return { reason: "dynamic" };
921
+ if (name === "children") sawChildrenProp = true;
922
+ if (detail.props.includes(name) || ctx.isValidProperty(name)) {
923
+ if (!(name in styles)) return { reason: "dynamic" };
924
+ continue;
925
+ }
926
+ passthrough.push(attribute.getText());
927
+ }
928
+ if (!tagFromAs && !isIntrinsicTag(tag)) return { reason: "unsupported-kind" };
929
+ if (sawChildrenProp && !isIntrinsicTag(tag)) return { reason: "dynamic" };
930
+ let resolved;
931
+ try {
932
+ resolved = runtimeCss(ctx.patterns.transform(detail.baseName, styles));
933
+ } catch {
934
+ return { reason: "dynamic" };
935
+ }
936
+ const className = [resolved, staticClassName].filter(Boolean).join(" ");
937
+ if (!className) return { reason: "dynamic" };
938
+ return buildEdits(node, tag, passthrough, className);
939
+ };
940
+ const planJsxFold = (item, ctx, runtimeCss, deps) => {
941
+ const node = item.box?.getNode?.();
942
+ if (!node || !ts_morph.Node.isJsxOpeningElement(node) && !ts_morph.Node.isJsxSelfClosingElement(node)) return { reason: "unsupported-kind" };
943
+ const baseTag = intrinsicTag(node.getTagNameNode().getText(), ctx.jsx.factoryName);
944
+ if (!baseTag) return { reason: "unsupported-kind" };
945
+ let tag = baseTag;
946
+ const styles = item.data?.[0] ?? {};
947
+ const propBoxes = _bamboocss_extractor.box.isMap(item.box) ? item.box.value : void 0;
948
+ const passthrough = [];
949
+ const staticProps = [];
950
+ const dynamicProps = [];
951
+ let staticClassName = "";
952
+ let dynamicClassName = "";
953
+ let sawClassName = false;
954
+ let sawChildrenProp = false;
955
+ /**
956
+ * Where a dynamic `className` was written, and where the attributes that outlive the
957
+ * fold were.
958
+ *
959
+ * The factory appends `className` after the styles, so a folded one is emitted last and
960
+ * anything written after it runs before it instead. That covers more than the style
961
+ * props — a passthrough keeps its own place among the attributes — but only where the
962
+ * expression survives at all. A static style prop is not among them: it is *deleted*,
963
+ * its value having been resolved at build time. That is a larger change than reordering
964
+ * and a separate pre-existing gap — `<styled.div color={counted()} />` folds and never
965
+ * calls it, with or without this — rather than a reason the reordering is moot.
966
+ */
967
+ let classNameIndex = -1;
968
+ let classNamePurity = "constant";
969
+ const survivors = [];
970
+ let index = -1;
971
+ for (const attribute of node.getAttributes()) {
972
+ if (!ts_morph.Node.isJsxAttribute(attribute)) return { reason: "dynamic" };
973
+ index += 1;
974
+ const name = attribute.getNameNode().getText();
975
+ if (name === "className") {
976
+ const initializer = attribute.getInitializer();
977
+ if (!initializer) return { reason: "dynamic" };
978
+ if (sawClassName && (dynamicClassName || !ts_morph.Node.isStringLiteral(initializer))) return { reason: "dynamic" };
979
+ sawClassName = true;
980
+ if (ts_morph.Node.isStringLiteral(initializer)) {
981
+ staticClassName = initializer.getLiteralValue();
982
+ continue;
983
+ }
984
+ const expression = ts_morph.Node.isJsxExpression(initializer) ? initializer.getExpression() : void 0;
985
+ if (!expression) return { reason: "dynamic" };
986
+ dynamicClassName = expression.getText();
987
+ classNameIndex = index;
988
+ classNamePurity = purityOf(expression);
989
+ continue;
990
+ }
991
+ if (name === "as") {
992
+ const resolved = asTag(attribute);
993
+ if (!resolved) return { reason: "dynamic" };
994
+ survivors.push({
995
+ index,
996
+ purity: attributePurity(attribute)
997
+ });
998
+ tag = resolved;
999
+ continue;
1000
+ }
1001
+ if (RESERVED_PROPS.has(name) || HTML_PROPS.has(name)) return { reason: "dynamic" };
1002
+ if (MECHANICAL_PROPS.has(name) && !MECHANICAL_FRAMEWORKS.has(ctx.jsx.framework ?? "")) return { reason: "dynamic" };
1003
+ if (name === "children") sawChildrenProp = true;
1004
+ if (ctx.isValidProperty(name)) {
1005
+ const attributeValue = attribute.getInitializer();
1006
+ const valueExpression = attributeValue && ts_morph.Node.isJsxExpression(attributeValue) ? attributeValue.getExpression() : void 0;
1007
+ const propBox = propBoxes?.get(name);
1008
+ if (name in styles && isStaticBox(propBox) && accountsForSource(valueExpression, propBox)) {
1009
+ staticProps.push(name);
1010
+ continue;
1011
+ }
1012
+ if (!valueExpression) return { reason: "dynamic" };
1013
+ const expression = valueExpression;
1014
+ survivors.push({
1015
+ index,
1016
+ purity: attributePurity(attribute)
1017
+ });
1018
+ dynamicProps.push({
1019
+ name,
1020
+ text: expression.getText(),
1021
+ expression
1022
+ });
1023
+ continue;
1024
+ }
1025
+ survivors.push({
1026
+ index,
1027
+ purity: attributePurity(attribute)
1028
+ });
1029
+ passthrough.push(attribute.getText());
1030
+ }
1031
+ if (sawChildrenProp && !isIntrinsicTag(tag)) return { reason: "dynamic" };
1032
+ /**
1033
+ * Would emitting the className last move it past something that can observe it?
1034
+ *
1035
+ * A constant survivor commutes with anything. One that only reads commutes only while
1036
+ * the className expression cannot write — `className={cn} onClick={h}` is safe, and
1037
+ * `className={assigns()} bg={tone}` is not, because moving the read after the write
1038
+ * hands it the other value.
1039
+ *
1040
+ * This answers for the className and nothing else. `buildEdits` emits
1041
+ * `[...passthrough, className={cx(…)}]`, so a passthrough is also hoisted ahead of every
1042
+ * dynamic style prop's expression — `<styled.div bg={writes()} data-x={reads} />`
1043
+ * reorders those two with no className present at all. That is pre-existing and
1044
+ * reproduces on an unchanged tree; folding a dynamic className only makes it reachable
1045
+ * for more elements. Closing it means comparing every survivor against everything it
1046
+ * crosses rather than against one attribute, which is a different change.
1047
+ */
1048
+ const reordered = () => classNameIndex >= 0 && survivors.some((entry) => entry.index > classNameIndex && entry.purity !== "constant" && !(entry.purity === "reads" && classNamePurity !== "unknown"));
1049
+ if (dynamicProps.length) {
1050
+ if (!deps || item.data.length !== 1) return { reason: "dynamic" };
1051
+ if (collides(staticProps, dynamicProps.map((prop) => prop.name), ctx)) return { reason: "dynamic" };
1052
+ const resolveProp = (name) => ctx.utility.hasShorthand ? ctx.utility.resolveShorthand(name) : name;
1053
+ const claimed = /* @__PURE__ */ new Map();
1054
+ for (const prop of dynamicProps) claimed.set(resolveProp(prop.name), (claimed.get(resolveProp(prop.name)) ?? 0) + 1);
1055
+ const prefixes = /* @__PURE__ */ new Map();
1056
+ for (const prop of dynamicProps) {
1057
+ if (claimed.get(resolveProp(prop.name)) !== 1) continue;
1058
+ if (isWrittenAsCollection(prop.expression)) continue;
1059
+ const prefix = leafPrefix(prop.name, ctx, runtimeCss);
1060
+ if (prefix !== void 0) prefixes.set(prop.name, prefix);
1061
+ }
1062
+ const kinds = dynamicProps.map((prop) => prefixes.has(prop.name) ? "l" : "r").join("");
1063
+ if (!/^l*r*$/.test(kinds) && !/^r*l*$/.test(kinds)) prefixes.clear();
1064
+ if (reordered()) return { reason: "dynamic" };
1065
+ const helpers = resolveCssHelpers(node, deps.isBambooCssModule, deps.isGeneratedCssModule, deps.isShadowed, prefixes.size > 0);
1066
+ if (!helpers) return { reason: "dynamic" };
1067
+ if (!helpers.leaf) prefixes.clear();
1068
+ const staticStyles = {};
1069
+ for (const name of staticProps) staticStyles[name] = styles[name];
1070
+ const resolved = [runtimeCss(staticStyles), staticClassName].filter(Boolean).join(" ");
1071
+ const lowered = dynamicProps.filter((prop) => prefixes.has(prop.name)).map((prop) => leafCall(prefixes.get(prop.name), prop.name, prop.text, helpers.leaf));
1072
+ const residue = dynamicProps.filter((prop) => !prefixes.has(prop.name));
1073
+ const runtime = residue.length ? [`${helpers.css}({ ${residue.map((prop) => `${prop.name}: ${prop.text}`).join(", ")} })`] : [];
1074
+ if (!resolved && !lowered.length) return { reason: "dynamic" };
1075
+ const ordered = kinds.startsWith("r") ? [...runtime, ...lowered] : [...lowered, ...runtime];
1076
+ const parts = dynamicClassName ? [...ordered, dynamicClassName] : ordered;
1077
+ const plan = buildEdits(node, tag, passthrough, resolved, `${helpers.cx}(${[...resolved ? [JSON.stringify(resolved)] : [], ...parts].join(", ")})`);
1078
+ return "reason" in plan ? plan : {
1079
+ ...plan,
1080
+ insert: helpers.insert
1081
+ };
1082
+ }
1083
+ if (item.data.length !== 1) return { reason: "dynamic" };
1084
+ const className = [runtimeCss(styles), staticClassName].filter(Boolean).join(" ");
1085
+ if (dynamicClassName) {
1086
+ if (reordered()) return { reason: "dynamic" };
1087
+ const helpers = deps && resolveCssHelpers(node, deps.isBambooCssModule, deps.isGeneratedCssModule, deps.isShadowed);
1088
+ if (!helpers) return { reason: "dynamic" };
1089
+ const args = [...className ? [JSON.stringify(className)] : [], dynamicClassName];
1090
+ const plan = buildEdits(node, tag, passthrough, className, `${helpers.cx}(${args.join(", ")})`);
1091
+ return "reason" in plan ? plan : {
1092
+ ...plan,
1093
+ insert: helpers.insert
1094
+ };
1095
+ }
1096
+ if (!className) return { reason: "dynamic" };
1097
+ return buildEdits(node, tag, passthrough, className);
1098
+ };
1099
+ /** Rewrite the opening element, and the closing one when there is a pair. */
1100
+ const buildEdits = (node, tag, passthrough, className, classExpression) => {
1101
+ const attributes = [...passthrough, `className={${classExpression ?? JSON.stringify(className)}}`].join(" ");
1102
+ const selfClosing = ts_morph.Node.isJsxSelfClosingElement(node);
1103
+ const edits = [{
1104
+ start: node.getStart(),
1105
+ end: node.getEnd(),
1106
+ text: `<${tag} ${attributes}${selfClosing ? " />" : ">"}`
1107
+ }];
1108
+ let end = node.getEnd();
1109
+ if (!selfClosing) {
1110
+ const parent = node.getParent();
1111
+ if (!ts_morph.Node.isJsxElement(parent)) return { reason: "unsupported-kind" };
1112
+ const closing = parent.getClosingElement();
1113
+ edits.push({
1114
+ start: closing.getStart(),
1115
+ end: closing.getEnd(),
1116
+ text: `</${tag}>`
1117
+ });
1118
+ end = closing.getEnd();
1119
+ }
1120
+ return {
1121
+ edits,
1122
+ className,
1123
+ start: node.getStart(),
1124
+ end
1125
+ };
1126
+ };
1127
+ //#endregion
1128
+ //#region src/runtime-css.ts
1129
+ /** The shape `createCss` and `createMergeCss` both take, derived from a resolved context. */
1130
+ const createCssContext = (ctx) => ({
1131
+ grouped: ctx.config.cssMode === "grouped",
1132
+ hash: Boolean(ctx.hash.className),
1133
+ conditions: {
1134
+ shift: ctx.conditions.shift,
1135
+ finalize: ctx.conditions.finalize,
1136
+ breakpoints: { keys: ctx.conditions.breakpoints.keys }
1137
+ },
1138
+ utility: {
1139
+ prefix: ctx.utility.prefix,
1140
+ hasShorthand: ctx.utility.hasShorthand,
1141
+ resolveShorthand: ctx.utility.resolveShorthand.bind(ctx.utility),
1142
+ transform: ctx.utility.transform.bind(ctx.utility),
1143
+ toHash: ctx.utility.toHash.bind(ctx.utility)
1144
+ }
1145
+ });
1146
+ const createRuntimeCss = (ctx) => {
1147
+ const cssContext = createCssContext(ctx);
1148
+ const cssFn = (0, _bamboocss_shared.createCss)(cssContext);
1149
+ const { mergeCss } = (0, _bamboocss_shared.createMergeCss)(cssContext);
1150
+ return (...styles) => cssFn(mergeCss(...styles));
1151
+ };
1152
+ const createRuntimeRecipe = (ctx, runtimeCss) => {
1153
+ const separator = ctx.utility.separator;
1154
+ const { mergeCss } = (0, _bamboocss_shared.createMergeCss)(createCssContext(ctx));
1155
+ return (name, variants) => {
1156
+ const config = ctx.recipes.getConfig(name);
1157
+ const node = ctx.recipes.getRecipe(name);
1158
+ if (!config || !node) return void 0;
1159
+ if ("slots" in config) return void 0;
1160
+ const className = node.className;
1161
+ const { defaultVariants = {}, compoundVariants = [] } = config;
1162
+ const recipeCss = (0, _bamboocss_shared.createCss)({
1163
+ hash: Boolean(ctx.hash.className),
1164
+ conditions: {
1165
+ shift: ctx.conditions.shift,
1166
+ finalize: ctx.conditions.finalize,
1167
+ breakpoints: { keys: ctx.conditions.breakpoints.keys }
1168
+ },
1169
+ utility: {
1170
+ prefix: ctx.utility.prefix,
1171
+ hasShorthand: false,
1172
+ resolveShorthand: (prop) => prop,
1173
+ toHash: ctx.utility.toHash.bind(ctx.utility),
1174
+ transform: (prop, value) => {
1175
+ if (value === "__ignore__") return { className };
1176
+ return { className: `${className}--${prop}${separator}${(0, _bamboocss_shared.withoutSpace)(value)}` };
1177
+ }
1178
+ }
1179
+ });
1180
+ const recipeStyles = {
1181
+ [className]: "__ignore__",
1182
+ ...defaultVariants,
1183
+ ...(0, _bamboocss_shared.compact)(variants)
1184
+ };
1185
+ if (compoundVariants.length > 0 && Object.keys(recipeStyles).some((prop) => typeof variants[prop] === "object")) return;
1186
+ const compoundStyles = getCompoundVariantCss(compoundVariants, recipeStyles, mergeCss);
1187
+ return [recipeCss(recipeStyles), runtimeCss(compoundStyles)].filter(Boolean).join(" ");
1188
+ };
1189
+ };
1190
+ /**
1191
+ * Mirrors the function of the same name in the generated `cva` artifact, down to the
1192
+ * `mergeCss` it accumulates with.
1193
+ *
1194
+ * That merge has to be the deep one. More than one compound variant can match a single
1195
+ * selection, and their `css` objects then combine rather than replace: `_hover` set by
1196
+ * one and `_hover` set by another have to end up as a single condition holding both
1197
+ * declarations. `Object.assign` drops everything the earlier match contributed under a
1198
+ * shared key, which produces a shorter class list and no error at all.
1199
+ *
1200
+ * Taking `mergeCss` as an argument rather than importing one keeps it the same instance
1201
+ * the rest of the fold resolves through, built from the same context.
1202
+ */
1203
+ const getCompoundVariantCss = (compoundVariants, variantMap, mergeCss) => {
1204
+ let result = {};
1205
+ for (const compoundVariant of compoundVariants) {
1206
+ if (!compoundVariant) continue;
1207
+ if (Object.entries(compoundVariant).every(([key, value]) => {
1208
+ if (key === "css") return true;
1209
+ return (Array.isArray(value) ? value : [value]).some((entry) => variantMap[key] === entry);
1210
+ })) result = mergeCss(result, compoundVariant.css);
1211
+ }
1212
+ return result;
1213
+ };
1214
+ //#endregion
1215
+ //#region src/fold.ts
1216
+ /**
1217
+ * `cva`/`sva` return a function and `token` returns a value, so none of them can
1218
+ * collapse to a class string. Their *invocations* could, but those are separate call
1219
+ * sites the parser does not record as such.
1220
+ */
1221
+ const FOLDABLE_TYPES = new Set([
1222
+ "css",
1223
+ "pattern",
1224
+ "recipe"
1225
+ ]);
1226
+ /**
1227
+ * The class strings inside a lowered ternary — `e ? "c_red" : "c_blue"` gives both arms.
1228
+ *
1229
+ * They are read back out of the emitted text rather than threaded through the planner,
1230
+ * because the planner's product *is* that text: anything it did not write cannot appear
1231
+ * here, and anything it did cannot be missed.
1232
+ */
1233
+ const literalsIn = (expression) => [...expression.matchAll(/"((?:[^"\\]|\\.)*)"/g)].flatMap((match) => JSON.parse(match[0]).split(" ")).filter(Boolean);
1234
+ /**
1235
+ * The kinds reported as `not-foldable`, which is permanent rather than a limit of this
1236
+ * phase — hence separate from `unsupported-kind`, where a slot recipe lands because it
1237
+ * resolves to one class per slot rather than to a single string.
1238
+ */
1239
+ const UNFOLDABLE_TYPES = new Set([
1240
+ "cva",
1241
+ "sva",
1242
+ "token"
1243
+ ]);
1244
+ /** Element surfaces `foldJsx` handles, as opposed to call sites. */
1245
+ const JSX_TYPES = new Set(["jsx-factory", "jsx-pattern"]);
1246
+ /**
1247
+ * Source files a box tree reaches, other than the one being folded.
1248
+ *
1249
+ * When the extractor resolves an imported identifier it boxes the *declaration's*
1250
+ * node, which lives in the defining module. Walking the tree and reading each node's
1251
+ * source file therefore recovers exactly the files a fold depended on — narrower and
1252
+ * more accurate than treating every import of the module as a dependency.
1253
+ */
1254
+ const collectSourceFiles = (node, ctx, seen = /* @__PURE__ */ new Set()) => {
1255
+ if (!node || seen.has(node)) return;
1256
+ seen.add(node);
1257
+ ctx.record(node.getNode?.());
1258
+ if (_bamboocss_extractor.box.isMap(node)) {
1259
+ for (const child of node.value.values()) collectSourceFiles(child, ctx, seen);
1260
+ return;
1261
+ }
1262
+ if (_bamboocss_extractor.box.isArray(node)) for (const child of node.value) collectSourceFiles(child, ctx, seen);
1263
+ };
1264
+ const createDependencyScan = (ownFile) => {
1265
+ const results = /* @__PURE__ */ new Set();
1266
+ const paths = /* @__PURE__ */ new Map();
1267
+ return {
1268
+ results,
1269
+ record(node) {
1270
+ if (!node) return;
1271
+ const sourceFile = node.getSourceFile();
1272
+ if (sourceFile === ownFile) return;
1273
+ let path = paths.get(sourceFile);
1274
+ if (path === void 0) {
1275
+ path = sourceFile.getFilePath();
1276
+ paths.set(sourceFile, path);
1277
+ }
1278
+ if (path) results.add(path);
1279
+ }
1280
+ };
1281
+ };
1282
+ /**
1283
+ * The call expression to replace.
1284
+ *
1285
+ * `extractCallExpressionArguments` boxes the argument list against the call node and
1286
+ * pushes `[callNode, argNode]` onto each argument's stack, so the call is reachable
1287
+ * from either shape the parser stores: the argument array (multi-arg) or the first
1288
+ * argument's map (single-arg).
1289
+ */
1290
+ const findCallExpression = (node) => {
1291
+ const own = node.getNode?.();
1292
+ if (own && ts_morph.Node.isCallExpression(own)) return own;
1293
+ const stack = node.getStack?.() ?? [];
1294
+ for (const entry of stack) if (ts_morph.Node.isCallExpression(entry)) return entry;
1295
+ let current = own;
1296
+ for (let depth = 0; current && depth < 3; depth++) {
1297
+ if (ts_morph.Node.isCallExpression(current)) return current;
1298
+ current = current.getParent();
1299
+ }
1300
+ };
1301
+ /**
1302
+ * `css.raw(...)` must keep returning a style object — folding it to a class string
1303
+ * breaks every caller composing those styles. The file matcher strips `.raw` when it
1304
+ * normalizes function names, so the parser result cannot tell us; the callee text can.
1305
+ */
1306
+ const isRawCall = (call) => {
1307
+ if (!ts_morph.Node.isCallExpression(call)) return false;
1308
+ const callee = call.getExpression().getText();
1309
+ return callee === "raw" || callee.endsWith(".raw");
1310
+ };
1311
+ /** The identifier a callee is rooted at: `css` for `css(…)`, `panda` for `panda.css(…)`. */
1312
+ const calleeRootName = (call) => {
1313
+ if (!ts_morph.Node.isCallExpression(call)) return void 0;
1314
+ let current = call.getExpression();
1315
+ while (ts_morph.Node.isPropertyAccessExpression(current)) current = current.getExpression();
1316
+ return ts_morph.Node.isIdentifier(current) ? current.getText() : void 0;
1317
+ };
1318
+ /**
1319
+ * The identifier a JSX tag is rooted at: `styled` for `<styled.div>`, `bamboo` for
1320
+ * `<bamboo.styled.div>`. The same question `calleeRootName` answers for a call, and it
1321
+ * feeds the same import and shadowing checks.
1322
+ */
1323
+ const tagRootName = (element) => {
1324
+ if (!ts_morph.Node.isJsxOpeningElement(element) && !ts_morph.Node.isJsxSelfClosingElement(element)) return void 0;
1325
+ let current = element.getTagNameNode();
1326
+ while (ts_morph.Node.isPropertyAccessExpression(current)) current = current.getExpression();
1327
+ return ts_morph.Node.isIdentifier(current) ? current.getText() : void 0;
1328
+ };
1329
+ /**
1330
+ * Local names a module binds to an import of bamboo's own generated system.
1331
+ *
1332
+ * The parser matches by name and asks neither question this does — deliberately, since
1333
+ * for CSS extraction the worst case is a few unused rules. A transform cannot be that
1334
+ * relaxed, and it needs both halves:
1335
+ *
1336
+ * - imported at all, or a user's `const css = (s) => JSON.stringify(s)` gets rewritten
1337
+ * - imported *from bamboo*, or `import { css } from '@emotion/css'` does, which is the
1338
+ * likelier accident of the two since a migrating project has both in the tree
1339
+ *
1340
+ * Answered together and once per file, because the scan is the expensive part and both
1341
+ * answers fall out of the same pass. Per call site instead of per file, this scan
1342
+ * measured +74% on the largest sandbox module.
1343
+ */
1344
+ const bambooImportedNames = (sourceFile, ctx) => {
1345
+ const names = /* @__PURE__ */ new Set();
1346
+ for (const declaration of sourceFile.getImportDeclarations()) {
1347
+ const mod = declaration.getModuleSpecifierValue();
1348
+ for (const named of declaration.getNamedImports()) {
1349
+ const name = named.getNameNode().getText();
1350
+ const alias = named.getAliasNode()?.getText() ?? name;
1351
+ if (ctx.imports.match({
1352
+ mod,
1353
+ name,
1354
+ alias
1355
+ })) names.add(alias);
1356
+ }
1357
+ const namespace = declaration.getNamespaceImport();
1358
+ if (namespace) {
1359
+ const alias = namespace.getText();
1360
+ if (ctx.imports.match({
1361
+ mod,
1362
+ name: alias,
1363
+ alias,
1364
+ kind: "namespace"
1365
+ })) names.add(alias);
1366
+ }
1367
+ }
1368
+ return names;
1369
+ };
1370
+ /**
1371
+ * Is the callee the imported binding, or a local one that shadows it?
1372
+ *
1373
+ * A block-scoped binding of the same name is legal alongside the import, and it is
1374
+ * the one the call actually reaches. Walking ancestors is the precise answer; the
1375
+ * cost is kept off the common path by only inspecting the two node kinds that can
1376
+ * introduce a binding. Ancestors of a call in JSX are overwhelmingly elements and
1377
+ * attributes, which match neither and cost nothing.
1378
+ */
1379
+ const isShadowed = (call, name) => {
1380
+ for (let node = call.getParent(); node; node = node.getParent()) {
1381
+ if (ts_morph.Node.isSourceFile(node)) return false;
1382
+ if (bindsName(node, name)) return true;
1383
+ }
1384
+ return false;
1385
+ };
1386
+ /**
1387
+ * Does a binding name introduce `name`?
1388
+ *
1389
+ * A plain identifier check is not enough: destructuring is the likeliest way a
1390
+ * same-named local reaches a call, since `({ css }) => css(…)` is what a component
1391
+ * taking a `css` prop looks like. Nested and rest elements bind too, so the pattern
1392
+ * is walked rather than inspected at the top level.
1393
+ */
1394
+ const bindingIntroduces = (nameNode, name) => {
1395
+ if (!nameNode) return false;
1396
+ if (ts_morph.Node.isIdentifier(nameNode)) return nameNode.getText() === name;
1397
+ 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));
1398
+ return false;
1399
+ };
1400
+ const declarationsBind = (list, name) => ts_morph.Node.isVariableDeclarationList(list) && list.getDeclarations().some((declaration) => bindingIntroduces(declaration.getNameNode(), name));
1401
+ const bindsName = (scope, name) => {
1402
+ if (ts_morph.Node.isBlock(scope)) return scope.getStatements().some((statement) => statementBinds(statement, name));
1403
+ 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));
1404
+ if (ts_morph.Node.isCatchClause(scope)) return bindingIntroduces(scope.getVariableDeclaration()?.getNameNode(), name);
1405
+ if (ts_morph.Node.isForStatement(scope) || ts_morph.Node.isForOfStatement(scope) || ts_morph.Node.isForInStatement(scope)) return declarationsBind(scope.getInitializer(), name);
1406
+ return false;
1407
+ };
1408
+ const statementBinds = (statement, name) => {
1409
+ if (ts_morph.Node.isVariableStatement(statement)) return statement.getDeclarations().some((declaration) => bindingIntroduces(declaration.getNameNode(), name));
1410
+ if (ts_morph.Node.isFunctionDeclaration(statement) || ts_morph.Node.isClassDeclaration(statement)) return statement.getNameNode()?.getText() === name;
1411
+ return false;
1412
+ };
1413
+ const hasStyles = (data) => data.length > 0 && data.every((entry) => entry != null && typeof entry === "object");
1414
+ /**
1415
+ * Pair each source argument with the box the parser stored for it, and require the
1416
+ * box to account for all of it. The parser keeps either the whole argument array
1417
+ * (multi-arg calls) or just the first argument's map (single-arg calls).
1418
+ */
1419
+ const argumentsAccountedFor = (call, boxNode) => {
1420
+ if (!ts_morph.Node.isCallExpression(call)) return false;
1421
+ const args = call.getArguments();
1422
+ if (args.length === 0) return false;
1423
+ if (_bamboocss_extractor.box.isArray(boxNode) && boxNode.getNode() === call) {
1424
+ if (boxNode.value.length !== args.length) return false;
1425
+ return args.every((arg, index) => accountsForSource(arg, boxNode.value[index]));
1426
+ }
1427
+ if (args.length !== 1) return false;
1428
+ return accountsForSource(args[0], boxNode);
1429
+ };
1430
+ const foldSource = (options) => {
1431
+ const { ctx, code, parserResult, jsx = true, partial: partial_ = true, runtimeCss = createRuntimeCss(ctx) } = options;
1432
+ /**
1433
+ * Recover the static half of a call the whole-call path gave up on. Only a
1434
+ * single-argument `css()` qualifies: a pattern or recipe call takes props rather than a
1435
+ * style object, and a multi-argument `css` is later-wins across the whole object.
1436
+ */
1437
+ const tryPartial = (item, call, rootName) => {
1438
+ if (item.type !== "css" || !rootName) return void 0;
1439
+ if (!ts_morph.Node.isCallExpression(call)) return void 0;
1440
+ const args = call.getArguments();
1441
+ if (args.length !== 1) return void 0;
1442
+ const unboxed = _bamboocss_extractor.box.isMap(item.box) ? (0, _bamboocss_extractor.unbox)(item.box) : void 0;
1443
+ if (!unboxed?.raw) return void 0;
1444
+ const raw = unboxed.raw;
1445
+ if (unboxed.spreadConditions?.length) return void 0;
1446
+ const argument = args[0];
1447
+ if (!argument || !ts_morph.Node.isObjectLiteralExpression(argument)) return void 0;
1448
+ const leafName = findBambooBinding(call, LEAF_HELPER, isBambooCssModule, isShadowed);
1449
+ const plan_ = (allowLeaf) => {
1450
+ try {
1451
+ return planPartialFold(argument, item.box, raw, {
1452
+ ctx,
1453
+ runtimeCss,
1454
+ isAccounted: accountsForSource,
1455
+ isStatic: (boxNode) => isStaticBox(boxNode),
1456
+ allowLeaf,
1457
+ leafName: leafName ?? "cssLeaf"
1458
+ });
1459
+ } catch {
1460
+ return;
1461
+ }
1462
+ };
1463
+ let plan = plan_(true);
1464
+ if (!plan) return void 0;
1465
+ const usesLeaf = () => plan.finite.some((entry) => !entry.emitsLiterals);
1466
+ let cx = ensureCxImport(call, rootName, isBambooCssModule, isGeneratedCssModule, isShadowed, usesLeaf() ? [LEAF_HELPER] : []);
1467
+ if (!cx && usesLeaf()) {
1468
+ plan = plan_(false);
1469
+ if (!plan) return void 0;
1470
+ cx = ensureCxImport(call, rootName, isBambooCssModule, isGeneratedCssModule, isShadowed);
1471
+ }
1472
+ if (!cx) return void 0;
1473
+ const callee = call.getExpression().getText();
1474
+ const runtimePart = plan.dynamicText ? `${callee}(${plan.dynamicText})` : void 0;
1475
+ const runtimeParts = runtimePart ? [runtimePart] : [];
1476
+ const lowered = plan.finite.map((entry) => entry.expression);
1477
+ const parts = [...plan.className ? [JSON.stringify(plan.className)] : [], ...plan.finiteFirst ? [...lowered, ...runtimeParts] : [...runtimeParts, ...lowered]];
1478
+ if (!parts.length || parts.length === 1 && runtimePart) return void 0;
1479
+ return {
1480
+ className: plan.className,
1481
+ classNames: [plan.className, ...plan.finite.filter((entry) => entry.emitsLiterals).flatMap((entry) => literalsIn(entry.expression))].filter(Boolean),
1482
+ replacement: `${cx.name}(${parts.join(", ")})`,
1483
+ insert: cx.insert
1484
+ };
1485
+ };
1486
+ const runtimeRecipe = createRuntimeRecipe(ctx, runtimeCss);
1487
+ /**
1488
+ * Does this specifier name a module that exports the css API, exactly?
1489
+ *
1490
+ * `ImportMap.match` is substring-based, which is right for deciding whether a call is
1491
+ * bamboo's and wrong for deciding whether a module can be imported *from*:
1492
+ * `styled-system/css/css` matches while exporting no `cx`. So the comparison is
1493
+ * equality, not containment.
1494
+ *
1495
+ * A tsconfig path alias is resolved first, the same way `ImportMap.match` does. Without
1496
+ * that, `@site/styled-system/css` — the spelling this repo's own website uses — fails
1497
+ * the check and silently loses partial folding, which is indistinguishable in the
1498
+ * diagnostics from a genuinely dynamic call.
1499
+ */
1500
+ const cssModules = ctx.imports.matchers.css?.mods ?? [];
1501
+ /**
1502
+ * The generated css module, the only one whose exports are known.
1503
+ *
1504
+ * A configured `importMap.css` points at the user's own wrapper, and a wrapper that
1505
+ * re-exports `css` need not re-export `cx` — adding one there imports a binding that
1506
+ * may not exist. Reusing a `cx` the user already imported from it stays fine, since
1507
+ * that binding demonstrably resolves; only *adding* one is restricted.
1508
+ */
1509
+ const generatedCssModule = [ctx.imports.outdir, "css"].join("/");
1510
+ const pathMappings = ctx.conf.tsOptions?.pathMappings;
1511
+ const trim = (value) => value.replaceAll("\\", "/").replace(/^(?:\.\.?\/)+/, "").replace(/\/$/, "");
1512
+ const matchesModule = (mod, entries) => {
1513
+ const candidates = [mod];
1514
+ if (pathMappings) {
1515
+ const resolved = (0, _bamboocss_config_ts_path.resolveTsPathPattern)(pathMappings, mod);
1516
+ if (resolved) candidates.push(resolved);
1517
+ }
1518
+ return candidates.some((candidate) => {
1519
+ const normalized = trim(candidate);
1520
+ return entries.some((entry) => {
1521
+ const target = trim(entry);
1522
+ return normalized === target || normalized.endsWith(`/${target}`);
1523
+ });
1524
+ });
1525
+ };
1526
+ const isBambooCssModule = (mod) => matchesModule(mod, cssModules);
1527
+ const isGeneratedCssModule = (mod) => matchesModule(mod, [generatedCssModule]);
1528
+ const jsxDeps = {
1529
+ isBambooCssModule,
1530
+ isGeneratedCssModule,
1531
+ isShadowed
1532
+ };
1533
+ const folded = [];
1534
+ const skipped = [];
1535
+ const candidates = [];
1536
+ const seenRanges = /* @__PURE__ */ new Set();
1537
+ const importCache = /* @__PURE__ */ new Map();
1538
+ const importsFor = (sourceFile) => {
1539
+ let names = importCache.get(sourceFile);
1540
+ if (!names) {
1541
+ names = bambooImportedNames(sourceFile, ctx);
1542
+ importCache.set(sourceFile, names);
1543
+ }
1544
+ return names;
1545
+ };
1546
+ for (const item of parserResult.toArray()) {
1547
+ const type = item.type ?? "";
1548
+ const name = item.name ?? type;
1549
+ if (!item.box) continue;
1550
+ const call = findCallExpression(item.box);
1551
+ if (jsx && JSX_TYPES.has(type)) {
1552
+ const element = item.box.getNode?.();
1553
+ if (!element) continue;
1554
+ const elementStart = element.getStart();
1555
+ const elementEnd = element.getEnd();
1556
+ if (code.slice(elementStart, elementEnd) !== element.getText()) {
1557
+ skipped.push({
1558
+ name,
1559
+ reason: "no-call-expression",
1560
+ start: 0,
1561
+ end: 0
1562
+ });
1563
+ continue;
1564
+ }
1565
+ const rootName = tagRootName(element);
1566
+ if (!rootName || !importsFor(element.getSourceFile()).has(rootName) || isShadowed(element, rootName)) {
1567
+ skipped.push({
1568
+ name,
1569
+ reason: "not-imported",
1570
+ start: elementStart,
1571
+ end: elementEnd
1572
+ });
1573
+ continue;
1574
+ }
1575
+ const plan = type === "jsx-pattern" ? planPatternFold(item, ctx, runtimeCss) : planJsxFold(item, ctx, runtimeCss, partial_ ? jsxDeps : void 0);
1576
+ if ("reason" in plan) {
1577
+ skipped.push({
1578
+ name,
1579
+ reason: plan.reason,
1580
+ start: elementStart,
1581
+ end: elementEnd
1582
+ });
1583
+ continue;
1584
+ }
1585
+ candidates.push({
1586
+ item,
1587
+ node: element,
1588
+ edits: plan.edits,
1589
+ className: plan.className,
1590
+ insert: plan.insert,
1591
+ start: plan.start,
1592
+ end: plan.end
1593
+ });
1594
+ continue;
1595
+ }
1596
+ if (!FOLDABLE_TYPES.has(type)) {
1597
+ if (call && UNFOLDABLE_TYPES.has(type)) skipped.push({
1598
+ name,
1599
+ reason: "not-foldable",
1600
+ start: call.getStart(),
1601
+ end: call.getEnd()
1602
+ });
1603
+ continue;
1604
+ }
1605
+ if (!call) {
1606
+ skipped.push({
1607
+ name,
1608
+ reason: "no-call-expression",
1609
+ start: 0,
1610
+ end: 0
1611
+ });
1612
+ continue;
1613
+ }
1614
+ const start = call.getStart();
1615
+ const end = call.getEnd();
1616
+ if (code.slice(start, end) !== call.getText()) {
1617
+ skipped.push({
1618
+ name,
1619
+ reason: "no-call-expression",
1620
+ start: 0,
1621
+ end: 0
1622
+ });
1623
+ continue;
1624
+ }
1625
+ const rangeKey = `${start}:${end}`;
1626
+ if (seenRanges.has(rangeKey)) continue;
1627
+ seenRanges.add(rangeKey);
1628
+ if (isRawCall(call)) {
1629
+ skipped.push({
1630
+ name,
1631
+ reason: "raw-call",
1632
+ start,
1633
+ end
1634
+ });
1635
+ continue;
1636
+ }
1637
+ const rootName = calleeRootName(call);
1638
+ if (!rootName || !importsFor(call.getSourceFile()).has(rootName) || isShadowed(call, rootName)) {
1639
+ skipped.push({
1640
+ name,
1641
+ reason: "not-imported",
1642
+ start,
1643
+ end
1644
+ });
1645
+ continue;
1646
+ }
1647
+ if (!isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
1648
+ const partial = partial_ ? tryPartial(item, call, rootName) : void 0;
1649
+ if (partial) {
1650
+ candidates.push({
1651
+ item,
1652
+ call,
1653
+ node: call,
1654
+ start,
1655
+ end,
1656
+ ...partial
1657
+ });
1658
+ continue;
1659
+ }
1660
+ skipped.push({
1661
+ name,
1662
+ reason: "dynamic",
1663
+ start,
1664
+ end
1665
+ });
1666
+ continue;
1667
+ }
1668
+ candidates.push({
1669
+ item,
1670
+ call,
1671
+ node: call,
1672
+ start,
1673
+ end
1674
+ });
1675
+ }
1676
+ if (candidates.length === 0) return {
1677
+ code,
1678
+ map: null,
1679
+ folded,
1680
+ skipped,
1681
+ dependencies: []
1682
+ };
1683
+ const dependencyScan = createDependencyScan(candidates[0].node.getSourceFile());
1684
+ candidates.sort((a, b) => a.start - b.start || b.end - a.end);
1685
+ const magic = new magic_string.default(code);
1686
+ const insertedNames = /* @__PURE__ */ new Set();
1687
+ const applyInsert = (insert) => {
1688
+ if (!insert) return;
1689
+ const missing = insert.names.filter((name) => !insertedNames.has(name));
1690
+ if (!missing.length) return;
1691
+ magic.appendLeft(insert.pos, missing.map((name) => `, ${name}`).join(""));
1692
+ for (const name of missing) insertedNames.add(name);
1693
+ };
1694
+ const applied = [];
1695
+ const collides = (edits) => edits.some(([start, end]) => applied.some(([from, to]) => start < to && from < end));
1696
+ for (const candidate of candidates) {
1697
+ const { item, start, end } = candidate;
1698
+ const name = item.name ?? item.type ?? "";
1699
+ const ranges = candidate.edits ? candidate.edits.map((edit) => [edit.start, edit.end]) : [[start, end]];
1700
+ if (collides(ranges)) {
1701
+ skipped.push({
1702
+ name,
1703
+ reason: "overlapping",
1704
+ start,
1705
+ end
1706
+ });
1707
+ continue;
1708
+ }
1709
+ if (candidate.replacement) {
1710
+ magic.overwrite(start, end, candidate.replacement);
1711
+ applyInsert(candidate.insert);
1712
+ applied.push(...ranges);
1713
+ folded.push({
1714
+ name,
1715
+ className: candidate.className,
1716
+ classNames: (candidate.classNames ?? [candidate.className]).filter(Boolean),
1717
+ start,
1718
+ end
1719
+ });
1720
+ collectSourceFiles(item.box, dependencyScan);
1721
+ continue;
1722
+ }
1723
+ if (candidate.edits) {
1724
+ for (const edit of candidate.edits) magic.overwrite(edit.start, edit.end, edit.text);
1725
+ applyInsert(candidate.insert);
1726
+ applied.push(...ranges);
1727
+ folded.push({
1728
+ name,
1729
+ className: candidate.className,
1730
+ classNames: (candidate.classNames ?? [candidate.className]).filter(Boolean),
1731
+ start,
1732
+ end
1733
+ });
1734
+ collectSourceFiles(item.box, dependencyScan);
1735
+ continue;
1736
+ }
1737
+ let className;
1738
+ try {
1739
+ if (item.type === "pattern") className = runtimeCss(...item.data.map((entry) => ctx.patterns.transform(name, entry)));
1740
+ else if (item.type === "recipe") {
1741
+ const resolved = item.data.length === 1 ? runtimeRecipe(name, item.data[0]) : void 0;
1742
+ if (resolved == null) {
1743
+ skipped.push({
1744
+ name,
1745
+ reason: "unsupported-kind",
1746
+ start,
1747
+ end
1748
+ });
1749
+ continue;
1750
+ }
1751
+ className = resolved;
1752
+ } else className = runtimeCss(...item.data);
1753
+ } catch {
1754
+ skipped.push({
1755
+ name,
1756
+ reason: "dynamic",
1757
+ start,
1758
+ end
1759
+ });
1760
+ continue;
1761
+ }
1762
+ if (!className) {
1763
+ skipped.push({
1764
+ name,
1765
+ reason: "empty",
1766
+ start,
1767
+ end
1768
+ });
1769
+ continue;
1770
+ }
1771
+ magic.overwrite(start, end, JSON.stringify(className));
1772
+ applied.push(...ranges);
1773
+ folded.push({
1774
+ name,
1775
+ className,
1776
+ classNames: [className],
1777
+ start,
1778
+ end
1779
+ });
1780
+ collectSourceFiles(item.box, dependencyScan);
1781
+ }
1782
+ if (folded.length === 0) return {
1783
+ code,
1784
+ map: null,
1785
+ folded,
1786
+ skipped,
1787
+ dependencies: []
1788
+ };
1789
+ return {
1790
+ code: magic.toString(),
1791
+ map: magic.generateMap({
1792
+ source: options.filePath,
1793
+ hires: true,
1794
+ includeContent: true
1795
+ }),
1796
+ folded,
1797
+ skipped,
1798
+ dependencies: Array.from(dependencyScan.results)
1799
+ };
1800
+ };
1801
+ //#endregion
1802
+ //#region src/plugin.ts
1803
+ const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
1804
+ const NODE_MODULES = /node_modules/;
1805
+ const shouldTransform = (id) => {
1806
+ if (id.startsWith("\0")) return false;
1807
+ const [filePath] = id.split("?");
1808
+ if (!filePath) return false;
1809
+ if (NODE_MODULES.test(filePath)) return false;
1810
+ return DEFAULT_EXTENSIONS.test(filePath);
1811
+ };
1812
+ /**
1813
+ * Is this file part of the generated `styled-system` rather than the user's source?
1814
+ *
1815
+ * Resolved to a path and compared as a prefix, rather than by looking for the outdir's
1816
+ * last segment somewhere in the file's path. `outdir` is a user setting: a project that
1817
+ * generates into `src/styles` would otherwise have *every* directory named `styles`
1818
+ * treated as generated, and folding would quietly stop happening in the one place an app
1819
+ * is most likely to keep its style calls.
1820
+ *
1821
+ * `resolve` rather than `join`, so an absolute `outdir` is honoured rather than appended
1822
+ * to the cwd.
1823
+ */
1824
+ const isGeneratedOutput = (filePath, ctx) => {
1825
+ const { cwd, outdir } = ctx.config;
1826
+ if (!outdir) return false;
1827
+ const slashed = (value) => value.replaceAll("\\", "/").replace(/\/$/, "");
1828
+ const root = slashed((0, node_path.resolve)(cwd, outdir));
1829
+ const file = slashed(filePath);
1830
+ return file === root || file.startsWith(`${root}/`);
1831
+ };
1832
+ const formatSkipped = (id, skipped) => {
1833
+ const counts = /* @__PURE__ */ new Map();
1834
+ for (const entry of skipped) counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1);
1835
+ return `${id}: ${Array.from(counts.entries()).map(([reason, count]) => `${reason}=${count}`).join(" ")}`;
1836
+ };
1837
+ /**
1838
+ * Vite integration for Bamboo CSS.
1839
+ *
1840
+ * This plugin does not emit CSS — keep your existing PostCSS setup for that. Its only
1841
+ * job is the optional build-time fold.
1842
+ *
1843
+ * It runs with `enforce: 'pre'` so it sees module source as close as possible to what
1844
+ * the CSS extractor reads off disk. A plugin that rewrites style calls before bamboo
1845
+ * sees them would otherwise make the two disagree, and a folded class could end up
1846
+ * with no matching rule.
1847
+ */
1848
+ const bamboocss = (options = {}) => {
1849
+ const { transform = false, jsx, partial, configPath, cwd, reportSkipped = false, reportSummary = true } = options;
1850
+ /** Totals across the build, for the summary. */
1851
+ const totals = {
1852
+ folded: 0,
1853
+ files: 0,
1854
+ filesWithFolds: 0,
1855
+ skipped: /* @__PURE__ */ new Map()
1856
+ };
1857
+ let ctx;
1858
+ let runtimeCss;
1859
+ let setup;
1860
+ const ensureContext = async () => {
1861
+ if (!setup) setup = (0, _bamboocss_node.loadConfigAndCreateContext)({
1862
+ configPath,
1863
+ cwd
1864
+ }).then((loaded) => {
1865
+ ctx = loaded;
1866
+ runtimeCss = createRuntimeCss(loaded);
1867
+ });
1868
+ await setup;
1869
+ };
1870
+ return {
1871
+ name: "bamboocss",
1872
+ enforce: "pre",
1873
+ apply: "build",
1874
+ async buildStart() {
1875
+ if (!transform) return;
1876
+ totals.folded = 0;
1877
+ totals.files = 0;
1878
+ totals.filesWithFolds = 0;
1879
+ totals.skipped.clear();
1880
+ await ensureContext();
1881
+ },
1882
+ async transform(code, id) {
1883
+ if (!transform) return null;
1884
+ if (!shouldTransform(id)) return null;
1885
+ await ensureContext();
1886
+ if (!ctx || !runtimeCss) return null;
1887
+ const [filePath] = id.split("?");
1888
+ if (isGeneratedOutput(filePath, ctx)) return null;
1889
+ let result;
1890
+ try {
1891
+ ctx.project.addSourceFile(filePath, code);
1892
+ const parserResult = ctx.project.parseSourceFile(filePath);
1893
+ if (!parserResult || parserResult.isEmpty()) return null;
1894
+ result = foldSource({
1895
+ ctx,
1896
+ code,
1897
+ parserResult,
1898
+ filePath,
1899
+ runtimeCss,
1900
+ jsx,
1901
+ partial
1902
+ });
1903
+ } catch (error) {
1904
+ _bamboocss_logger.logger.caughtError("vite:transform", `Failed to fold ${filePath}`, error);
1905
+ return null;
1906
+ }
1907
+ totals.files++;
1908
+ totals.folded += result.folded.length;
1909
+ if (result.folded.length) totals.filesWithFolds++;
1910
+ for (const entry of result.skipped) totals.skipped.set(entry.reason, (totals.skipped.get(entry.reason) ?? 0) + 1);
1911
+ if (reportSkipped && result.skipped.length) _bamboocss_logger.logger.info("vite:transform", formatSkipped(filePath, result.skipped));
1912
+ for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
1913
+ if (!result.folded.length) return null;
1914
+ _bamboocss_logger.logger.debug("vite:transform", `Folded ${result.folded.length} call(s) in ${filePath}`);
1915
+ return {
1916
+ code: result.code,
1917
+ map: result.map
1918
+ };
1919
+ },
1920
+ buildEnd() {
1921
+ if (!transform || !reportSummary) return;
1922
+ const declined = Array.from(totals.skipped.values()).reduce((sum, count) => sum + count, 0);
1923
+ const total = totals.folded + declined;
1924
+ if (!total) return;
1925
+ const share = Math.round(totals.folded / total * 100);
1926
+ const reasons = Array.from(totals.skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
1927
+ _bamboocss_logger.logger.info("vite:transform", `Folded ${totals.folded}/${total} (${share}%) across ${totals.filesWithFolds}/${totals.files} files` + (reasons ? ` — declined: ${reasons}` : ""));
1928
+ }
1929
+ };
1930
+ };
1931
+ //#endregion
1932
+ exports.bamboocss = bamboocss;
1933
+ exports.createRuntimeCss = createRuntimeCss;
1934
+ exports.default = bamboocss;
1935
+ exports.foldSource = foldSource;