@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 +1935 -0
- package/dist/index.d.cts +154 -0
- package/dist/index.d.mts +154 -0
- package/dist/index.mjs +1905 -0
- package/package.json +59 -0
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;
|