@bamboocss/vite 1.34.1 → 1.35.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 +989 -742
- package/dist/index.d.cts +19 -264
- package/dist/index.d.mts +19 -264
- package/dist/index.mjs +987 -740
- package/package.json +12 -9
package/dist/index.cjs
CHANGED
|
@@ -26,14 +26,89 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
//#endregion
|
|
27
27
|
let _bamboocss_node = require("@bamboocss/node");
|
|
28
28
|
let _bamboocss_logger = require("@bamboocss/logger");
|
|
29
|
-
let
|
|
30
|
-
let
|
|
29
|
+
let _bamboocss_shared = require("@bamboocss/shared");
|
|
30
|
+
let _ampproject_remapping = require("@ampproject/remapping");
|
|
31
|
+
_ampproject_remapping = __toESM(_ampproject_remapping);
|
|
31
32
|
let magic_string = require("magic-string");
|
|
32
33
|
magic_string = __toESM(magic_string);
|
|
34
|
+
let postcss = require("postcss");
|
|
35
|
+
postcss = __toESM(postcss);
|
|
36
|
+
let postcss_selector_parser = require("postcss-selector-parser");
|
|
37
|
+
postcss_selector_parser = __toESM(postcss_selector_parser);
|
|
33
38
|
let node_path = require("node:path");
|
|
39
|
+
let _bamboocss_config_ts_path = require("@bamboocss/config/ts-path");
|
|
40
|
+
let _bamboocss_extractor = require("@bamboocss/extractor");
|
|
34
41
|
let ts_morph = require("ts-morph");
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
//#region src/prune-static-css.ts
|
|
43
|
+
/** The generated declaration that identifies a Bamboo stylesheet after minification. */
|
|
44
|
+
const SENTINEL = "--made-with-bamboo";
|
|
45
|
+
/**
|
|
46
|
+
* Remove source-graph atoms no transformed module can emit.
|
|
47
|
+
*
|
|
48
|
+
* `prunableClasses` contains only atoms extracted from the source graph. Explicit `staticCss`
|
|
49
|
+
* additions are absent and survive as a safelist; graph atoms are governed by the transformed
|
|
50
|
+
* module reachability set, regardless of whether they originated in `css()` or a recipe.
|
|
51
|
+
*/
|
|
52
|
+
const pruneStaticCss = (css, session, { prune = true } = {}) => {
|
|
53
|
+
if (!css.includes(SENTINEL)) return css;
|
|
54
|
+
const root = postcss.default.parse(css);
|
|
55
|
+
const isUtilityRule = (rule) => {
|
|
56
|
+
let parent = rule.parent;
|
|
57
|
+
while (parent) {
|
|
58
|
+
if (parent.type === "atrule") {
|
|
59
|
+
const atRule = parent;
|
|
60
|
+
if (atRule.name === "layer" && atRule.params === session.utilityLayer) return true;
|
|
61
|
+
}
|
|
62
|
+
parent = parent.parent;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
};
|
|
66
|
+
if (prune) root.walkRules((rule) => {
|
|
67
|
+
if (!isUtilityRule(rule)) return;
|
|
68
|
+
const classes = /* @__PURE__ */ new Set();
|
|
69
|
+
try {
|
|
70
|
+
(0, postcss_selector_parser.default)((selectors) => {
|
|
71
|
+
selectors.walkClasses((classNode) => {
|
|
72
|
+
classes.add(classNode.toString().slice(1));
|
|
73
|
+
});
|
|
74
|
+
}).processSync(rule.selector);
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (classes.size !== 1) return;
|
|
79
|
+
const [className] = classes;
|
|
80
|
+
if (!className || !session.prunableClasses.has(className) || session.usedClasses.has(className)) return;
|
|
81
|
+
rule.remove();
|
|
82
|
+
});
|
|
83
|
+
let removed = true;
|
|
84
|
+
while (removed) {
|
|
85
|
+
removed = false;
|
|
86
|
+
root.walkAtRules((rule) => {
|
|
87
|
+
if (rule.nodes?.length !== 0) return;
|
|
88
|
+
rule.remove();
|
|
89
|
+
removed = true;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (session.denseClassNames) root.walkRules((rule) => {
|
|
93
|
+
if (!isUtilityRule(rule)) return;
|
|
94
|
+
const transitionClasses = /* @__PURE__ */ new Set();
|
|
95
|
+
try {
|
|
96
|
+
rule.selector = (0, postcss_selector_parser.default)((selectors) => {
|
|
97
|
+
selectors.walkClasses((classNode) => {
|
|
98
|
+
if (!session.prunableClasses.has(classNode.toString().slice(1))) return;
|
|
99
|
+
if (session.viewTransitionClasses.has(classNode.value)) transitionClasses.add(classNode.value);
|
|
100
|
+
classNode.value = session.allocateClassString(classNode.value);
|
|
101
|
+
});
|
|
102
|
+
}).processSync(rule.selector);
|
|
103
|
+
} catch {}
|
|
104
|
+
if (transitionClasses.size) rule.walkDecls("view-transition-class", (declaration) => {
|
|
105
|
+
if (!transitionClasses.has(declaration.value)) return;
|
|
106
|
+
declaration.value = session.allocateClassString(declaration.value);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
return root.toString();
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
37
112
|
//#region src/css.ts
|
|
38
113
|
/**
|
|
39
114
|
* What a project imports to get the stylesheet.
|
|
@@ -48,6 +123,76 @@ const VIRTUAL_CSS_ID = "virtual:bamboo.css";
|
|
|
48
123
|
* not to try reading it off disk.
|
|
49
124
|
*/
|
|
50
125
|
const RESOLVED_ID = `\0${VIRTUAL_CSS_ID}`;
|
|
126
|
+
const INLINE_SOURCE_MAP = /\n?\/\/# sourceMappingURL=data:application\/json[^\n]*$/;
|
|
127
|
+
/** Rewrite one generated chunk without invalidating all mappings after the changed string. */
|
|
128
|
+
const replaceChunkReference = (chunk, bundle, previous, next, sourcemap) => {
|
|
129
|
+
if (!chunk.code.includes(previous)) return;
|
|
130
|
+
const magic = new magic_string.default(chunk.code);
|
|
131
|
+
let index = chunk.code.indexOf(previous);
|
|
132
|
+
while (index !== -1) {
|
|
133
|
+
magic.overwrite(index, index + previous.length, next);
|
|
134
|
+
index = chunk.code.indexOf(previous, index + previous.length);
|
|
135
|
+
}
|
|
136
|
+
chunk.code = magic.toString();
|
|
137
|
+
if (!chunk.map) return;
|
|
138
|
+
const file = chunk.map.file;
|
|
139
|
+
const debugId = chunk.map.debugId;
|
|
140
|
+
const combined = (0, _ampproject_remapping.default)([magic.generateMap({
|
|
141
|
+
source: chunk.fileName,
|
|
142
|
+
hires: "boundary"
|
|
143
|
+
}), chunk.map], () => null);
|
|
144
|
+
if (file) combined.file = file;
|
|
145
|
+
if (debugId) combined.debugId = debugId;
|
|
146
|
+
const rollupMap = combined;
|
|
147
|
+
rollupMap.toUrl = () => `data:application/json;charset=utf-8;base64,${Buffer.from(combined.toString()).toString("base64")}`;
|
|
148
|
+
chunk.map = rollupMap;
|
|
149
|
+
if (sourcemap === "inline") {
|
|
150
|
+
chunk.code = chunk.code.replace(INLINE_SOURCE_MAP, "");
|
|
151
|
+
chunk.code += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from(combined.toString()).toString("base64")}`;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const mapAsset = bundle[`${chunk.fileName}.map`];
|
|
155
|
+
if (mapAsset?.type === "asset") mapAsset.source = combined.toString();
|
|
156
|
+
};
|
|
157
|
+
/** Replace an emitted filename wherever Vite or Rollup has already recorded it. */
|
|
158
|
+
const replaceAssetReferences = (bundle, previous, next, sourcemap) => {
|
|
159
|
+
const replace = (value) => value.replaceAll(previous, next);
|
|
160
|
+
for (const output of Object.values(bundle)) {
|
|
161
|
+
if (output.type === "asset") {
|
|
162
|
+
if (typeof output.source === "string") output.source = replace(output.source);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
replaceChunkReference(output, bundle, previous, next, sourcemap);
|
|
166
|
+
output.referencedFiles = output.referencedFiles.map(replace);
|
|
167
|
+
const importedCss = output.viteMetadata?.importedCss;
|
|
168
|
+
if (importedCss?.delete(previous)) importedCss.add(next);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Prune and rename compiler-owned CSS, then give changed assets a hash of their final bytes.
|
|
173
|
+
*
|
|
174
|
+
* Rollup has already expanded `[hash]` when `generateBundle` runs. Mutating only `source`
|
|
175
|
+
* would therefore leave two different reachable subsets under one CDN key. The extra final
|
|
176
|
+
* hash is not cosmetic: it makes late graph reachability cache-safe.
|
|
177
|
+
*/
|
|
178
|
+
const optimizeStaticCssAssets = (bundle, session) => {
|
|
179
|
+
for (const [bundleName, output] of Object.entries(bundle)) {
|
|
180
|
+
if (output.type !== "asset") continue;
|
|
181
|
+
const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString();
|
|
182
|
+
if (!source.includes("--made-with-bamboo")) continue;
|
|
183
|
+
const optimized = pruneStaticCss(source, session);
|
|
184
|
+
output.source = optimized;
|
|
185
|
+
if (optimized === source) continue;
|
|
186
|
+
const nextName = output.fileName.replace(/\.css$/, `.b-${(0, _bamboocss_shared.toHash)(optimized)}.css`);
|
|
187
|
+
if (nextName === output.fileName) continue;
|
|
188
|
+
if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
|
|
189
|
+
const previous = output.fileName;
|
|
190
|
+
output.fileName = nextName;
|
|
191
|
+
replaceAssetReferences(bundle, previous, nextName, session.sourcemap);
|
|
192
|
+
delete bundle[bundleName];
|
|
193
|
+
bundle[nextName] = output;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
51
196
|
/**
|
|
52
197
|
* Serve bamboo's stylesheet as a virtual module, in dev and in build.
|
|
53
198
|
*
|
|
@@ -61,10 +206,11 @@ const RESOLVED_ID = `\0${VIRTUAL_CSS_ID}`;
|
|
|
61
206
|
* `styles.css` and asking the project to import it means the build reads a file the same
|
|
62
207
|
* process just wrote, which is a race on any watch rebuild.
|
|
63
208
|
*/
|
|
64
|
-
const bamboocssCss = (options
|
|
65
|
-
const { configPath, cwd } = options;
|
|
209
|
+
const bamboocssCss = (options) => {
|
|
210
|
+
const { configPath, cwd, session } = options;
|
|
66
211
|
const builder = new _bamboocss_node.Builder();
|
|
67
212
|
let server;
|
|
213
|
+
let command = "build";
|
|
68
214
|
/**
|
|
69
215
|
* Serialised, because both `load` and the watcher can reach it and `Builder` keeps one
|
|
70
216
|
* context. Two overlapping passes would extract into the same encoder and emit the
|
|
@@ -78,7 +224,33 @@ const bamboocssCss = (options = {}) => {
|
|
|
78
224
|
});
|
|
79
225
|
await builder.emit();
|
|
80
226
|
builder.extract();
|
|
81
|
-
|
|
227
|
+
if (builder.context?.config.polyfill) throw new Error("bamboocss: the cascade-layer polyfill is incompatible with compiled atomic styles. The polyfill removes the utility-layer boundary required for safe atom reachability and renaming.");
|
|
228
|
+
if (builder.context) {
|
|
229
|
+
session.cssLoaded = true;
|
|
230
|
+
session.utilityLayer = builder.context.config.layers?.utilities ?? "utilities";
|
|
231
|
+
session.extractedFiles.clear();
|
|
232
|
+
for (const file of builder.context.getFiles()) session.extractedFiles.add(builder.context.runtime.path.abs(builder.context.config.cwd, file));
|
|
233
|
+
}
|
|
234
|
+
let graphAtomHashes;
|
|
235
|
+
if (builder.context) {
|
|
236
|
+
builder.context.encoder.atomizeObservedRecipes();
|
|
237
|
+
graphAtomHashes = new Set(builder.context.encoder.atomic);
|
|
238
|
+
}
|
|
239
|
+
const css = builder.toCss({
|
|
240
|
+
layerParams: true,
|
|
241
|
+
includeRecipes: false
|
|
242
|
+
});
|
|
243
|
+
session.prunableClasses.clear();
|
|
244
|
+
session.viewTransitionClasses.clear();
|
|
245
|
+
if (graphAtomHashes && builder.context) {
|
|
246
|
+
const decoder = builder.context.decoder.collect(builder.context.encoder);
|
|
247
|
+
for (const atom of decoder.atomic) if (graphAtomHashes.has(atom.hash)) session.prunableClasses.add(atom.className);
|
|
248
|
+
for (const transition of decoder.view_transitions) {
|
|
249
|
+
session.viewTransitionClasses.add(transition.className);
|
|
250
|
+
session.prunableClasses.add((0, _bamboocss_shared.esc)(transition.className));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return command === "serve" ? pruneStaticCss(css, session, { prune: false }) : css;
|
|
82
254
|
};
|
|
83
255
|
const generate = () => {
|
|
84
256
|
pending = Promise.resolve(pending).catch(() => void 0).then(build);
|
|
@@ -86,6 +258,10 @@ const bamboocssCss = (options = {}) => {
|
|
|
86
258
|
};
|
|
87
259
|
return {
|
|
88
260
|
name: "bamboocss:css",
|
|
261
|
+
configResolved(config) {
|
|
262
|
+
command = config.command;
|
|
263
|
+
session.sourcemap = config.build.sourcemap;
|
|
264
|
+
},
|
|
89
265
|
resolveId(id) {
|
|
90
266
|
if (id === "virtual:bamboo.css") return RESOLVED_ID;
|
|
91
267
|
return null;
|
|
@@ -105,20 +281,23 @@ const bamboocssCss = (options = {}) => {
|
|
|
105
281
|
const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
|
|
106
282
|
if (!mod) return;
|
|
107
283
|
server?.moduleGraph.invalidateModule(mod);
|
|
108
|
-
server?.
|
|
109
|
-
type: "update",
|
|
110
|
-
updates: []
|
|
111
|
-
});
|
|
284
|
+
server?.reloadModule(mod);
|
|
112
285
|
_bamboocss_logger.logger.debug("vite", `styles invalidated by ${file}`);
|
|
113
286
|
};
|
|
114
287
|
devServer.watcher.on("change", invalidate);
|
|
115
288
|
devServer.watcher.on("add", invalidate);
|
|
116
289
|
devServer.watcher.on("unlink", invalidate);
|
|
290
|
+
},
|
|
291
|
+
generateBundle: {
|
|
292
|
+
order: "post",
|
|
293
|
+
handler(_, bundle) {
|
|
294
|
+
optimizeStaticCssAssets(bundle, session);
|
|
295
|
+
}
|
|
117
296
|
}
|
|
118
297
|
};
|
|
119
298
|
};
|
|
120
299
|
//#endregion
|
|
121
|
-
//#region src/fold-
|
|
300
|
+
//#region src/fold-analysis.ts
|
|
122
301
|
/**
|
|
123
302
|
* Statically resolvable means: every box in the tree carries a known value.
|
|
124
303
|
*
|
|
@@ -133,7 +312,10 @@ const isStaticBox = (node, seen = /* @__PURE__ */ new Set()) => {
|
|
|
133
312
|
seen.add(node);
|
|
134
313
|
if (_bamboocss_extractor.box.isUnresolvable(node) || _bamboocss_extractor.box.isConditional(node)) return false;
|
|
135
314
|
if (!("type" in node) || node.type == null) return false;
|
|
136
|
-
if (_bamboocss_extractor.box.isLiteral(node) && node.value === void 0)
|
|
315
|
+
if (_bamboocss_extractor.box.isLiteral(node) && node.value === void 0) {
|
|
316
|
+
const source = node.getNode?.();
|
|
317
|
+
return Boolean(source && ts_morph.Node.isIdentifier(source) && source.getText() === "undefined");
|
|
318
|
+
}
|
|
137
319
|
if (_bamboocss_extractor.box.isMap(node)) {
|
|
138
320
|
for (const child of node.value.values()) if (!isStaticBox(child, seen)) return false;
|
|
139
321
|
return true;
|
|
@@ -258,8 +440,8 @@ const isCollapsedBinary = (node) => ts_morph.Node.isBinaryExpression(node) && CO
|
|
|
258
440
|
* Spreads are the conservative case. `{ ...base }` where `base` is a static local
|
|
259
441
|
* object *is* resolved by the extractor, but a resolved spread and an unresolved one
|
|
260
442
|
* are indistinguishable once flattened into the map — both just contribute keys, or
|
|
261
|
-
* fail to. Rather than guess
|
|
262
|
-
*
|
|
443
|
+
* fail to. Rather than guess and erase evaluation the compiler cannot reproduce, the call
|
|
444
|
+
* is rejected.
|
|
263
445
|
*/
|
|
264
446
|
/**
|
|
265
447
|
* What a property's value is written as. A shorthand names it, so the name *is* the
|
|
@@ -304,37 +486,6 @@ const accountsForSource = (node, boxNode) => {
|
|
|
304
486
|
return true;
|
|
305
487
|
};
|
|
306
488
|
/**
|
|
307
|
-
* A property whose value is a ternary is not dynamic, it is *finite*: both branches are
|
|
308
|
-
* known, so each can be resolved now and the choice left to a ternary between two
|
|
309
|
-
* literals. That removes the `css()` call without needing to know which branch runs.
|
|
310
|
-
*
|
|
311
|
-
* Independent conditionals stay linear rather than multiplying, because each property
|
|
312
|
-
* contributes its own ternary. Two conditionals give two ternaries, not four
|
|
313
|
-
* combinations — which is only sound because `collides()` already rules out two
|
|
314
|
-
* properties resolving to the same class, so no combination can interact with another.
|
|
315
|
-
*/
|
|
316
|
-
const finiteBranches = (key, value, boxNode, deps) => {
|
|
317
|
-
if (!_bamboocss_extractor.box.isConditional(boxNode)) return void 0;
|
|
318
|
-
const node = boxNode.getNode();
|
|
319
|
-
if (!ts_morph.Node.isConditionalExpression(node)) return void 0;
|
|
320
|
-
if (!value || unwrapExpression(value) !== node) return void 0;
|
|
321
|
-
const [whenTrue, whenFalse] = [[node.getWhenTrue(), boxNode.whenTrue], [node.getWhenFalse(), boxNode.whenFalse]].map(([source, branch]) => {
|
|
322
|
-
if (!deps.isStatic(branch)) return void 0;
|
|
323
|
-
if (!deps.isAccounted(source, branch)) return void 0;
|
|
324
|
-
const value = (0, _bamboocss_extractor.unbox)(branch).raw;
|
|
325
|
-
try {
|
|
326
|
-
return deps.runtimeCss({ [key]: value });
|
|
327
|
-
} catch {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
if (whenTrue === void 0 || whenFalse === void 0) return void 0;
|
|
332
|
-
if (!whenTrue && !whenFalse) return void 0;
|
|
333
|
-
return `${node.getCondition().getText()} ? ${JSON.stringify(whenTrue)} : ${JSON.stringify(whenFalse)}`;
|
|
334
|
-
};
|
|
335
|
-
/** The binding the leaf fold calls, exported by the generated css module. */
|
|
336
|
-
const LEAF_HELPER = "cssLeaf";
|
|
337
|
-
/**
|
|
338
489
|
* Memo keyed on a file, thrown away when its text is replaced.
|
|
339
490
|
*
|
|
340
491
|
* A plain `WeakMap<SourceFile, …>` is wrong here: ts-morph reuses the wrapper when a path
|
|
@@ -355,18 +506,6 @@ const byText = (cache, sourceFile, compute) => {
|
|
|
355
506
|
return value;
|
|
356
507
|
};
|
|
357
508
|
/**
|
|
358
|
-
* The modules this file imports from, as specifiers.
|
|
359
|
-
*
|
|
360
|
-
* Only strings are cached. A ts-morph *node* cannot be: re-adding a path forgets the old
|
|
361
|
-
* nodes even when the text is identical, so the memo would hand back wrappers that throw
|
|
362
|
-
* on access. Strings outlive that, and answer the one question worth asking before the
|
|
363
|
-
* helper resolution walks the declarations — whether this file imports from bamboo at
|
|
364
|
-
* all. On a module of many elements that never fold, that walk was the entire cost of
|
|
365
|
-
* trying.
|
|
366
|
-
*/
|
|
367
|
-
const specifierCache = /* @__PURE__ */ new WeakMap();
|
|
368
|
-
const importsAnything = (sourceFile, matches) => byText(specifierCache, sourceFile, () => sourceFile.getImportDeclarations().map((declaration) => declaration.getModuleSpecifierValue())).some(matches);
|
|
369
|
-
/**
|
|
370
509
|
* Every name declared at module scope, which is what an added import could collide with.
|
|
371
510
|
*
|
|
372
511
|
* This replaced `sourceFile.getLocals()`. That was precise, but it goes through the
|
|
@@ -477,282 +616,6 @@ const collectModuleScopeNames = (sourceFile) => {
|
|
|
477
616
|
}
|
|
478
617
|
return names;
|
|
479
618
|
};
|
|
480
|
-
/**
|
|
481
|
-
* The local name an already-imported bamboo binding goes by.
|
|
482
|
-
*
|
|
483
|
-
* `ensureCxImport` answers this too, but it also decides whether a *missing* binding can
|
|
484
|
-
* be added, which needs `getLocals()` — the compiler's binder over the whole module. This
|
|
485
|
-
* runs for every candidate whether it folds or not, so it stops at what the import
|
|
486
|
-
* declarations already say and never forces that.
|
|
487
|
-
*/
|
|
488
|
-
const findBambooBinding = (call, imported, isBambooCssModule, isShadowed) => {
|
|
489
|
-
if (!importsAnything(call.getSourceFile(), isBambooCssModule)) return void 0;
|
|
490
|
-
for (const declaration of call.getSourceFile().getImportDeclarations()) {
|
|
491
|
-
if (declaration.isTypeOnly() || !isBambooCssModule(declaration.getModuleSpecifierValue())) continue;
|
|
492
|
-
for (const named of declaration.getNamedImports()) {
|
|
493
|
-
if (named.isTypeOnly() || named.getNameNode().getText() !== imported) continue;
|
|
494
|
-
const local = (named.getAliasNode() ?? named.getNameNode()).getText();
|
|
495
|
-
return isShadowed(call, local) ? void 0 : local;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
};
|
|
499
|
-
/**
|
|
500
|
-
* A value that survives the class pipeline unchanged, so the class built around it is the
|
|
501
|
-
* prefix and nothing else: no whitespace for `sanitize` to collapse, no `!` for the
|
|
502
|
-
* important regex, no space for `withoutSpace`, and nothing a token or condition could
|
|
503
|
-
* plausibly be named.
|
|
504
|
-
*/
|
|
505
|
-
const LEAF_SENTINEL = "bamboo0leaf0sentinel0";
|
|
506
|
-
/**
|
|
507
|
-
* A property the extractor could not resolve is *open-ended* rather than finite — but its
|
|
508
|
-
* class is still `prefix + value`, and the prefix is known now.
|
|
509
|
-
*
|
|
510
|
-
* `utility.transform` is string construction over a table fixed at build time, and
|
|
511
|
-
* nothing consults which rules were emitted. So `css({ color: tone })` already returns
|
|
512
|
-
* `c_<tone>` for a value the extractor never saw, with no CSS behind it. Emitting that
|
|
513
|
-
* string directly cannot be less correct than the call it replaces.
|
|
514
|
-
*
|
|
515
|
-
* The prefix is read off the real implementation rather than rebuilt: resolving a
|
|
516
|
-
* sentinel through `runtimeCss` applies the shorthand table and the utility's class name
|
|
517
|
-
* in one step. It also self-gates — a hashed or grouped class does not contain the
|
|
518
|
-
* sentinel, so both modes decline here without this having to read the config.
|
|
519
|
-
*
|
|
520
|
-
* Shared with the element surface, which asks the same question about a JSX style prop.
|
|
521
|
-
*
|
|
522
|
-
* Top level only, like the finite lowering beside it. A nested leaf's class carries its
|
|
523
|
-
* condition path, which the prefix would describe correctly — but the helper's fallback
|
|
524
|
-
* rebuilds `{ [prop]: value }` to hand back to `css()`, and that reconstruction has to
|
|
525
|
-
* carry the same path or the declined shape resolves without its condition.
|
|
526
|
-
*/
|
|
527
|
-
const leafPrefix = (key, ctx, runtimeCss) => {
|
|
528
|
-
if (ctx.conditions.isCondition(key)) return void 0;
|
|
529
|
-
let resolved;
|
|
530
|
-
try {
|
|
531
|
-
resolved = runtimeCss({ [key]: LEAF_SENTINEL });
|
|
532
|
-
} catch {
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
if (!resolved.endsWith(LEAF_SENTINEL)) return void 0;
|
|
536
|
-
const prefix = resolved.slice(0, -21);
|
|
537
|
-
return !prefix || prefix.includes(" ") ? void 0 : prefix;
|
|
538
|
-
};
|
|
539
|
-
/**
|
|
540
|
-
* Written as an object or an array, this is a condition block or a responsive list: one
|
|
541
|
-
* class per entry rather than one class. `leafClass` declines both at runtime and falls
|
|
542
|
-
* back, so lowering one is not wrong — it is a guaranteed round trip through the fallback,
|
|
543
|
-
* which is the same reason a condition key is declined.
|
|
544
|
-
*/
|
|
545
|
-
const isWrittenAsCollection = (value) => {
|
|
546
|
-
const inner = unwrapExpression(value);
|
|
547
|
-
return ts_morph.Node.isObjectLiteralExpression(inner) || ts_morph.Node.isArrayLiteralExpression(inner);
|
|
548
|
-
};
|
|
549
|
-
/** The call this surface emits in place of the property. */
|
|
550
|
-
const leafCall = (prefix, key, valueText, name = LEAF_HELPER) => `${name}(${JSON.stringify(prefix)}, ${JSON.stringify(key)}, ${valueText})`;
|
|
551
|
-
const dynamicLeaf = (key, value, deps) => {
|
|
552
|
-
if (!deps.allowLeaf || !value) return void 0;
|
|
553
|
-
if (isWrittenAsCollection(value)) return void 0;
|
|
554
|
-
const prefix = leafPrefix(key, deps.ctx, deps.runtimeCss);
|
|
555
|
-
return prefix === void 0 ? void 0 : leafCall(prefix, key, value.getText(), deps.leafName ?? "cssLeaf");
|
|
556
|
-
};
|
|
557
|
-
/**
|
|
558
|
-
* Properties are partitioned whole rather than recursed into. A top-level property is
|
|
559
|
-
* either entirely static or entirely dynamic, which keeps the reconstructed object a
|
|
560
|
-
* verbatim slice of the source and avoids rebuilding nested conditions by hand.
|
|
561
|
-
*/
|
|
562
|
-
const planPartialFold = (argument, boxNode, styles, deps) => {
|
|
563
|
-
const partition = partitionObject(argument, boxNode, styles, deps);
|
|
564
|
-
if (!partition) return void 0;
|
|
565
|
-
const className = deps.runtimeCss(partition.staticStyles);
|
|
566
|
-
if (!className && !partition.finite.length) return void 0;
|
|
567
|
-
return {
|
|
568
|
-
className,
|
|
569
|
-
dynamicText: partition.dynamicText.length ? `{ ${partition.dynamicText.join(", ")} }` : void 0,
|
|
570
|
-
finite: partition.finite,
|
|
571
|
-
finiteFirst: partition.finiteFirst
|
|
572
|
-
};
|
|
573
|
-
};
|
|
574
|
-
/**
|
|
575
|
-
* Split one object level, recursing into a block that is part static and part dynamic.
|
|
576
|
-
*
|
|
577
|
-
* Without the recursion a single dynamic leaf sends its whole block to the runtime:
|
|
578
|
-
* `{ _hover: { color: 'red.300', bg: p } }` loses the resolved `color` even though
|
|
579
|
-
* nothing about it depends on `p`. That is a precision loss rather than a wrong answer,
|
|
580
|
-
* but it costs exactly the calls a component re-renders most.
|
|
581
|
-
*
|
|
582
|
-
* A class is identified by its condition path *and* its property, so `_hover.color` in
|
|
583
|
-
* one half and `_hover.bg` in the other cannot collide, and neither can `color` against
|
|
584
|
-
* `_hover.color`. Collision is therefore checked per level, among siblings.
|
|
585
|
-
*
|
|
586
|
-
* The static subtree is read from the extracted data rather than rebuilt: the extractor
|
|
587
|
-
* has already dropped the unresolvable leaves, so `styles[key]` for a mixed block is
|
|
588
|
-
* exactly the resolvable part. The dynamic side is taken from source text, so nothing
|
|
589
|
-
* depends on that pruning being complete.
|
|
590
|
-
*/
|
|
591
|
-
const partitionObject = (node, boxNode, styles, deps, topLevel = true) => {
|
|
592
|
-
if (!_bamboocss_extractor.box.isMap(boxNode)) return void 0;
|
|
593
|
-
const { ctx, isAccounted, isStatic } = deps;
|
|
594
|
-
const staticKeys = [];
|
|
595
|
-
const staticStyles = {};
|
|
596
|
-
const seenKeys = /* @__PURE__ */ new Set();
|
|
597
|
-
/**
|
|
598
|
-
* Everything not resolved outright, in source order. Kept as one list because a ternary
|
|
599
|
-
* that cannot be lowered has to become a runtime property *in its original position*,
|
|
600
|
-
* and because whether the two kinds interleave is a property of that order.
|
|
601
|
-
*/
|
|
602
|
-
const slots = [];
|
|
603
|
-
for (const property of node.getProperties()) {
|
|
604
|
-
if (!ts_morph.Node.isPropertyAssignment(property) && !ts_morph.Node.isShorthandPropertyAssignment(property)) return void 0;
|
|
605
|
-
const nameNode = property.getNameNode();
|
|
606
|
-
if (ts_morph.Node.isComputedPropertyName(nameNode)) return void 0;
|
|
607
|
-
const key = ts_morph.Node.isStringLiteral(nameNode) || ts_morph.Node.isNumericLiteral(nameNode) ? String(nameNode.getLiteralValue()) : nameNode.getText();
|
|
608
|
-
if (seenKeys.has(key)) return void 0;
|
|
609
|
-
seenKeys.add(key);
|
|
610
|
-
const value = valueOf(property);
|
|
611
|
-
const valueBox = boxNode.value.get(key);
|
|
612
|
-
if (key in styles && isStatic(valueBox) && isAccounted(value, valueBox)) {
|
|
613
|
-
staticKeys.push(key);
|
|
614
|
-
staticStyles[key] = styles[key];
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
if (topLevel) {
|
|
618
|
-
const branches = finiteBranches(key, value, valueBox, deps);
|
|
619
|
-
if (branches) {
|
|
620
|
-
slots.push({
|
|
621
|
-
key,
|
|
622
|
-
kind: "finite",
|
|
623
|
-
lowered: {
|
|
624
|
-
expression: branches,
|
|
625
|
-
emitsLiterals: true
|
|
626
|
-
},
|
|
627
|
-
text: property.getText()
|
|
628
|
-
});
|
|
629
|
-
continue;
|
|
630
|
-
}
|
|
631
|
-
const leaf = dynamicLeaf(key, value, deps);
|
|
632
|
-
if (leaf) {
|
|
633
|
-
slots.push({
|
|
634
|
-
key,
|
|
635
|
-
kind: "finite",
|
|
636
|
-
lowered: {
|
|
637
|
-
expression: leaf,
|
|
638
|
-
emitsLiterals: false
|
|
639
|
-
},
|
|
640
|
-
text: property.getText()
|
|
641
|
-
});
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
const nested = value && ts_morph.Node.isObjectLiteralExpression(value) && isAccounted(value, valueBox) ? partitionObject(value, valueBox, styles[key] ?? {}, deps, false) : void 0;
|
|
646
|
-
if (nested && Object.keys(nested.staticStyles).length && nested.dynamicText.length) {
|
|
647
|
-
staticKeys.push(key);
|
|
648
|
-
staticStyles[key] = nested.staticStyles;
|
|
649
|
-
slots.push({
|
|
650
|
-
key,
|
|
651
|
-
kind: "dynamic",
|
|
652
|
-
split: true,
|
|
653
|
-
text: `${property.getNameNode().getText()}: { ${nested.dynamicText.join(", ")} }`
|
|
654
|
-
});
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
slots.push({
|
|
658
|
-
key,
|
|
659
|
-
kind: "dynamic",
|
|
660
|
-
text: property.getText()
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
const demote = (slot) => {
|
|
664
|
-
slot.kind = "dynamic";
|
|
665
|
-
slot.lowered = void 0;
|
|
666
|
-
};
|
|
667
|
-
const contested = () => slots.filter((slot) => slot.kind === "dynamic" && !slot.split).map((slot) => slot.key);
|
|
668
|
-
for (;;) {
|
|
669
|
-
const lowered = slots.filter((slot) => slot.kind === "finite");
|
|
670
|
-
const dynamic = contested();
|
|
671
|
-
const offender = lowered.find((slot, index) => collides([slot.key], [
|
|
672
|
-
...staticKeys,
|
|
673
|
-
...dynamic,
|
|
674
|
-
...lowered.slice(0, index).map((kept) => kept.key)
|
|
675
|
-
], ctx));
|
|
676
|
-
if (!offender) break;
|
|
677
|
-
demote(offender);
|
|
678
|
-
}
|
|
679
|
-
const written = () => slots.map((slot) => slot.kind === "finite" ? "f" : "d").join("");
|
|
680
|
-
while (!/^f*d*$/.test(written()) && !/^d*f*$/.test(written())) demote(slots.findLast((slot) => slot.kind === "finite"));
|
|
681
|
-
const dynamicText = slots.filter((slot) => slot.kind === "dynamic").map((slot) => slot.text);
|
|
682
|
-
const finite = slots.filter((slot) => slot.kind === "finite").map((slot) => slot.lowered);
|
|
683
|
-
if (!staticKeys.length && !finite.length) return void 0;
|
|
684
|
-
if (!dynamicText.length && !finite.length) return void 0;
|
|
685
|
-
if (collides(staticKeys, contested(), ctx)) return void 0;
|
|
686
|
-
return {
|
|
687
|
-
staticStyles,
|
|
688
|
-
dynamicText,
|
|
689
|
-
finite,
|
|
690
|
-
finiteFirst: !written().startsWith("d")
|
|
691
|
-
};
|
|
692
|
-
};
|
|
693
|
-
/**
|
|
694
|
-
* Do the two halves resolve to a shared property?
|
|
695
|
-
*
|
|
696
|
-
* Compared after shorthand resolution, since that is where distinct keys become the same
|
|
697
|
-
* property. An unrecognised key resolves to itself, so two distinct unknown keys are read
|
|
698
|
-
* as distinct — which is right for atomic output, where one class is emitted per key.
|
|
699
|
-
*/
|
|
700
|
-
const collides = (staticKeys, dynamicKeys, ctx) => {
|
|
701
|
-
if (staticKeys.includes("base") || dynamicKeys.includes("base")) return true;
|
|
702
|
-
const resolve = (key) => ctx.utility.hasShorthand ? ctx.utility.resolveShorthand(key) : key;
|
|
703
|
-
const resolvedStatic = new Set(staticKeys.map(resolve));
|
|
704
|
-
return dynamicKeys.some((key) => resolvedStatic.has(resolve(key)));
|
|
705
|
-
};
|
|
706
|
-
/**
|
|
707
|
-
* The `cx` binding to call, adding it to the import that already brings in the style
|
|
708
|
-
* helper when it is not there yet.
|
|
709
|
-
*
|
|
710
|
-
* Reusing that declaration rather than writing a new one avoids having to guess the
|
|
711
|
-
* module specifier, which varies with `importMap`, path aliases and how the project
|
|
712
|
-
* spells its outdir.
|
|
713
|
-
*/
|
|
714
|
-
const ensureCxImport = (call, calleeRoot, isBambooCssModule, isGeneratedCssModule, isShadowed, extra = []) => {
|
|
715
|
-
const sourceFile = call.getSourceFile();
|
|
716
|
-
const wanted = ["cx", ...extra];
|
|
717
|
-
const resolved = {};
|
|
718
|
-
let host;
|
|
719
|
-
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
720
|
-
const mod = declaration.getModuleSpecifierValue();
|
|
721
|
-
for (const named of declaration.getNamedImports()) {
|
|
722
|
-
const local = (named.getAliasNode() ?? named.getNameNode()).getText();
|
|
723
|
-
const imported = named.getNameNode().getText();
|
|
724
|
-
if (wanted.includes(imported) && !(imported in resolved)) {
|
|
725
|
-
if (declaration.isTypeOnly() || named.isTypeOnly()) return void 0;
|
|
726
|
-
if (!isBambooCssModule(mod)) return void 0;
|
|
727
|
-
if (isShadowed(call, local)) return void 0;
|
|
728
|
-
resolved[imported] = local;
|
|
729
|
-
}
|
|
730
|
-
if (local === calleeRoot) host = declaration;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
const missing = wanted.filter((name) => !(name in resolved));
|
|
734
|
-
if (!missing.length) return {
|
|
735
|
-
name: resolved.cx,
|
|
736
|
-
names: resolved
|
|
737
|
-
};
|
|
738
|
-
if (!host) return void 0;
|
|
739
|
-
if (!isGeneratedCssModule(host.getModuleSpecifierValue())) return void 0;
|
|
740
|
-
const declared = declaredAtModuleScope(sourceFile);
|
|
741
|
-
for (const name of missing) {
|
|
742
|
-
if (declared.has(name) || isShadowed(call, name)) return void 0;
|
|
743
|
-
resolved[name] = name;
|
|
744
|
-
}
|
|
745
|
-
const last = host.getNamedImports().at(-1);
|
|
746
|
-
if (!last) return void 0;
|
|
747
|
-
return {
|
|
748
|
-
name: resolved.cx,
|
|
749
|
-
names: resolved,
|
|
750
|
-
insert: {
|
|
751
|
-
pos: last.getEnd(),
|
|
752
|
-
names: missing
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
};
|
|
756
619
|
//#endregion
|
|
757
620
|
//#region src/fold-recipe.ts
|
|
758
621
|
/**
|
|
@@ -762,15 +625,12 @@ const ensureCxImport = (call, calleeRoot, isBambooCssModule, isGeneratedCssModul
|
|
|
762
625
|
* that names it. The parser records a definition under the name it was *imported* as (`cva`),
|
|
763
626
|
* and a call under the name the file *bound* (`badge`); this is what joins the two.
|
|
764
627
|
*
|
|
765
|
-
*
|
|
766
|
-
* a call of *either* as a recipe call, but an `sva` invocation returns one class per slot — an
|
|
767
|
-
* object, not a string — so there is no literal to substitute. Leaving slot recipes out of this
|
|
768
|
-
* map is what makes them decline as `unknown-recipe` instead of folding to a string that would
|
|
769
|
-
* break every consumer reading `.root` off it.
|
|
628
|
+
* Slot and ordinary recipes share one representation.
|
|
770
629
|
*/
|
|
771
630
|
const collectRecipeConfigs = (parserResult) => {
|
|
772
631
|
const configs = /* @__PURE__ */ new Map();
|
|
773
|
-
|
|
632
|
+
const definitions = [...parserResult.cva, ...parserResult.sva];
|
|
633
|
+
for (const definition of definitions) {
|
|
774
634
|
const node = definition.box?.getNode?.();
|
|
775
635
|
if (!node) continue;
|
|
776
636
|
const nameNode = ((ts_morph.Node.isCallExpression(node) ? node : node.getFirstAncestorByKind(ts_morph.SyntaxKind.CallExpression))?.getFirstAncestorByKind(ts_morph.SyntaxKind.VariableDeclaration))?.getNameNode();
|
|
@@ -787,21 +647,20 @@ const collectRecipeConfigs = (parserResult) => {
|
|
|
787
647
|
}
|
|
788
648
|
configs.set(nameNode.getText(), {
|
|
789
649
|
config,
|
|
790
|
-
name: (0, _bamboocss_shared.getRecipeIdentity)(config),
|
|
791
650
|
box: definition.box
|
|
792
651
|
});
|
|
793
652
|
}
|
|
794
653
|
return configs;
|
|
795
654
|
};
|
|
796
|
-
/**
|
|
797
|
-
const
|
|
655
|
+
/** Pick a complete precompiled StyleSet for one or more runtime recipe axes. */
|
|
656
|
+
const RECIPE_MAP_HELPER = "cvaMap";
|
|
657
|
+
/** Guard the exact compiler against accidentally materialising an enormous Cartesian product. */
|
|
658
|
+
const DEFAULT_MAX_RECIPE_STATES = 65536;
|
|
798
659
|
/** What `recipe.splitVariantProps` calls, reached directly once the call is lowered. */
|
|
799
660
|
const SPLIT_PROPS_HELPER = "splitProps";
|
|
800
|
-
const HELPER = RECIPE_PICK_HELPER;
|
|
801
661
|
/** Marker for a binding the fold must never resolve — declared twice, or unresolvable. */
|
|
802
662
|
const AMBIGUOUS = Object.freeze({
|
|
803
663
|
config: {},
|
|
804
|
-
name: "",
|
|
805
664
|
box: void 0
|
|
806
665
|
});
|
|
807
666
|
/** `.size`, or `["x-large"]` when the variant is not a valid identifier. */
|
|
@@ -843,10 +702,9 @@ const propertyKey = (nameNode) => {
|
|
|
843
702
|
if (ts_morph.Node.isNumericLiteral(nameNode)) return String(nameNode.getLiteralValue());
|
|
844
703
|
};
|
|
845
704
|
/**
|
|
846
|
-
* Make
|
|
705
|
+
* Make a generated compile helper callable at this call site, by whatever name the file gives it.
|
|
847
706
|
*
|
|
848
|
-
*
|
|
849
|
-
* matching the *callee* against an import. An inline recipe's callee is a local binding, so
|
|
707
|
+
* Unlike `cx`, an inline recipe's callee is a local binding, so
|
|
850
708
|
* there is nothing to match — the host here is any import of the generated css module, which
|
|
851
709
|
* a file defining a recipe necessarily has, since `cva` came from it.
|
|
852
710
|
*/
|
|
@@ -898,13 +756,18 @@ const ensureRecipeHelperImport = (imported, call, isBambooCssModule, isGenerated
|
|
|
898
756
|
* an unresolved variant does not merely omit a class, it can change which of several the
|
|
899
757
|
* recipe applies — so a partially-known selection is not foldable at all.
|
|
900
758
|
*/
|
|
901
|
-
const lowerRecipeCall = (call, entry,
|
|
759
|
+
const lowerRecipeCall = (call, entry, styleCompiler, isInert, resolvedSelection, slot, maxRecipeStates = DEFAULT_MAX_RECIPE_STATES) => {
|
|
902
760
|
if (!entry || entry === AMBIGUOUS) return {
|
|
903
761
|
kind: "decline",
|
|
904
762
|
reason: "unknown-recipe"
|
|
905
763
|
};
|
|
906
|
-
const { config
|
|
907
|
-
if (config.slots !== void 0)
|
|
764
|
+
const { config } = entry;
|
|
765
|
+
if (config.slots !== void 0) {
|
|
766
|
+
if (!Array.isArray(config.slots) || slot !== void 0 && !config.slots.includes(slot)) return {
|
|
767
|
+
kind: "decline",
|
|
768
|
+
reason: "unsupported-shape"
|
|
769
|
+
};
|
|
770
|
+
} else if (slot) return {
|
|
908
771
|
kind: "decline",
|
|
909
772
|
reason: "unsupported-shape"
|
|
910
773
|
};
|
|
@@ -940,16 +803,13 @@ const lowerRecipeCall = (call, entry, ctx, isInert, resolvedSelection) => {
|
|
|
940
803
|
/**
|
|
941
804
|
* `input(variantProps)` — a selection the build cannot see inside.
|
|
942
805
|
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
* selection through `createCss`, which *expands* conditions into one class per breakpoint,
|
|
947
|
-
* so a scalar lookup silently drops them. That is why this lowering is not applied to
|
|
948
|
-
* config recipes: for a dynamic axis the build cannot know which kind of value arrives.
|
|
806
|
+
* The compiled recipe contract accepts scalar declared variant values. A conditional
|
|
807
|
+
* object is not a finite selection value; responsiveness belongs inside a variant's style
|
|
808
|
+
* declaration, where the compiler can materialize its conditions ahead of time.
|
|
949
809
|
*
|
|
950
|
-
* The
|
|
951
|
-
*
|
|
952
|
-
*
|
|
810
|
+
* The complete StyleSets are knowable: the config declares every scalar value each axis
|
|
811
|
+
* accepts. This is the shape a wrapper component takes, where variants are its public API
|
|
812
|
+
* and therefore cannot be literals by definition.
|
|
953
813
|
*
|
|
954
814
|
* An identifier only. Each variant reads the binding again, and re-reading anything else —
|
|
955
815
|
* a call, a property access — would evaluate it once per axis instead of once.
|
|
@@ -1032,19 +892,51 @@ const lowerRecipeCall = (call, entry, ctx, isInert, resolvedSelection) => {
|
|
|
1032
892
|
* typecheck and does transform `.js`, so this is reachable.
|
|
1033
893
|
*/
|
|
1034
894
|
const everyEffectSurvives = () => effectful.every(({ key, text }) => dynamicAxes.get(key) === text);
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
895
|
+
const compiledSelection = (selected) => {
|
|
896
|
+
if (Array.isArray(config.slots) && slot === void 0) {
|
|
897
|
+
const slots = {};
|
|
898
|
+
const classNames = /* @__PURE__ */ new Set();
|
|
899
|
+
for (const slotName of config.slots) {
|
|
900
|
+
const styles = styleCompiler.resolveRecipe(config, selected, slotName);
|
|
901
|
+
if (!styles) return void 0;
|
|
902
|
+
const className = styleCompiler.className(styles);
|
|
903
|
+
slots[slotName] = className;
|
|
904
|
+
for (const token of className.split(" ")) if (token) classNames.add(token);
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
value: slots,
|
|
908
|
+
classNames: [...classNames]
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
const styles = styleCompiler.resolveRecipe(config, selected, slot);
|
|
912
|
+
if (!styles) return void 0;
|
|
913
|
+
const className = styleCompiler.className(styles);
|
|
914
|
+
return {
|
|
915
|
+
value: className,
|
|
916
|
+
classNames: className.split(" ").filter(Boolean),
|
|
917
|
+
styles
|
|
918
|
+
};
|
|
1038
919
|
};
|
|
1039
|
-
const format = (0, _bamboocss_core.classFormatter)(ctx);
|
|
1040
920
|
if (dynamicAxes.size === 0) {
|
|
1041
921
|
if (!everyEffectSurvives()) return {
|
|
1042
922
|
kind: "decline",
|
|
1043
923
|
reason: "dynamic"
|
|
1044
924
|
};
|
|
1045
|
-
|
|
925
|
+
const compiled = compiledSelection(selection);
|
|
926
|
+
if (!compiled) return {
|
|
927
|
+
kind: "decline",
|
|
928
|
+
reason: "dynamic"
|
|
929
|
+
};
|
|
930
|
+
if (typeof compiled.value === "string") return {
|
|
1046
931
|
kind: "class",
|
|
1047
|
-
className:
|
|
932
|
+
className: compiled.value,
|
|
933
|
+
styles: compiled.styles
|
|
934
|
+
};
|
|
935
|
+
return {
|
|
936
|
+
kind: "slots",
|
|
937
|
+
expression: JSON.stringify(compiled.value),
|
|
938
|
+
classNames: compiled.classNames,
|
|
939
|
+
dynamic: false
|
|
1048
940
|
};
|
|
1049
941
|
}
|
|
1050
942
|
if (!everyEffectSurvives()) return {
|
|
@@ -1060,45 +952,126 @@ const lowerRecipeCall = (call, entry, ctx, isInert, resolvedSelection) => {
|
|
|
1060
952
|
};
|
|
1061
953
|
}
|
|
1062
954
|
for (const key of [...dynamicAxes.keys()]) if (!Object.hasOwn(config.variants ?? {}, key)) dynamicAxes.delete(key);
|
|
1063
|
-
if (dynamicAxes.size === 0)
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
continue;
|
|
1081
|
-
}
|
|
1082
|
-
const values = config.variants[key];
|
|
1083
|
-
const table = {};
|
|
1084
|
-
for (const value of Object.keys(values)) {
|
|
1085
|
-
const className = format(`${name}--${key}${ctx.utility.separator}${(0, _bamboocss_shared.withoutSpace)(value)}`);
|
|
1086
|
-
table[value] = ` ${className}`;
|
|
1087
|
-
classNames.push(className);
|
|
1088
|
-
}
|
|
1089
|
-
const fallbackValue = config.defaultVariants?.[key];
|
|
1090
|
-
const fallback = fallbackValue != null && values[fallbackValue] != null ? ` ${format(`${name}--${key}${ctx.utility.separator}${(0, _bamboocss_shared.withoutSpace)(fallbackValue)}`)}` : void 0;
|
|
1091
|
-
parts.push(`${HELPER}(${expression}, ${JSON.stringify(table)}${fallback === void 0 ? "" : `, ${JSON.stringify(fallback)}`})`);
|
|
955
|
+
if (dynamicAxes.size === 0) {
|
|
956
|
+
const compiled = compiledSelection(selection);
|
|
957
|
+
if (!compiled) return {
|
|
958
|
+
kind: "decline",
|
|
959
|
+
reason: "dynamic"
|
|
960
|
+
};
|
|
961
|
+
if (typeof compiled.value === "string") return {
|
|
962
|
+
kind: "class",
|
|
963
|
+
className: compiled.value,
|
|
964
|
+
styles: compiled.styles
|
|
965
|
+
};
|
|
966
|
+
return {
|
|
967
|
+
kind: "slots",
|
|
968
|
+
expression: JSON.stringify(compiled.value),
|
|
969
|
+
classNames: compiled.classNames,
|
|
970
|
+
dynamic: false
|
|
971
|
+
};
|
|
1092
972
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
973
|
+
/**
|
|
974
|
+
* Compile the finite recipe state space into a reduced decision table.
|
|
975
|
+
*
|
|
976
|
+
* Each leaf is a *complete* final StyleSet. This matters for declarations overridden by
|
|
977
|
+
* variants and compounds: selecting independent per-axis atoms would put both values in
|
|
978
|
+
* the utility layer and let stylesheet order, rather than the recipe's merge order, pick
|
|
979
|
+
* the winner. Complete leaves retain the same precedence while sharing their atoms with
|
|
980
|
+
* every `css()` and recipe in the build.
|
|
981
|
+
*
|
|
982
|
+
* `undefined` is its own edge because it restores a default variant. `null` and any
|
|
983
|
+
* undeclared value take the miss edge and explicitly suppress that default. Declared
|
|
984
|
+
* values use string keys, matching JavaScript's property-key coercion in the recipe
|
|
985
|
+
* runtime. A flat alternating key/value array avoids the special `__proto__` semantics
|
|
986
|
+
* of an object literal.
|
|
987
|
+
*/
|
|
988
|
+
const axes = Object.keys(config.variants ?? {}).filter((key) => dynamicAxes.has(key));
|
|
989
|
+
const stateCount = axes.reduce((product, axis) => product * (Object.keys(config.variants?.[axis] ?? {}).length + 2), 1);
|
|
990
|
+
if (stateCount > maxRecipeStates) throw new Error(`Static recipe compilation would inspect ${stateCount.toLocaleString("en-US")} selections across ${axes.length} runtime variant axes, above maxRecipeStates=${maxRecipeStates.toLocaleString("en-US")}. Make one or more axes statically known, split the recipe, or raise the limit explicitly.`);
|
|
991
|
+
const expressions = axes.map((axis) => dynamicAxes.get(axis));
|
|
992
|
+
const wholeSlots = Array.isArray(config.slots) && slot === void 0;
|
|
1097
993
|
return {
|
|
1098
|
-
kind: "
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
994
|
+
kind: "dynamic-style",
|
|
995
|
+
map: {
|
|
996
|
+
outputKind: wholeSlots ? "slots" : "class",
|
|
997
|
+
compile(before = [], after = []) {
|
|
998
|
+
const nodes = [];
|
|
999
|
+
const nodeByShape = /* @__PURE__ */ new Map();
|
|
1000
|
+
const leaves = [];
|
|
1001
|
+
const leafByShape = /* @__PURE__ */ new Map();
|
|
1002
|
+
const emittedClasses = /* @__PURE__ */ new Set();
|
|
1003
|
+
const leaf = (dynamicSelection) => {
|
|
1004
|
+
const selected = {
|
|
1005
|
+
...selection,
|
|
1006
|
+
...dynamicSelection
|
|
1007
|
+
};
|
|
1008
|
+
if (wholeSlots) {
|
|
1009
|
+
const compiled = compiledSelection(selected);
|
|
1010
|
+
if (!compiled || typeof compiled.value === "string") return internLeaf("");
|
|
1011
|
+
for (const token of compiled.classNames) emittedClasses.add(token);
|
|
1012
|
+
return internLeaf(compiled.value);
|
|
1013
|
+
}
|
|
1014
|
+
const styles = styleCompiler.resolveRecipe(config, selected, slot);
|
|
1015
|
+
if (!styles) return internLeaf("");
|
|
1016
|
+
const className = styleCompiler.className(styleCompiler.compose(...before, styles, ...after));
|
|
1017
|
+
for (const token of className.split(" ")) if (token) emittedClasses.add(token);
|
|
1018
|
+
return internLeaf(className);
|
|
1019
|
+
};
|
|
1020
|
+
function internLeaf(value) {
|
|
1021
|
+
const shape = JSON.stringify(value);
|
|
1022
|
+
const known = leafByShape.get(shape);
|
|
1023
|
+
if (known !== void 0) return ~known;
|
|
1024
|
+
const id = leaves.length;
|
|
1025
|
+
leaves.push(value);
|
|
1026
|
+
leafByShape.set(shape, id);
|
|
1027
|
+
return ~id;
|
|
1028
|
+
}
|
|
1029
|
+
const buildNode = (index, dynamicSelection) => {
|
|
1030
|
+
if (index === axes.length) return leaf(dynamicSelection);
|
|
1031
|
+
const axis = axes[index];
|
|
1032
|
+
const values = Object.keys(config.variants?.[axis] ?? {});
|
|
1033
|
+
const miss = buildNode(index + 1, {
|
|
1034
|
+
...dynamicSelection,
|
|
1035
|
+
[axis]: null
|
|
1036
|
+
});
|
|
1037
|
+
const absentSelection = { ...dynamicSelection };
|
|
1038
|
+
delete absentSelection[axis];
|
|
1039
|
+
const absent = buildNode(index + 1, absentSelection);
|
|
1040
|
+
const byValue = [];
|
|
1041
|
+
for (const value of values) byValue.push(value, buildNode(index + 1, {
|
|
1042
|
+
...dynamicSelection,
|
|
1043
|
+
[axis]: value
|
|
1044
|
+
}));
|
|
1045
|
+
const refs = [
|
|
1046
|
+
miss,
|
|
1047
|
+
absent,
|
|
1048
|
+
...byValue.filter((_, valueIndex) => valueIndex % 2 === 1)
|
|
1049
|
+
];
|
|
1050
|
+
if (refs.every((ref) => ref === refs[0])) return refs[0];
|
|
1051
|
+
const node = [
|
|
1052
|
+
miss,
|
|
1053
|
+
absent,
|
|
1054
|
+
byValue
|
|
1055
|
+
];
|
|
1056
|
+
const shape = JSON.stringify(node);
|
|
1057
|
+
const known = nodeByShape.get(shape);
|
|
1058
|
+
if (known !== void 0) return known;
|
|
1059
|
+
const id = nodes.length;
|
|
1060
|
+
nodes.push(node);
|
|
1061
|
+
nodeByShape.set(shape, id);
|
|
1062
|
+
return id;
|
|
1063
|
+
};
|
|
1064
|
+
const root = buildNode(0, {});
|
|
1065
|
+
const staticLeaf = root < 0 ? leaves[~root] : void 0;
|
|
1066
|
+
return {
|
|
1067
|
+
expression: root < 0 && effectful.length === 0 ? JSON.stringify(staticLeaf) : `${RECIPE_MAP_HELPER}([${expressions.join(", ")}], ${JSON.stringify(nodes)}, ${JSON.stringify(leaves)}, ${root})`,
|
|
1068
|
+
classNames: [...emittedClasses],
|
|
1069
|
+
staticClasses: typeof staticLeaf === "string" ? staticLeaf : "",
|
|
1070
|
+
outputKind: wholeSlots ? "slots" : "class",
|
|
1071
|
+
usesHelper: !(root < 0 && effectful.length === 0)
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1102
1075
|
};
|
|
1103
1076
|
};
|
|
1104
1077
|
//#endregion
|
|
@@ -1162,114 +1135,29 @@ const createRuntimeTokenValue = (ctx) => (path) => {
|
|
|
1162
1135
|
* the default form the trivially foldable one.
|
|
1163
1136
|
*/
|
|
1164
1137
|
const createRuntimeToken = (ctx) => (path) => tokenValuesFor(ctx).get(path)?.variable || void 0;
|
|
1165
|
-
/**
|
|
1166
|
-
* Whether a slot's class is independent of the variant props.
|
|
1167
|
-
*
|
|
1168
|
-
* A scoped slot recipe delivers variants through `@scope` rules anchored on an enclosing
|
|
1169
|
-
* slot, so every *other* slot carries a constant class — the same string whatever the props
|
|
1170
|
-
* are. That is what makes `recipe(anything).slot` foldable even when the variant is fully
|
|
1171
|
-
* dynamic, which was not true before scoping existed.
|
|
1172
|
-
*/
|
|
1173
|
-
const createConstantSlotCheck = (ctx) => (name, slot) => {
|
|
1174
|
-
const config = ctx.recipes.getConfig(name);
|
|
1175
|
-
if (!config || !("slots" in config)) return false;
|
|
1176
|
-
if (!config.slots.includes(slot)) return false;
|
|
1177
|
-
const anchors = _bamboocss_core.Recipes.getScopeRoots(config);
|
|
1178
|
-
return anchors.length > 0 && !anchors.includes(slot);
|
|
1179
|
-
};
|
|
1180
|
-
const createRuntimeRecipe = (ctx) => {
|
|
1181
|
-
const separator = ctx.utility.separator;
|
|
1182
|
-
return (name, variants, slot) => {
|
|
1183
|
-
const config = ctx.recipes.getConfig(name);
|
|
1184
|
-
const node = ctx.recipes.getRecipe(name);
|
|
1185
|
-
if (!config || !node) return void 0;
|
|
1186
|
-
const isSlotRecipe = "slots" in config;
|
|
1187
|
-
if (isSlotRecipe !== Boolean(slot)) return void 0;
|
|
1188
|
-
if (slot && !config.slots.includes(slot)) return void 0;
|
|
1189
|
-
const anchors = isSlotRecipe ? _bamboocss_core.Recipes.getScopeRoots(config) : [];
|
|
1190
|
-
const isConstantSlot = Boolean(slot) && anchors.length > 0 && !anchors.includes(slot);
|
|
1191
|
-
const className = slot ? ctx.recipes.getSlotKey(node.className, slot) : node.className;
|
|
1192
|
-
const { defaultVariants = {} } = config;
|
|
1193
|
-
const compoundVariants = slot ? (0, _bamboocss_shared.getSlotCompoundVariant)(config.compoundVariants ?? [], slot) : config.compoundVariants ?? [];
|
|
1194
|
-
const recipeCss = (0, _bamboocss_shared.createCssUncached)({
|
|
1195
|
-
hash: Boolean(ctx.hash.className),
|
|
1196
|
-
conditions: {
|
|
1197
|
-
shift: ctx.conditions.shift,
|
|
1198
|
-
finalize: ctx.conditions.finalize
|
|
1199
|
-
},
|
|
1200
|
-
utility: {
|
|
1201
|
-
prefix: ctx.utility.prefix,
|
|
1202
|
-
hasShorthand: false,
|
|
1203
|
-
resolveShorthand: (prop) => prop,
|
|
1204
|
-
toHash: ctx.utility.toHash.bind(ctx.utility),
|
|
1205
|
-
transform: (prop, value) => {
|
|
1206
|
-
if (value === "__ignore__") return { className };
|
|
1207
|
-
return { className: `${className}--${prop}${separator}${(0, _bamboocss_shared.withoutSpace)(value)}` };
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
});
|
|
1211
|
-
const declaredValues = config.variants ?? {};
|
|
1212
|
-
/**
|
|
1213
|
-
* The same filter the generated `createRecipe` applies: only a value the config declares
|
|
1214
|
-
* names a class.
|
|
1215
|
-
*
|
|
1216
|
-
* Scalars only — a conditional or responsive value is an object of leaves, and the leaves
|
|
1217
|
-
* are what name classes when `createCss` walks them.
|
|
1218
|
-
*/
|
|
1219
|
-
const onlyDeclared = (styles) => Object.fromEntries(Object.entries(styles).filter(([prop, value]) => {
|
|
1220
|
-
if (prop === className) return true;
|
|
1221
|
-
if (value === null || typeof value === "object") return true;
|
|
1222
|
-
return Object.hasOwn(declaredValues, prop) && Object.hasOwn(declaredValues[prop] ?? {}, String(value));
|
|
1223
|
-
}));
|
|
1224
|
-
const recipeStyles = isConstantSlot ? { [className]: "__ignore__" } : onlyDeclared({
|
|
1225
|
-
[className]: "__ignore__",
|
|
1226
|
-
...defaultVariants,
|
|
1227
|
-
...(0, _bamboocss_shared.compact)(variants)
|
|
1228
|
-
});
|
|
1229
|
-
if (compoundVariants.length > 0 && Object.keys(recipeStyles).some((prop) => typeof variants[prop] === "object")) return;
|
|
1230
|
-
if (isSlotRecipe) {
|
|
1231
|
-
const evaluated = anchors.length > 0 ? anchors : config.slots;
|
|
1232
|
-
if (Object.values(variants).some((value) => typeof value === "object" && value !== null) && evaluated.some((slotName) => (0, _bamboocss_shared.getSlotCompoundVariant)(config.compoundVariants ?? [], slotName).length > 0)) return void 0;
|
|
1233
|
-
}
|
|
1234
|
-
return recipeCss(recipeStyles);
|
|
1235
|
-
};
|
|
1236
|
-
};
|
|
1237
1138
|
//#endregion
|
|
1238
1139
|
//#region src/fold.ts
|
|
1239
1140
|
/**
|
|
1240
|
-
* `cva`/`sva` return a function, so
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1141
|
+
* `cva`/`sva` return a function, so their definitions are compile-time declarations rather
|
|
1142
|
+
* than class-producing calls; once their uses are lowered, the factory calls are erased.
|
|
1143
|
+
* `token` also resolves to no class, but it does resolve to a literal, so it compiles through
|
|
1144
|
+
* its own path rather than being declined outright. A static `viewTransition` bag resolves to
|
|
1145
|
+
* its extracted class and uses the ordinary class candidate path.
|
|
1243
1146
|
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1147
|
+
* Recipe invocations compile through `fold-recipe`. Inline calls
|
|
1148
|
+
* are recorded under the name the file bound; config calls arrive as `recipe`. Routing both
|
|
1149
|
+
* through one exact finite-state lowering keeps their selection contract identical.
|
|
1247
1150
|
*/
|
|
1248
1151
|
const FOLDABLE_TYPES = new Set([
|
|
1249
1152
|
"css",
|
|
1250
1153
|
"pattern",
|
|
1251
|
-
"
|
|
1154
|
+
"viewTransition"
|
|
1252
1155
|
]);
|
|
1253
1156
|
/**
|
|
1254
|
-
* The class strings inside a lowered ternary — `e ? "c_red" : "c_blue"` gives both arms.
|
|
1255
|
-
*
|
|
1256
|
-
* They are read back out of the emitted text rather than threaded through the planner,
|
|
1257
|
-
* because the planner's product *is* that text: anything it did not write cannot appear
|
|
1258
|
-
* here, and anything it did cannot be missed.
|
|
1259
|
-
*/
|
|
1260
|
-
const literalsIn = (expression) => [...expression.matchAll(/"((?:[^"\\]|\\.)*)"/g)].flatMap((match) => JSON.parse(match[0]).split(" ")).filter(Boolean);
|
|
1261
|
-
/**
|
|
1262
|
-
* The kinds reported as `not-foldable`, which is permanent rather than a limit of this
|
|
1263
|
-
* phase — hence separate from `unsupported-kind`, where a slot recipe lands because it
|
|
1264
|
-
* resolves to one class per slot rather than to a single string.
|
|
1265
|
-
*/
|
|
1266
|
-
const UNFOLDABLE_TYPES = new Set(["cva", "sva"]);
|
|
1267
|
-
/**
|
|
1268
1157
|
* The skip reasons that leave a `css()`-family call in the output.
|
|
1269
1158
|
*
|
|
1270
1159
|
* `overlapping` is handled by the enclosing fold, and `not-imported` is somebody else's
|
|
1271
|
-
* function of the same name — neither leaves a call of ours.
|
|
1272
|
-
* definition, which keeps the recipe runtime rather than the css engine; see `failOnUnfolded`.
|
|
1160
|
+
* function of the same name — neither leaves a call of ours.
|
|
1273
1161
|
*/
|
|
1274
1162
|
const SURVIVES_TO_RUNTIME = new Set([
|
|
1275
1163
|
"dynamic",
|
|
@@ -1277,9 +1165,8 @@ const SURVIVES_TO_RUNTIME = new Set([
|
|
|
1277
1165
|
"raw-call",
|
|
1278
1166
|
"unsupported-kind",
|
|
1279
1167
|
"no-call-expression",
|
|
1280
|
-
"empty",
|
|
1281
1168
|
"unresolved-token",
|
|
1282
|
-
"
|
|
1169
|
+
"compile-failed"
|
|
1283
1170
|
]);
|
|
1284
1171
|
/**
|
|
1285
1172
|
* A call of a recipe the file bound itself: `const badge = cva(...)`, then `badge({ tone })`.
|
|
@@ -1327,21 +1214,13 @@ const isValueReference = (identifier) => {
|
|
|
1327
1214
|
/**
|
|
1328
1215
|
* Imports a surviving reference to is not a failure.
|
|
1329
1216
|
*
|
|
1330
|
-
*
|
|
1217
|
+
* These are what the compiler itself writes; all live in `cx` and pull no engine, so a
|
|
1331
1218
|
* reference to one is the fold having worked.
|
|
1332
|
-
*
|
|
1333
|
-
* `cva` and `sva` are there for the reason `SURVIVES_TO_RUNTIME` omits `not-foldable`: a
|
|
1334
|
-
* recipe *definition* cannot fold to a class string and never could, and what it keeps is the
|
|
1335
|
-
* recipe runtime rather than the css engine — which `failOnUnfolded` accepts. Their unfoldable
|
|
1336
|
-
* invocations are reported separately, as `recipe-call`.
|
|
1337
1219
|
*/
|
|
1338
1220
|
const PERMITTED_BINDINGS = new Set([
|
|
1339
1221
|
"cx",
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
RECIPE_PICK_HELPER,
|
|
1343
|
-
SPLIT_PROPS_HELPER,
|
|
1344
|
-
LEAF_HELPER
|
|
1222
|
+
RECIPE_MAP_HELPER,
|
|
1223
|
+
SPLIT_PROPS_HELPER
|
|
1345
1224
|
]);
|
|
1346
1225
|
/**
|
|
1347
1226
|
* The pieces `trim` reduces a module specifier by, hoisted because a regex literal
|
|
@@ -1569,64 +1448,7 @@ const argumentsAccountedFor = (call, boxNode) => {
|
|
|
1569
1448
|
return accountsForSource(args[0], boxNode);
|
|
1570
1449
|
};
|
|
1571
1450
|
const foldSource = (options) => {
|
|
1572
|
-
const { ctx, code, parserResult,
|
|
1573
|
-
/**
|
|
1574
|
-
* Recover the static half of a call the whole-call path gave up on. Only a
|
|
1575
|
-
* single-argument `css()` qualifies: a pattern or recipe call takes props rather than a
|
|
1576
|
-
* style object, and a multi-argument `css` is later-wins across the whole object.
|
|
1577
|
-
*/
|
|
1578
|
-
const tryPartial = (item, call, rootName) => {
|
|
1579
|
-
if (item.type !== "css" || !rootName) return void 0;
|
|
1580
|
-
if (!ts_morph.Node.isCallExpression(call)) return void 0;
|
|
1581
|
-
const args = call.getArguments();
|
|
1582
|
-
if (args.length !== 1) return void 0;
|
|
1583
|
-
const unboxed = _bamboocss_extractor.box.isMap(item.box) ? (0, _bamboocss_extractor.unbox)(item.box) : void 0;
|
|
1584
|
-
if (!unboxed?.raw) return void 0;
|
|
1585
|
-
const raw = unboxed.raw;
|
|
1586
|
-
if (unboxed.spreadConditions?.length) return void 0;
|
|
1587
|
-
const argument = args[0];
|
|
1588
|
-
if (!argument || !ts_morph.Node.isObjectLiteralExpression(argument)) return void 0;
|
|
1589
|
-
const leafName = findBambooBinding(call, LEAF_HELPER, isBambooCssModule, isShadowed);
|
|
1590
|
-
const plan_ = (allowLeaf) => {
|
|
1591
|
-
try {
|
|
1592
|
-
return planPartialFold(argument, item.box, raw, {
|
|
1593
|
-
ctx,
|
|
1594
|
-
runtimeCss,
|
|
1595
|
-
isAccounted: accountsForSource,
|
|
1596
|
-
isStatic: (boxNode) => isStaticBox(boxNode),
|
|
1597
|
-
allowLeaf,
|
|
1598
|
-
leafName: leafName ?? "cssLeaf"
|
|
1599
|
-
});
|
|
1600
|
-
} catch {
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
};
|
|
1604
|
-
let plan = plan_(true);
|
|
1605
|
-
if (!plan) return void 0;
|
|
1606
|
-
const usesLeaf = () => plan.finite.some((entry) => !entry.emitsLiterals);
|
|
1607
|
-
let cx = ensureCxImport(call, rootName, isBambooCssModule, isGeneratedCssModule, isShadowed, usesLeaf() ? [LEAF_HELPER] : []);
|
|
1608
|
-
if (!cx && usesLeaf()) {
|
|
1609
|
-
plan = plan_(false);
|
|
1610
|
-
if (!plan) return void 0;
|
|
1611
|
-
cx = ensureCxImport(call, rootName, isBambooCssModule, isGeneratedCssModule, isShadowed);
|
|
1612
|
-
}
|
|
1613
|
-
if (!cx) return void 0;
|
|
1614
|
-
const callee = call.getExpression().getText();
|
|
1615
|
-
const runtimePart = plan.dynamicText ? `${callee}(${plan.dynamicText})` : void 0;
|
|
1616
|
-
const runtimeParts = runtimePart ? [runtimePart] : [];
|
|
1617
|
-
const lowered = plan.finite.map((entry) => entry.expression);
|
|
1618
|
-
const parts = [...plan.className ? [JSON.stringify(plan.className)] : [], ...plan.finiteFirst ? [...lowered, ...runtimeParts] : [...runtimeParts, ...lowered]];
|
|
1619
|
-
if (!parts.length || parts.length === 1 && runtimePart) return void 0;
|
|
1620
|
-
return {
|
|
1621
|
-
className: plan.className,
|
|
1622
|
-
classNames: [plan.className, ...plan.finite.filter((entry) => entry.emitsLiterals).flatMap((entry) => literalsIn(entry.expression))].filter(Boolean),
|
|
1623
|
-
replacement: `${cx.name}(${parts.join(", ")})`,
|
|
1624
|
-
insert: cx.insert,
|
|
1625
|
-
runtimeCallee: runtimePart ? callee : void 0
|
|
1626
|
-
};
|
|
1627
|
-
};
|
|
1628
|
-
const runtimeRecipe = createRuntimeRecipe(ctx);
|
|
1629
|
-
const isConstantSlot = createConstantSlotCheck(ctx);
|
|
1451
|
+
const { ctx, code, parserResult, runtimeCss = createRuntimeCss(ctx), styleCompiler, maxRecipeStates, parseModule, recipeConfigCache, reportSurvivors, sourceFile: ownSourceFile } = options;
|
|
1630
1452
|
const runtimeToken = createRuntimeToken(ctx);
|
|
1631
1453
|
const runtimeTokenValue = createRuntimeTokenValue(ctx);
|
|
1632
1454
|
/**
|
|
@@ -1639,7 +1461,7 @@ const foldSource = (options) => {
|
|
|
1639
1461
|
*
|
|
1640
1462
|
* A tsconfig path alias is resolved first, the same way `ImportMap.match` does. Without
|
|
1641
1463
|
* that, `@site/styled-system/css` — the spelling this repo's own website uses — fails
|
|
1642
|
-
* the check and silently loses
|
|
1464
|
+
* the check and silently loses helper lowering, which is indistinguishable in the
|
|
1643
1465
|
* diagnostics from a genuinely dynamic call.
|
|
1644
1466
|
*/
|
|
1645
1467
|
const cssModules = ctx.imports.matchers.css?.mods ?? [];
|
|
@@ -1689,10 +1511,30 @@ const foldSource = (options) => {
|
|
|
1689
1511
|
const isBambooCssModule = (mod) => matchesModule(mod, cssModules);
|
|
1690
1512
|
const isGeneratedCssModule = (mod) => matchesModule(mod, [generatedCssModule]);
|
|
1691
1513
|
/**
|
|
1514
|
+
* The generated css entry as spelled beside an imported config recipe.
|
|
1515
|
+
*
|
|
1516
|
+
* A decision table needs only `cvaMap`, but a module importing a config recipe often has
|
|
1517
|
+
* no css import to extend. Preserve a relative/aliased styled-system spelling by replacing
|
|
1518
|
+
* its `/recipes` suffix; falling back to the configured generated entry covers bare imports.
|
|
1519
|
+
*/
|
|
1520
|
+
const configRecipeCssSpecifier = (call, binding) => {
|
|
1521
|
+
for (const declaration of call.getSourceFile().getImportDeclarations()) {
|
|
1522
|
+
if (declaration.isTypeOnly()) continue;
|
|
1523
|
+
if (!declaration.getNamedImports().some((named) => {
|
|
1524
|
+
if (named.isTypeOnly()) return false;
|
|
1525
|
+
return (named.getAliasNode() ?? named.getNameNode()).getText() === binding;
|
|
1526
|
+
})) continue;
|
|
1527
|
+
const mod = declaration.getModuleSpecifierValue().replaceAll("\\", "/");
|
|
1528
|
+
const at = mod.lastIndexOf("/recipes");
|
|
1529
|
+
if (at >= 0) return `${mod.slice(0, at)}/css`;
|
|
1530
|
+
}
|
|
1531
|
+
return generatedCssModule;
|
|
1532
|
+
};
|
|
1533
|
+
/**
|
|
1692
1534
|
* How *this* module would have to spell the css module, learnt from one that already does.
|
|
1693
1535
|
*
|
|
1694
1536
|
* A file calling an imported recipe need not import the css module at all, so when the
|
|
1695
|
-
* lowering needs
|
|
1537
|
+
* lowering needs a decision-table helper there is no spelling in the file to copy. The declaring module
|
|
1696
1538
|
* necessarily has one — `cva` came from it — and that is the spelling reused here.
|
|
1697
1539
|
*
|
|
1698
1540
|
* A bare or aliased specifier resolves identically from any file, so it is taken as
|
|
@@ -1744,9 +1586,8 @@ const foldSource = (options) => {
|
|
|
1744
1586
|
* either end. Each hop is an alias symbol, so following them to a non-alias lands on the
|
|
1745
1587
|
* declaration wherever it lives.
|
|
1746
1588
|
*
|
|
1747
|
-
* The
|
|
1748
|
-
*
|
|
1749
|
-
* sites produce, and exactly the one the runtime would have.
|
|
1589
|
+
* The selected declarations do not depend on which module the call is in. A recipe lowered
|
|
1590
|
+
* here therefore reaches the same globally shared atoms as a call in its declaring module.
|
|
1750
1591
|
*/
|
|
1751
1592
|
const resolveImportedRecipe = (call, name, origin) => {
|
|
1752
1593
|
if (importedRecipes.has(name)) return importedRecipes.get(name);
|
|
@@ -1763,7 +1604,6 @@ const foldSource = (options) => {
|
|
|
1763
1604
|
const configs = /* @__PURE__ */ new Map();
|
|
1764
1605
|
for (const [key, entry] of collected) configs.set(key, entry === AMBIGUOUS ? entry : {
|
|
1765
1606
|
config: entry.config,
|
|
1766
|
-
name: entry.name,
|
|
1767
1607
|
box: void 0
|
|
1768
1608
|
});
|
|
1769
1609
|
foreign = {
|
|
@@ -1786,21 +1626,19 @@ const foldSource = (options) => {
|
|
|
1786
1626
|
const skipped = [];
|
|
1787
1627
|
const candidates = [];
|
|
1788
1628
|
const seenRanges = /* @__PURE__ */ new Set();
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
/** Bindings whose `splitVariantProps` was rewritten, so that access no longer reads them. */
|
|
1803
|
-
const loweredSplitProps = /* @__PURE__ */ new Set();
|
|
1629
|
+
const recipeConfigs = collectRecipeConfigs(parserResult);
|
|
1630
|
+
const recipeDefinitions = [];
|
|
1631
|
+
for (const [name, entry] of recipeConfigs) {
|
|
1632
|
+
if (entry === AMBIGUOUS) continue;
|
|
1633
|
+
const definition = entry.box?.getNode?.();
|
|
1634
|
+
if (!definition) continue;
|
|
1635
|
+
const call = ts_morph.Node.isCallExpression(definition) ? definition : definition.getFirstAncestorByKind(ts_morph.SyntaxKind.CallExpression);
|
|
1636
|
+
if (!call || code.slice(call.getStart(), call.getEnd()) !== call.getText()) continue;
|
|
1637
|
+
recipeDefinitions.push({
|
|
1638
|
+
name,
|
|
1639
|
+
call
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1804
1642
|
/** Ranges already reported as declined, so one call is never counted twice. */
|
|
1805
1643
|
const reportedRanges = /* @__PURE__ */ new Set();
|
|
1806
1644
|
const importCache = /* @__PURE__ */ new Map();
|
|
@@ -1921,13 +1759,7 @@ const foldSource = (options) => {
|
|
|
1921
1759
|
continue;
|
|
1922
1760
|
}
|
|
1923
1761
|
if (!FOLDABLE_TYPES.has(type)) {
|
|
1924
|
-
if (call &&
|
|
1925
|
-
name,
|
|
1926
|
-
reason: "not-foldable",
|
|
1927
|
-
start: call.getStart(),
|
|
1928
|
-
end: call.getEnd()
|
|
1929
|
-
});
|
|
1930
|
-
if (call && type === RECIPE_CALL_TYPE && !isShadowed(call, name)) {
|
|
1762
|
+
if (call && (type === RECIPE_CALL_TYPE || type === "recipe") && !isShadowed(call, name)) {
|
|
1931
1763
|
const start = call.getStart();
|
|
1932
1764
|
const end = call.getEnd();
|
|
1933
1765
|
const rangeKey = `${start}:${end}`;
|
|
@@ -1942,35 +1774,95 @@ const foldSource = (options) => {
|
|
|
1942
1774
|
});
|
|
1943
1775
|
continue;
|
|
1944
1776
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1777
|
+
if (isRawCall(call)) {
|
|
1778
|
+
skipped.push({
|
|
1779
|
+
name,
|
|
1780
|
+
reason: "raw-call",
|
|
1781
|
+
start,
|
|
1782
|
+
end
|
|
1783
|
+
});
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
if (!recipeConfigs.has(name)) {
|
|
1787
|
+
if (type === "recipe") {
|
|
1788
|
+
const config = ctx.recipes.getConfig(name);
|
|
1789
|
+
if (config) {
|
|
1790
|
+
recipeConfigs.set(name, {
|
|
1791
|
+
config,
|
|
1792
|
+
box: void 0
|
|
1793
|
+
});
|
|
1794
|
+
helperModules.set(name, configRecipeCssSpecifier(call, name));
|
|
1795
|
+
}
|
|
1796
|
+
} else if (item.origin) {
|
|
1797
|
+
const imported = resolveImportedRecipe(call, name, item.origin);
|
|
1798
|
+
if (imported) recipeConfigs.set(name, imported);
|
|
1799
|
+
}
|
|
1949
1800
|
}
|
|
1950
|
-
const tally = recipeCalls.get(name) ?? {
|
|
1951
|
-
seen: 0,
|
|
1952
|
-
lowered: 0
|
|
1953
|
-
};
|
|
1954
|
-
tally.seen++;
|
|
1955
|
-
recipeCalls.set(name, tally);
|
|
1956
1801
|
const resolvedSelection = item.data?.length === 1 ? item.data[0] : void 0;
|
|
1957
1802
|
const entry = recipeConfigs.get(name);
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1803
|
+
let inlineSlot;
|
|
1804
|
+
let inlineEnd = end;
|
|
1805
|
+
if (Array.isArray(entry?.config.slots)) {
|
|
1806
|
+
const parent = call.getParent();
|
|
1807
|
+
if (ts_morph.Node.isPropertyAccessExpression(parent) && parent.getExpression() === call) {
|
|
1808
|
+
const accessed = parent.getName();
|
|
1809
|
+
if (entry.config.slots.includes(accessed)) {
|
|
1810
|
+
inlineSlot = accessed;
|
|
1811
|
+
inlineEnd = parent.getEnd();
|
|
1812
|
+
}
|
|
1813
|
+
} else if (ts_morph.Node.isElementAccessExpression(parent) && parent.getExpression() === call) {
|
|
1814
|
+
const argument = parent.getArgumentExpression();
|
|
1815
|
+
const accessed = argument && (ts_morph.Node.isStringLiteral(argument) || ts_morph.Node.isNoSubstitutionTemplateLiteral(argument)) ? argument.getLiteralValue() : void 0;
|
|
1816
|
+
if (typeof accessed === "string" && entry.config.slots.includes(accessed)) {
|
|
1817
|
+
inlineSlot = accessed;
|
|
1818
|
+
inlineEnd = parent.getEnd();
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const lowered = lowerRecipeCall(call, entry, styleCompiler, isInertExpression, resolvedSelection, inlineSlot, maxRecipeStates);
|
|
1823
|
+
if (lowered.kind === "dynamic-style") {
|
|
1824
|
+
const helper = ensureRecipeHelperImport(RECIPE_MAP_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed, helperModules.get(name));
|
|
1961
1825
|
if (helper) {
|
|
1962
|
-
tally.lowered++;
|
|
1963
1826
|
candidates.push({
|
|
1964
1827
|
item,
|
|
1965
1828
|
call,
|
|
1966
1829
|
node: call,
|
|
1967
1830
|
start,
|
|
1968
|
-
end,
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1831
|
+
end: inlineEnd,
|
|
1832
|
+
className: "",
|
|
1833
|
+
classNames: [],
|
|
1834
|
+
styleMap: lowered.map,
|
|
1835
|
+
mapHelperName: helper.name,
|
|
1972
1836
|
insert: helper.insert,
|
|
1973
|
-
configBox: entry?.box
|
|
1837
|
+
configBox: entry?.box,
|
|
1838
|
+
outputKind: lowered.map.outputKind === "slots" ? "slots" : void 0
|
|
1839
|
+
});
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
skipped.push({
|
|
1843
|
+
name,
|
|
1844
|
+
reason: "recipe-call",
|
|
1845
|
+
start,
|
|
1846
|
+
end
|
|
1847
|
+
});
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
if (lowered.kind === "slots") {
|
|
1851
|
+
const helper = lowered.helper ? ensureRecipeHelperImport(lowered.helper, call, isBambooCssModule, isGeneratedCssModule, isShadowed, helperModules.get(name)) : void 0;
|
|
1852
|
+
if (!lowered.helper || helper) {
|
|
1853
|
+
const replacement = lowered.helper && helper && helper.name !== lowered.helper ? lowered.expression.replaceAll(`${lowered.helper}(`, `${helper.name}(`) : lowered.expression;
|
|
1854
|
+
candidates.push({
|
|
1855
|
+
item,
|
|
1856
|
+
call,
|
|
1857
|
+
node: call,
|
|
1858
|
+
start,
|
|
1859
|
+
end: inlineEnd,
|
|
1860
|
+
replacement,
|
|
1861
|
+
className: "",
|
|
1862
|
+
classNames: lowered.classNames,
|
|
1863
|
+
insert: helper?.insert,
|
|
1864
|
+
configBox: entry?.box,
|
|
1865
|
+
outputKind: "slots"
|
|
1974
1866
|
});
|
|
1975
1867
|
continue;
|
|
1976
1868
|
}
|
|
@@ -1983,16 +1875,16 @@ const foldSource = (options) => {
|
|
|
1983
1875
|
continue;
|
|
1984
1876
|
}
|
|
1985
1877
|
if (lowered.kind === "class") {
|
|
1986
|
-
tally.lowered++;
|
|
1987
1878
|
candidates.push({
|
|
1988
1879
|
item,
|
|
1989
1880
|
call,
|
|
1990
1881
|
node: call,
|
|
1991
1882
|
start,
|
|
1992
|
-
end,
|
|
1883
|
+
end: inlineEnd,
|
|
1993
1884
|
replacement: JSON.stringify(lowered.className),
|
|
1994
1885
|
className: lowered.className,
|
|
1995
1886
|
classNames: lowered.className.split(" ").filter(Boolean),
|
|
1887
|
+
styleSet: lowered.styles,
|
|
1996
1888
|
configBox: entry?.box
|
|
1997
1889
|
});
|
|
1998
1890
|
continue;
|
|
@@ -2056,37 +1948,7 @@ const foldSource = (options) => {
|
|
|
2056
1948
|
});
|
|
2057
1949
|
continue;
|
|
2058
1950
|
}
|
|
2059
|
-
if (slot && isConstantSlot(name, slot) && ts_morph.Node.isCallExpression(call) && call.getArguments().every(isInertExpression)) {
|
|
2060
|
-
candidates.push({
|
|
2061
|
-
item,
|
|
2062
|
-
call,
|
|
2063
|
-
node: call,
|
|
2064
|
-
start,
|
|
2065
|
-
end: foldEnd,
|
|
2066
|
-
slot,
|
|
2067
|
-
constantSlot: true
|
|
2068
|
-
});
|
|
2069
|
-
continue;
|
|
2070
|
-
}
|
|
2071
1951
|
if (!(ts_morph.Node.isCallExpression(call) && call.getArguments().length === 0 && item.data.length === 1) && !isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
|
|
2072
|
-
const partial = partial_ ? tryPartial(item, call, rootName) : void 0;
|
|
2073
|
-
if (partial) {
|
|
2074
|
-
if (reportSurvivors && partial.runtimeCallee) skipped.push({
|
|
2075
|
-
name,
|
|
2076
|
-
reason: "runtime-binding",
|
|
2077
|
-
start,
|
|
2078
|
-
end
|
|
2079
|
-
});
|
|
2080
|
-
candidates.push({
|
|
2081
|
-
item,
|
|
2082
|
-
call,
|
|
2083
|
-
node: call,
|
|
2084
|
-
start,
|
|
2085
|
-
end,
|
|
2086
|
-
...partial
|
|
2087
|
-
});
|
|
2088
|
-
continue;
|
|
2089
|
-
}
|
|
2090
1952
|
skipped.push({
|
|
2091
1953
|
name,
|
|
2092
1954
|
reason: "dynamic",
|
|
@@ -2105,12 +1967,220 @@ const foldSource = (options) => {
|
|
|
2105
1967
|
});
|
|
2106
1968
|
}
|
|
2107
1969
|
/**
|
|
1970
|
+
* Resolve every fully static candidate to symbolic declarations before allocating a class.
|
|
1971
|
+
*
|
|
1972
|
+
* The normal fold can wait until the rewrite loop to compute a class string. Semantic
|
|
1973
|
+
* composition cannot: an enclosing `cx()` needs the declarations of its arguments so it can
|
|
1974
|
+
* discard overridden values before any string exists.
|
|
1975
|
+
*/
|
|
1976
|
+
{
|
|
1977
|
+
for (const candidate of candidates) {
|
|
1978
|
+
if (candidate.styleSet || candidate.value !== void 0 || candidate.replacement) continue;
|
|
1979
|
+
const { item } = candidate;
|
|
1980
|
+
if (item.type === "css") {
|
|
1981
|
+
candidate.styleSet = styleCompiler.compose(...item.data);
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
if (item.type === "pattern") {
|
|
1985
|
+
candidate.styleSet = styleCompiler.compose(...item.data.map((entry) => ctx.patterns.transform(item.name ?? "", entry)));
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
if (item.type === "viewTransition") {
|
|
1989
|
+
const semantic = (0, _bamboocss_shared.viewTransitionClassName)(item.data[0], ctx.utility.prefix);
|
|
1990
|
+
candidate.className = styleCompiler.allocateClassString(semantic);
|
|
1991
|
+
candidate.classNames = [candidate.className];
|
|
1992
|
+
candidate.replacement = JSON.stringify(candidate.className);
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile();
|
|
1997
|
+
if (sourceFile) {
|
|
1998
|
+
const cxBindings = /* @__PURE__ */ new Set();
|
|
1999
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
2000
|
+
if (declaration.isTypeOnly() || !isBambooCssModule(declaration.getModuleSpecifierValue())) continue;
|
|
2001
|
+
for (const named of declaration.getNamedImports()) {
|
|
2002
|
+
if (named.isTypeOnly() || named.getNameNode().getText() !== "cx") continue;
|
|
2003
|
+
cxBindings.add((named.getAliasNode() ?? named.getNameNode()).getText());
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
const byRange = new Map(candidates.map((candidate) => [`${candidate.start}:${candidate.end}`, candidate]));
|
|
2007
|
+
for (const call of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression)) {
|
|
2008
|
+
const callee = call.getExpression();
|
|
2009
|
+
if (!ts_morph.Node.isIdentifier(callee) || !cxBindings.has(callee.getText()) || isShadowed(call, callee.getText())) continue;
|
|
2010
|
+
const matched = [];
|
|
2011
|
+
const parts = [];
|
|
2012
|
+
const dynamic = [];
|
|
2013
|
+
const constantCandidates = [];
|
|
2014
|
+
let supported = true;
|
|
2015
|
+
const take = (arg) => {
|
|
2016
|
+
const candidate = byRange.get(`${arg.getStart()}:${arg.getEnd()}`);
|
|
2017
|
+
if (candidate?.styleMap?.outputKind === "class") {
|
|
2018
|
+
dynamic.push(candidate);
|
|
2019
|
+
parts.push({
|
|
2020
|
+
kind: "dynamic",
|
|
2021
|
+
candidate
|
|
2022
|
+
});
|
|
2023
|
+
return true;
|
|
2024
|
+
}
|
|
2025
|
+
if (candidate?.styleSet) {
|
|
2026
|
+
matched.push(candidate);
|
|
2027
|
+
parts.push({
|
|
2028
|
+
kind: "style",
|
|
2029
|
+
candidate
|
|
2030
|
+
});
|
|
2031
|
+
return true;
|
|
2032
|
+
}
|
|
2033
|
+
if (candidate?.item.type === "viewTransition" && candidate.replacement && candidate.className) {
|
|
2034
|
+
constantCandidates.push(candidate);
|
|
2035
|
+
parts.push({
|
|
2036
|
+
kind: "class",
|
|
2037
|
+
value: candidate.className,
|
|
2038
|
+
candidate
|
|
2039
|
+
});
|
|
2040
|
+
return true;
|
|
2041
|
+
}
|
|
2042
|
+
if (ts_morph.Node.isStringLiteral(arg) || ts_morph.Node.isNoSubstitutionTemplateLiteral(arg)) {
|
|
2043
|
+
parts.push({
|
|
2044
|
+
kind: "class",
|
|
2045
|
+
value: arg.getLiteralValue()
|
|
2046
|
+
});
|
|
2047
|
+
return true;
|
|
2048
|
+
}
|
|
2049
|
+
if (ts_morph.Node.isArrayLiteralExpression(arg)) {
|
|
2050
|
+
for (const element of arg.getElements()) if (ts_morph.Node.isSpreadElement(element) || !take(element)) return false;
|
|
2051
|
+
return true;
|
|
2052
|
+
}
|
|
2053
|
+
if (arg.getKind() === ts_morph.SyntaxKind.FalseKeyword || arg.getKind() === ts_morph.SyntaxKind.TrueKeyword || ts_morph.Node.isNumericLiteral(arg) || arg.getKind() === ts_morph.SyntaxKind.NullKeyword || ts_morph.Node.isIdentifier(arg) && arg.getText() === "undefined") return true;
|
|
2054
|
+
return false;
|
|
2055
|
+
};
|
|
2056
|
+
for (const arg of call.getArguments()) {
|
|
2057
|
+
if (take(arg)) continue;
|
|
2058
|
+
supported = false;
|
|
2059
|
+
break;
|
|
2060
|
+
}
|
|
2061
|
+
if (dynamic.length > 1) {
|
|
2062
|
+
skipped.push({
|
|
2063
|
+
name: "cx",
|
|
2064
|
+
reason: "dynamic",
|
|
2065
|
+
start: call.getStart(),
|
|
2066
|
+
end: call.getEnd()
|
|
2067
|
+
});
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
if (!supported) {
|
|
2071
|
+
skipped.push({
|
|
2072
|
+
name: "cx",
|
|
2073
|
+
reason: "dynamic",
|
|
2074
|
+
start: call.getStart(),
|
|
2075
|
+
end: call.getEnd()
|
|
2076
|
+
});
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
if (dynamic.length === 1 && matched.length > 0) {
|
|
2080
|
+
const dynamicCandidate = dynamic[0];
|
|
2081
|
+
const styleParts = parts.filter((part) => part.kind !== "class");
|
|
2082
|
+
const dynamicIndex = styleParts.findIndex((part) => part.kind === "dynamic");
|
|
2083
|
+
const before = styleParts.slice(0, dynamicIndex).filter((part) => part.kind === "style").map((part) => part.candidate.styleSet);
|
|
2084
|
+
const after = styleParts.slice(dynamicIndex + 1).filter((part) => part.kind === "style").map((part) => part.candidate.styleSet);
|
|
2085
|
+
const compiled = dynamicCandidate.styleMap.compile(before, after);
|
|
2086
|
+
const expression = compiled.usesHelper && dynamicCandidate.mapHelperName && dynamicCandidate.mapHelperName !== "cvaMap" ? compiled.expression.replaceAll(`${RECIPE_MAP_HELPER}(`, `${dynamicCandidate.mapHelperName}(`) : compiled.expression;
|
|
2087
|
+
const arguments_ = [];
|
|
2088
|
+
let wroteCompiled = false;
|
|
2089
|
+
for (const part of parts) {
|
|
2090
|
+
if (part.kind === "class") {
|
|
2091
|
+
if (part.value) arguments_.push(JSON.stringify(part.value));
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
if (!wroteCompiled) {
|
|
2095
|
+
arguments_.push(expression);
|
|
2096
|
+
wroteCompiled = true;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
dynamicCandidate.subsumed = true;
|
|
2100
|
+
const first = styleParts[0].candidate;
|
|
2101
|
+
candidates.push({
|
|
2102
|
+
...first,
|
|
2103
|
+
call,
|
|
2104
|
+
node: call,
|
|
2105
|
+
start: call.getStart(),
|
|
2106
|
+
end: call.getEnd(),
|
|
2107
|
+
displayName: "cx",
|
|
2108
|
+
replacement: arguments_.length === 1 ? arguments_[0] : `${callee.getText()}(${arguments_.join(", ")})`,
|
|
2109
|
+
className: "",
|
|
2110
|
+
classNames: [...compiled.classNames, ...parts.filter((part) => part.kind === "class").flatMap((part) => part.value.split(" "))].filter(Boolean),
|
|
2111
|
+
styleSet: void 0,
|
|
2112
|
+
styleMap: void 0,
|
|
2113
|
+
outputKind: void 0,
|
|
2114
|
+
insert: compiled.usesHelper ? dynamicCandidate.insert : void 0,
|
|
2115
|
+
sourceBoxes: styleParts.flatMap((part) => [part.candidate.item.box, part.candidate.configBox]).concat(constantCandidates.map((candidate) => candidate.item.box)).filter(Boolean)
|
|
2116
|
+
});
|
|
2117
|
+
continue;
|
|
2118
|
+
}
|
|
2119
|
+
if (matched.length === 0) {
|
|
2120
|
+
if (constantCandidates.length === 0) continue;
|
|
2121
|
+
const className = parts.filter((part) => part.kind === "class").map((part) => part.value).filter(Boolean).join(" ");
|
|
2122
|
+
const first = constantCandidates[0];
|
|
2123
|
+
candidates.push({
|
|
2124
|
+
...first,
|
|
2125
|
+
call,
|
|
2126
|
+
node: call,
|
|
2127
|
+
start: call.getStart(),
|
|
2128
|
+
end: call.getEnd(),
|
|
2129
|
+
displayName: "cx",
|
|
2130
|
+
replacement: JSON.stringify(className),
|
|
2131
|
+
className,
|
|
2132
|
+
classNames: className.split(" ").filter(Boolean),
|
|
2133
|
+
sourceBoxes: constantCandidates.map((candidate) => candidate.item.box).filter(Boolean)
|
|
2134
|
+
});
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
const merged = styleCompiler.compose(...matched.map((candidate) => candidate.styleSet));
|
|
2138
|
+
const compiled = styleCompiler.className(merged);
|
|
2139
|
+
const classParts = [];
|
|
2140
|
+
let wroteCompiled = false;
|
|
2141
|
+
for (const part of parts) {
|
|
2142
|
+
if (part.kind === "class") {
|
|
2143
|
+
if (part.value) classParts.push(part.value);
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
if (!wroteCompiled && compiled) {
|
|
2147
|
+
classParts.push(compiled);
|
|
2148
|
+
wroteCompiled = true;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
const first = matched[0];
|
|
2152
|
+
candidates.push({
|
|
2153
|
+
...first,
|
|
2154
|
+
call,
|
|
2155
|
+
node: call,
|
|
2156
|
+
start: call.getStart(),
|
|
2157
|
+
end: call.getEnd(),
|
|
2158
|
+
displayName: "cx",
|
|
2159
|
+
replacement: JSON.stringify(classParts.join(" ")),
|
|
2160
|
+
className: classParts.join(" "),
|
|
2161
|
+
classNames: classParts.flatMap((part) => part.split(" ")).filter(Boolean),
|
|
2162
|
+
styleSet: merged,
|
|
2163
|
+
sourceBoxes: [...matched.flatMap((candidate) => [candidate.item.box, candidate.configBox]), ...constantCandidates.map((candidate) => candidate.item.box)].filter(Boolean)
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
for (const candidate of candidates) {
|
|
2168
|
+
if (!candidate.styleMap || candidate.subsumed || candidate.replacement) continue;
|
|
2169
|
+
const compiled = candidate.styleMap.compile();
|
|
2170
|
+
candidate.replacement = compiled.usesHelper && candidate.mapHelperName && candidate.mapHelperName !== "cvaMap" ? compiled.expression.replaceAll(`${RECIPE_MAP_HELPER}(`, `${candidate.mapHelperName}(`) : compiled.expression;
|
|
2171
|
+
if (!compiled.usesHelper) candidate.insert = void 0;
|
|
2172
|
+
candidate.className = compiled.staticClasses;
|
|
2173
|
+
candidate.classNames = compiled.classNames;
|
|
2174
|
+
candidate.outputKind = compiled.outputKind === "slots" ? "slots" : void 0;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2108
2178
|
* Ranges the rewrite actually replaced. Declared before the early return below, because
|
|
2109
2179
|
* that return is now also a reporting point: a module with nothing to fold is exactly the
|
|
2110
2180
|
* shape `reportSurvivors` exists to catch.
|
|
2111
2181
|
*/
|
|
2112
2182
|
const applied = [];
|
|
2113
|
-
if (candidates.length === 0) {
|
|
2183
|
+
if (candidates.length === 0 && recipeDefinitions.length === 0) {
|
|
2114
2184
|
if (reportSurvivors) reportRuntimeBindings();
|
|
2115
2185
|
return {
|
|
2116
2186
|
code,
|
|
@@ -2120,7 +2190,15 @@ const foldSource = (options) => {
|
|
|
2120
2190
|
dependencies: []
|
|
2121
2191
|
};
|
|
2122
2192
|
}
|
|
2123
|
-
const
|
|
2193
|
+
const rewriteSourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
|
|
2194
|
+
if (!rewriteSourceFile) return {
|
|
2195
|
+
code,
|
|
2196
|
+
map: null,
|
|
2197
|
+
folded,
|
|
2198
|
+
skipped,
|
|
2199
|
+
dependencies: []
|
|
2200
|
+
};
|
|
2201
|
+
const dependencyScan = createDependencyScan(rewriteSourceFile);
|
|
2124
2202
|
candidates.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
2125
2203
|
const magic = new magic_string.default(code);
|
|
2126
2204
|
const insertedNames = /* @__PURE__ */ new Set();
|
|
@@ -2134,7 +2212,7 @@ const foldSource = (options) => {
|
|
|
2134
2212
|
const collides = (edits) => edits.some(([start, end]) => applied.some(([from, to]) => start < to && from < end));
|
|
2135
2213
|
for (const candidate of candidates) {
|
|
2136
2214
|
const { item, start, end } = candidate;
|
|
2137
|
-
const name = item.name ?? item.type ?? "";
|
|
2215
|
+
const name = candidate.displayName ?? item.name ?? item.type ?? "";
|
|
2138
2216
|
const ranges = [[start, end]];
|
|
2139
2217
|
if (collides(ranges)) {
|
|
2140
2218
|
skipped.push({
|
|
@@ -2166,7 +2244,7 @@ const foldSource = (options) => {
|
|
|
2166
2244
|
applied.push(...ranges);
|
|
2167
2245
|
folded.push({
|
|
2168
2246
|
name,
|
|
2169
|
-
kind: "class",
|
|
2247
|
+
kind: candidate.outputKind ?? "class",
|
|
2170
2248
|
className: candidate.className,
|
|
2171
2249
|
classNames: (candidate.classNames ?? [candidate.className]).filter(Boolean),
|
|
2172
2250
|
start,
|
|
@@ -2174,24 +2252,13 @@ const foldSource = (options) => {
|
|
|
2174
2252
|
});
|
|
2175
2253
|
collectSourceFiles(item.box, dependencyScan);
|
|
2176
2254
|
if (candidate.configBox) collectSourceFiles(candidate.configBox, dependencyScan);
|
|
2255
|
+
for (const box of candidate.sourceBoxes ?? []) collectSourceFiles(box, dependencyScan);
|
|
2177
2256
|
continue;
|
|
2178
2257
|
}
|
|
2179
2258
|
let className;
|
|
2180
2259
|
try {
|
|
2181
2260
|
if (item.type === "pattern") className = runtimeCss(...item.data.map((entry) => ctx.patterns.transform(name, entry)));
|
|
2182
|
-
else
|
|
2183
|
-
const resolved = candidate.constantSlot ? runtimeRecipe(name, {}, candidate.slot) : item.data.length === 1 ? runtimeRecipe(name, item.data[0], candidate.slot) : void 0;
|
|
2184
|
-
if (resolved == null) {
|
|
2185
|
-
skipped.push({
|
|
2186
|
-
name,
|
|
2187
|
-
reason: "unsupported-kind",
|
|
2188
|
-
start,
|
|
2189
|
-
end
|
|
2190
|
-
});
|
|
2191
|
-
continue;
|
|
2192
|
-
}
|
|
2193
|
-
className = resolved;
|
|
2194
|
-
} else className = runtimeCss(...item.data);
|
|
2261
|
+
else className = runtimeCss(...item.data);
|
|
2195
2262
|
} catch {
|
|
2196
2263
|
skipped.push({
|
|
2197
2264
|
name,
|
|
@@ -2201,22 +2268,13 @@ const foldSource = (options) => {
|
|
|
2201
2268
|
});
|
|
2202
2269
|
continue;
|
|
2203
2270
|
}
|
|
2204
|
-
if (!className) {
|
|
2205
|
-
skipped.push({
|
|
2206
|
-
name,
|
|
2207
|
-
reason: "empty",
|
|
2208
|
-
start,
|
|
2209
|
-
end
|
|
2210
|
-
});
|
|
2211
|
-
continue;
|
|
2212
|
-
}
|
|
2213
2271
|
magic.overwrite(start, end, JSON.stringify(className));
|
|
2214
2272
|
applied.push(...ranges);
|
|
2215
2273
|
folded.push({
|
|
2216
2274
|
name,
|
|
2217
2275
|
kind: "class",
|
|
2218
2276
|
className,
|
|
2219
|
-
classNames: [className],
|
|
2277
|
+
classNames: className ? [className] : [],
|
|
2220
2278
|
start,
|
|
2221
2279
|
end
|
|
2222
2280
|
});
|
|
@@ -2232,7 +2290,6 @@ const foldSource = (options) => {
|
|
|
2232
2290
|
const importedConfig = !local && importsFor(recipeSourceFile).has(target.getText()) ? ctx.recipes.getConfig(target.getText()) : void 0;
|
|
2233
2291
|
const entry = local && local !== AMBIGUOUS ? local : importedConfig ? {
|
|
2234
2292
|
config: importedConfig,
|
|
2235
|
-
name: "",
|
|
2236
2293
|
box: void 0
|
|
2237
2294
|
} : void 0;
|
|
2238
2295
|
if (!entry) continue;
|
|
@@ -2250,17 +2307,21 @@ const foldSource = (options) => {
|
|
|
2250
2307
|
magic.overwrite(start, end, `${helper.name}(${args[0].getText()}, ${JSON.stringify(keys)})`);
|
|
2251
2308
|
applyInsert(helper.insert);
|
|
2252
2309
|
applied.push([start, end]);
|
|
2253
|
-
loweredSplitProps.add(target.getText());
|
|
2254
2310
|
}
|
|
2255
|
-
for (const
|
|
2256
|
-
if (!tally.seen || tally.lowered !== tally.seen) continue;
|
|
2257
|
-
const definition = recipeConfigs?.get(binding)?.box?.getNode?.();
|
|
2258
|
-
if (!definition) continue;
|
|
2259
|
-
const call = ts_morph.Node.isCallExpression(definition) ? definition : definition.getFirstAncestorByKind(ts_morph.SyntaxKind.CallExpression);
|
|
2260
|
-
if (!call) continue;
|
|
2311
|
+
for (const { name, call } of recipeDefinitions) {
|
|
2261
2312
|
const start = call.getStart();
|
|
2262
|
-
|
|
2263
|
-
|
|
2313
|
+
const end = call.getEnd();
|
|
2314
|
+
if (collides([[start, end]])) continue;
|
|
2315
|
+
magic.overwrite(start, end, "undefined");
|
|
2316
|
+
applied.push([start, end]);
|
|
2317
|
+
folded.push({
|
|
2318
|
+
name,
|
|
2319
|
+
kind: "definition",
|
|
2320
|
+
className: "",
|
|
2321
|
+
classNames: [],
|
|
2322
|
+
start,
|
|
2323
|
+
end
|
|
2324
|
+
});
|
|
2264
2325
|
}
|
|
2265
2326
|
/**
|
|
2266
2327
|
* Bindings from a bamboo module still referenced once every rewrite is applied.
|
|
@@ -2268,21 +2329,65 @@ const foldSource = (options) => {
|
|
|
2268
2329
|
* Deliberately not driven by `parserResult`: that is the recogniser, and the point here is
|
|
2269
2330
|
* to catch what it did not see. A namespace import called as `s.cva(...)`, a default
|
|
2270
2331
|
* import, a specifier that resolved to nothing — each leaves a live reference and no ledger
|
|
2271
|
-
* entry at all, which
|
|
2332
|
+
* entry at all, which used to let a build silently ship the engine.
|
|
2272
2333
|
*
|
|
2273
|
-
* The helpers the
|
|
2274
|
-
*
|
|
2275
|
-
*
|
|
2334
|
+
* The helpers the compiler writes are excluded because they pull no style engine. `cx` is
|
|
2335
|
+
* also allowed to remain when it joins an arbitrary external class; only fully analyzable
|
|
2336
|
+
* arguments receive Bamboo's semantic composition guarantee.
|
|
2276
2337
|
*/
|
|
2277
2338
|
function reportRuntimeBindings() {
|
|
2278
|
-
const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile();
|
|
2339
|
+
const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
|
|
2279
2340
|
if (!sourceFile) return;
|
|
2341
|
+
for (const [binding, entry] of recipeConfigs) {
|
|
2342
|
+
if (entry === AMBIGUOUS) continue;
|
|
2343
|
+
const definition = entry.box?.getNode?.();
|
|
2344
|
+
if (!definition || definition.getSourceFile() !== sourceFile) continue;
|
|
2345
|
+
const nameNode = definition.getFirstAncestorByKind(ts_morph.SyntaxKind.VariableDeclaration)?.getNameNode();
|
|
2346
|
+
if (!nameNode || !ts_morph.Node.isIdentifier(nameNode)) continue;
|
|
2347
|
+
if (skipped.filter((item) => SURVIVES_TO_RUNTIME.has(item.reason) && item.end > item.start).some((item) => nameNode.findReferencesAsNodes().some((ref) => ref.getStart() >= item.start && ref.getStart() < item.end))) continue;
|
|
2348
|
+
const survivor = nameNode.findReferencesAsNodes().find((ref) => !applied.some(([from, to]) => ref.getStart() >= from && ref.getStart() < to));
|
|
2349
|
+
if (!survivor) continue;
|
|
2350
|
+
skipped.push({
|
|
2351
|
+
name: binding,
|
|
2352
|
+
reason: "runtime-binding",
|
|
2353
|
+
start: survivor.getStart(),
|
|
2354
|
+
end: survivor.getEnd()
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2280
2357
|
const bambooModules = [
|
|
2281
2358
|
...cssModules,
|
|
2282
2359
|
...ctx.imports.matchers.recipe?.mods ?? [],
|
|
2283
2360
|
...ctx.imports.matchers.pattern?.mods ?? [],
|
|
2284
2361
|
...ctx.imports.matchers.tokens?.mods ?? []
|
|
2285
2362
|
];
|
|
2363
|
+
for (const call of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression)) {
|
|
2364
|
+
const callee = call.getExpression();
|
|
2365
|
+
const argument = call.getArguments()[0];
|
|
2366
|
+
if (!argument || !ts_morph.Node.isStringLiteral(argument) && !ts_morph.Node.isNoSubstitutionTemplateLiteral(argument)) continue;
|
|
2367
|
+
if (!matchesModule(argument.getLiteralValue(), bambooModules)) continue;
|
|
2368
|
+
const isDynamicImport = callee.getKind() === ts_morph.SyntaxKind.ImportKeyword;
|
|
2369
|
+
const isRequire = ts_morph.Node.isIdentifier(callee) && callee.getText() === "require" && !isShadowed(call, "require");
|
|
2370
|
+
if (!isDynamicImport && !isRequire) continue;
|
|
2371
|
+
skipped.push({
|
|
2372
|
+
name: isDynamicImport ? "import" : "require",
|
|
2373
|
+
reason: "runtime-binding",
|
|
2374
|
+
start: call.getStart(),
|
|
2375
|
+
end: call.getEnd()
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
for (const declaration of sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.ImportEqualsDeclaration)) {
|
|
2379
|
+
if (declaration.isTypeOnly()) continue;
|
|
2380
|
+
const reference = declaration.getModuleReference();
|
|
2381
|
+
if (!ts_morph.Node.isExternalModuleReference(reference)) continue;
|
|
2382
|
+
const expression = reference.getExpression();
|
|
2383
|
+
if (!expression || !ts_morph.Node.isStringLiteral(expression) || !matchesModule(expression.getLiteralValue(), bambooModules)) continue;
|
|
2384
|
+
skipped.push({
|
|
2385
|
+
name: declaration.getName(),
|
|
2386
|
+
reason: "runtime-binding",
|
|
2387
|
+
start: declaration.getStart(),
|
|
2388
|
+
end: declaration.getEnd()
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2286
2391
|
/** Local name -> what to call it in the report. */
|
|
2287
2392
|
const watched = /* @__PURE__ */ new Map();
|
|
2288
2393
|
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
@@ -2380,6 +2485,122 @@ const foldSource = (options) => {
|
|
|
2380
2485
|
};
|
|
2381
2486
|
};
|
|
2382
2487
|
//#endregion
|
|
2488
|
+
//#region src/style-set.ts
|
|
2489
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2490
|
+
/** A compound selector matches only through variant classes the recipe actually emits. */
|
|
2491
|
+
const matchesCompound = (compound, selection, variants) => {
|
|
2492
|
+
for (const [key, expected] of Object.entries(compound)) {
|
|
2493
|
+
if (key === "css") continue;
|
|
2494
|
+
const declared = variants?.[key];
|
|
2495
|
+
const selected = selection[key];
|
|
2496
|
+
if (selected == null || !declared || !Object.hasOwn(declared, String(selected))) return false;
|
|
2497
|
+
if (!(Array.isArray(expected) ? expected : [expected]).some((value) => value != null && String(selected) === String(value))) return false;
|
|
2498
|
+
}
|
|
2499
|
+
return true;
|
|
2500
|
+
};
|
|
2501
|
+
/**
|
|
2502
|
+
* Resolve the style fragments one recipe call contributes, in emitted-rule precedence.
|
|
2503
|
+
*
|
|
2504
|
+
* This intentionally rejects conditional variant *selections*. A scalar selects a style
|
|
2505
|
+
* object; an object such as `{ base: 'sm', md: 'lg' }` selects several objects under
|
|
2506
|
+
* conditions and needs a separate lowering. Returning `undefined` rejects that call instead
|
|
2507
|
+
* of silently compiling only one branch.
|
|
2508
|
+
*/
|
|
2509
|
+
const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (className) => className) => {
|
|
2510
|
+
const { mergeCssUncached } = (0, _bamboocss_shared.createMergeCss)(createCssContext(ctx));
|
|
2511
|
+
const compose = (...styles) => mergeCssUncached(...styles);
|
|
2512
|
+
const resolveRecipe = (config, input = {}, slot) => {
|
|
2513
|
+
const slots = Array.isArray(config.slots) ? config.slots : void 0;
|
|
2514
|
+
if (Boolean(slots) !== Boolean(slot)) return void 0;
|
|
2515
|
+
if (slot && !slots?.includes(slot)) return void 0;
|
|
2516
|
+
const selection = {
|
|
2517
|
+
...config.defaultVariants ?? {},
|
|
2518
|
+
...(0, _bamboocss_shared.compact)(input)
|
|
2519
|
+
};
|
|
2520
|
+
if (Object.values(selection).some((value) => isRecord(value))) return void 0;
|
|
2521
|
+
const fragments = [];
|
|
2522
|
+
const take = (candidate) => {
|
|
2523
|
+
if (!isRecord(candidate)) return;
|
|
2524
|
+
const styles = slot ? candidate[slot] : candidate;
|
|
2525
|
+
if (isRecord(styles)) fragments.push(styles);
|
|
2526
|
+
};
|
|
2527
|
+
take(config.base);
|
|
2528
|
+
for (const variant of Object.keys(config.variants ?? {})) {
|
|
2529
|
+
const value = selection[variant];
|
|
2530
|
+
if (value == null) continue;
|
|
2531
|
+
take(config.variants?.[variant]?.[String(value)]);
|
|
2532
|
+
}
|
|
2533
|
+
for (const compound of config.compoundVariants ?? []) {
|
|
2534
|
+
if (!isRecord(compound) || !matchesCompound(compound, selection, config.variants)) continue;
|
|
2535
|
+
take(compound.css);
|
|
2536
|
+
}
|
|
2537
|
+
return compose(...fragments);
|
|
2538
|
+
};
|
|
2539
|
+
return {
|
|
2540
|
+
compose,
|
|
2541
|
+
resolveRecipe,
|
|
2542
|
+
className: (...styles) => runtimeCss(...styles),
|
|
2543
|
+
allocateClassString
|
|
2544
|
+
};
|
|
2545
|
+
};
|
|
2546
|
+
//#endregion
|
|
2547
|
+
//#region src/static-session.ts
|
|
2548
|
+
const ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
2549
|
+
const denseName = (index) => {
|
|
2550
|
+
let value = index;
|
|
2551
|
+
let result = "";
|
|
2552
|
+
do {
|
|
2553
|
+
result = ALPHABET[value % 52] + result;
|
|
2554
|
+
value = Math.floor(value / 52) - 1;
|
|
2555
|
+
} while (value >= 0);
|
|
2556
|
+
return `_${result}`;
|
|
2557
|
+
};
|
|
2558
|
+
const createStaticCompilationSession = (denseClassNames = true) => {
|
|
2559
|
+
const mode = denseClassNames === true ? "stable" : denseClassNames;
|
|
2560
|
+
const session = {
|
|
2561
|
+
utilityLayer: "utilities",
|
|
2562
|
+
sourcemap: false,
|
|
2563
|
+
cssLoaded: false,
|
|
2564
|
+
transformedFiles: /* @__PURE__ */ new Set(),
|
|
2565
|
+
extractedFiles: /* @__PURE__ */ new Set(),
|
|
2566
|
+
prunableClasses: /* @__PURE__ */ new Set(),
|
|
2567
|
+
viewTransitionClasses: /* @__PURE__ */ new Set(),
|
|
2568
|
+
usedClasses: /* @__PURE__ */ new Set(),
|
|
2569
|
+
denseClasses: /* @__PURE__ */ new Map(),
|
|
2570
|
+
semanticClasses: /* @__PURE__ */ new Map(),
|
|
2571
|
+
denseClassNames: Boolean(mode),
|
|
2572
|
+
allocateClassString(className) {
|
|
2573
|
+
if (!mode) return className;
|
|
2574
|
+
return className.split(" ").filter(Boolean).map((semantic) => {
|
|
2575
|
+
let dense = session.denseClasses.get(semantic);
|
|
2576
|
+
if (!dense) {
|
|
2577
|
+
dense = mode === "local" ? denseName(session.denseClasses.size) : `_${(0, _bamboocss_shared.toHash)(semantic)}`;
|
|
2578
|
+
const collision = session.semanticClasses.get(dense);
|
|
2579
|
+
if (collision && collision !== semantic) throw new Error(`Bamboo compact class collision between ${JSON.stringify(collision)} and ${JSON.stringify(semantic)}. Disable \`denseClassNames\` for this build.`);
|
|
2580
|
+
session.denseClasses.set(semantic, dense);
|
|
2581
|
+
session.semanticClasses.set(dense, semantic);
|
|
2582
|
+
}
|
|
2583
|
+
return dense;
|
|
2584
|
+
}).join(" ");
|
|
2585
|
+
},
|
|
2586
|
+
markClassUsed(className) {
|
|
2587
|
+
const semantic = session.semanticClasses.get(className) ?? className;
|
|
2588
|
+
session.usedClasses.add((0, _bamboocss_shared.esc)(semantic));
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
return session;
|
|
2592
|
+
};
|
|
2593
|
+
const resetStaticCompilationSession = (session) => {
|
|
2594
|
+
session.cssLoaded = false;
|
|
2595
|
+
session.transformedFiles.clear();
|
|
2596
|
+
session.extractedFiles.clear();
|
|
2597
|
+
session.prunableClasses.clear();
|
|
2598
|
+
session.viewTransitionClasses.clear();
|
|
2599
|
+
session.usedClasses.clear();
|
|
2600
|
+
session.denseClasses.clear();
|
|
2601
|
+
session.semanticClasses.clear();
|
|
2602
|
+
};
|
|
2603
|
+
//#endregion
|
|
2383
2604
|
//#region src/plugin.ts
|
|
2384
2605
|
const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
|
|
2385
2606
|
const NODE_MODULES = /node_modules/;
|
|
@@ -2422,15 +2643,17 @@ const formatSkipped = (id, skipped) => {
|
|
|
2422
2643
|
*
|
|
2423
2644
|
* Two plugins, because they do unrelated jobs on different schedules. The first emits the
|
|
2424
2645
|
* stylesheet as a virtual module and runs in dev and build alike — that is the integration,
|
|
2425
|
-
* and nothing styles without it. The second
|
|
2646
|
+
* and nothing styles without it. The second compiles every Bamboo source call in both dev
|
|
2647
|
+
* and build; there is no runtime styling fallback.
|
|
2426
2648
|
*
|
|
2427
|
-
* The
|
|
2649
|
+
* The compiler runs with `enforce: 'pre'` so it sees module source as close as possible to what
|
|
2428
2650
|
* the CSS extractor reads off disk. A plugin that rewrites style calls before bamboo
|
|
2429
2651
|
* sees them would otherwise make the two disagree, and a folded class could end up
|
|
2430
2652
|
* with no matching rule.
|
|
2431
2653
|
*/
|
|
2432
2654
|
const bamboocss = (options = {}) => {
|
|
2433
|
-
const {
|
|
2655
|
+
const { configPath, cwd, reportSkipped = false, reportSummary = true, denseClassNames = true, maxRecipeStates } = options;
|
|
2656
|
+
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
2434
2657
|
/** Totals across the build, for the summary. */
|
|
2435
2658
|
const totals = {
|
|
2436
2659
|
folded: 0,
|
|
@@ -2438,8 +2661,35 @@ const bamboocss = (options = {}) => {
|
|
|
2438
2661
|
filesWithFolds: 0,
|
|
2439
2662
|
skipped: /* @__PURE__ */ new Map()
|
|
2440
2663
|
};
|
|
2441
|
-
|
|
2664
|
+
const staticSession = createStaticCompilationSession(denseClassNames);
|
|
2442
2665
|
const survivors = [];
|
|
2666
|
+
const survivorKeys = /* @__PURE__ */ new Set();
|
|
2667
|
+
const addSurvivor = (entry) => {
|
|
2668
|
+
const key = `${entry.file}:${entry.line}:${entry.name}:${entry.reason}`;
|
|
2669
|
+
if (survivorKeys.has(key)) return;
|
|
2670
|
+
survivorKeys.add(key);
|
|
2671
|
+
survivors.push(entry);
|
|
2672
|
+
};
|
|
2673
|
+
const clearSurvivorsFor = (file) => {
|
|
2674
|
+
for (let index = survivors.length - 1; index >= 0; index--) if (survivors[index]?.file === file) survivors.splice(index, 1);
|
|
2675
|
+
survivorKeys.clear();
|
|
2676
|
+
for (const entry of survivors) survivorKeys.add(`${entry.file}:${entry.line}:${entry.name}:${entry.reason}`);
|
|
2677
|
+
};
|
|
2678
|
+
const createSurvivorError = (entries) => {
|
|
2679
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2680
|
+
for (const entry of entries) {
|
|
2681
|
+
const list = byFile.get(entry.file) ?? [];
|
|
2682
|
+
list.push(entry);
|
|
2683
|
+
byFile.set(entry.file, list);
|
|
2684
|
+
}
|
|
2685
|
+
const named = (entry) => entry.reason === "runtime-binding" || entry.reason === "compile-failed" ? entry.name : `${entry.name}()`;
|
|
2686
|
+
const detail = (0, _bamboocss_shared.truncateList)(Array.from(byFile.entries(), ([file, fileEntries]) => [` ${file}`, ...fileEntries.map((entry) => ` ${entry.line}: ${named(entry)} — ${entry.reason}`)].join("\n")), {
|
|
2687
|
+
unit: "file",
|
|
2688
|
+
separator: "\n"
|
|
2689
|
+
});
|
|
2690
|
+
const threw = entries.some((entry) => entry.reason === "compile-failed");
|
|
2691
|
+
return /* @__PURE__ */ new Error(`bamboocss: ${entries.length} call(s) could not be compiled.\n\n${detail}\n\n` + (threw ? "`compile-failed` is a module the compiler threw on — see the error logged for it above. Nothing was established about its calls either way.\n\n" : "") + "Bamboo emits no runtime styling fallback or recipe layer. Make the values finite and statically analyzable, move variation into declared recipe variants, or safelist intentional dynamic classes with `staticCss`.");
|
|
2692
|
+
};
|
|
2443
2693
|
/**
|
|
2444
2694
|
* Recipe configs read out of modules other than the one being transformed.
|
|
2445
2695
|
*
|
|
@@ -2449,6 +2699,8 @@ const bamboocss = (options = {}) => {
|
|
|
2449
2699
|
const recipeConfigCache = /* @__PURE__ */ new Map();
|
|
2450
2700
|
let ctx;
|
|
2451
2701
|
let runtimeCss;
|
|
2702
|
+
let styleCompiler;
|
|
2703
|
+
let command = "build";
|
|
2452
2704
|
let setup;
|
|
2453
2705
|
const ensureContext = async () => {
|
|
2454
2706
|
if (!setup) setup = (0, _bamboocss_node.loadConfigAndCreateContext)({
|
|
@@ -2456,25 +2708,31 @@ const bamboocss = (options = {}) => {
|
|
|
2456
2708
|
cwd
|
|
2457
2709
|
}).then((loaded) => {
|
|
2458
2710
|
ctx = loaded;
|
|
2459
|
-
|
|
2711
|
+
const semanticCss = createRuntimeCss(loaded);
|
|
2712
|
+
runtimeCss = (...styles) => staticSession.allocateClassString(semanticCss(...styles));
|
|
2713
|
+
styleCompiler = createStaticStyleSetCompiler(loaded, runtimeCss, staticSession.allocateClassString);
|
|
2460
2714
|
});
|
|
2461
2715
|
await setup;
|
|
2462
2716
|
};
|
|
2463
2717
|
return [bamboocssCss({
|
|
2464
2718
|
configPath,
|
|
2465
|
-
cwd
|
|
2719
|
+
cwd,
|
|
2720
|
+
session: staticSession
|
|
2466
2721
|
}), {
|
|
2467
|
-
name: "bamboocss:
|
|
2722
|
+
name: "bamboocss:compiler",
|
|
2468
2723
|
enforce: "pre",
|
|
2469
|
-
|
|
2724
|
+
configResolved(config) {
|
|
2725
|
+
command = config.command;
|
|
2726
|
+
},
|
|
2470
2727
|
async buildStart() {
|
|
2471
|
-
if (!transform) return;
|
|
2472
2728
|
totals.folded = 0;
|
|
2473
2729
|
totals.files = 0;
|
|
2474
2730
|
totals.filesWithFolds = 0;
|
|
2475
2731
|
totals.skipped.clear();
|
|
2476
2732
|
survivors.length = 0;
|
|
2733
|
+
survivorKeys.clear();
|
|
2477
2734
|
recipeConfigCache.clear();
|
|
2735
|
+
resetStaticCompilationSession(staticSession);
|
|
2478
2736
|
await ensureContext();
|
|
2479
2737
|
},
|
|
2480
2738
|
/**
|
|
@@ -2498,7 +2756,7 @@ const bamboocss = (options = {}) => {
|
|
|
2498
2756
|
* the parser still holds the file.
|
|
2499
2757
|
*/
|
|
2500
2758
|
watchChange(id, change) {
|
|
2501
|
-
if (!
|
|
2759
|
+
if (!ctx) return;
|
|
2502
2760
|
if (!shouldTransform(id)) return;
|
|
2503
2761
|
const [filePath] = id.split("?");
|
|
2504
2762
|
if (!filePath) return;
|
|
@@ -2510,101 +2768,90 @@ const bamboocss = (options = {}) => {
|
|
|
2510
2768
|
ctx.project.reloadSourceFile(filePath);
|
|
2511
2769
|
},
|
|
2512
2770
|
async transform(code, id) {
|
|
2513
|
-
if (!transform) return null;
|
|
2514
2771
|
if (!shouldTransform(id)) return null;
|
|
2515
2772
|
await ensureContext();
|
|
2516
|
-
if (!ctx || !runtimeCss) return null;
|
|
2773
|
+
if (!ctx || !runtimeCss || !styleCompiler) return null;
|
|
2517
2774
|
const [filePath] = id.split("?");
|
|
2775
|
+
clearSurvivorsFor(filePath);
|
|
2518
2776
|
if (isGeneratedOutput(filePath, ctx)) return null;
|
|
2519
2777
|
let result;
|
|
2520
2778
|
try {
|
|
2521
2779
|
const sourceFile = ctx.project.addSourceFile(filePath, code);
|
|
2522
2780
|
const parserResult = ctx.project.parseSourceFile(filePath);
|
|
2523
|
-
if (!parserResult
|
|
2781
|
+
if (!parserResult) return null;
|
|
2524
2782
|
result = foldSource({
|
|
2525
2783
|
ctx,
|
|
2526
2784
|
code,
|
|
2527
2785
|
parserResult,
|
|
2528
2786
|
filePath,
|
|
2529
2787
|
runtimeCss,
|
|
2530
|
-
|
|
2788
|
+
styleCompiler,
|
|
2789
|
+
maxRecipeStates,
|
|
2531
2790
|
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
2532
2791
|
recipeConfigCache,
|
|
2533
|
-
reportSurvivors:
|
|
2792
|
+
reportSurvivors: true,
|
|
2534
2793
|
sourceFile
|
|
2535
2794
|
});
|
|
2536
2795
|
} catch (error) {
|
|
2537
|
-
_bamboocss_logger.logger.caughtError("vite:transform", `Failed to
|
|
2796
|
+
_bamboocss_logger.logger.caughtError("vite:transform", `Failed to compile ${filePath}`, error);
|
|
2538
2797
|
totals.files++;
|
|
2539
|
-
totals.skipped.set("
|
|
2540
|
-
|
|
2798
|
+
totals.skipped.set("compile-failed", (totals.skipped.get("compile-failed") ?? 0) + 1);
|
|
2799
|
+
addSurvivor({
|
|
2541
2800
|
file: filePath,
|
|
2542
2801
|
line: 1,
|
|
2543
|
-
name: "
|
|
2544
|
-
reason: "
|
|
2802
|
+
name: "compiler",
|
|
2803
|
+
reason: "compile-failed"
|
|
2545
2804
|
});
|
|
2805
|
+
if (command === "serve") throw error;
|
|
2546
2806
|
return null;
|
|
2547
2807
|
}
|
|
2548
2808
|
totals.files++;
|
|
2549
2809
|
totals.folded += result.folded.length;
|
|
2550
2810
|
if (result.folded.length) totals.filesWithFolds++;
|
|
2551
2811
|
for (const entry of result.skipped) totals.skipped.set(entry.reason, (totals.skipped.get(entry.reason) ?? 0) + 1);
|
|
2552
|
-
if (
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
name: entry.name,
|
|
2559
|
-
reason: entry.reason
|
|
2560
|
-
});
|
|
2561
|
-
}
|
|
2562
|
-
if ((ctx.config.leafFallback ?? true) && result.code.includes("cssLeaf(")) survivors.push({
|
|
2812
|
+
if (result.folded.some((entry) => entry.kind === "class" || entry.kind === "slots")) staticSession.transformedFiles.add((0, node_path.resolve)(filePath));
|
|
2813
|
+
for (const entry of result.folded) for (const className of entry.classNames) staticSession.markClassUsed(className);
|
|
2814
|
+
for (const entry of result.skipped) {
|
|
2815
|
+
if (entry.reason === "not-imported" || entry.reason === "overlapping") continue;
|
|
2816
|
+
if (entry.name === "cx" && entry.reason === "dynamic") continue;
|
|
2817
|
+
addSurvivor({
|
|
2563
2818
|
file: filePath,
|
|
2564
|
-
line: lineAt(
|
|
2565
|
-
name:
|
|
2566
|
-
reason:
|
|
2819
|
+
line: lineAt(code, entry.start),
|
|
2820
|
+
name: entry.name,
|
|
2821
|
+
reason: entry.reason
|
|
2567
2822
|
});
|
|
2568
2823
|
}
|
|
2569
2824
|
if (reportSkipped && result.skipped.length) _bamboocss_logger.logger.info("vite:transform", formatSkipped(filePath, result.skipped));
|
|
2570
2825
|
for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
|
|
2826
|
+
if (command === "serve" && survivors.some((entry) => entry.file === filePath)) throw createSurvivorError(survivors.filter((entry) => entry.file === filePath));
|
|
2571
2827
|
if (!result.folded.length) return null;
|
|
2572
|
-
_bamboocss_logger.logger.debug("vite:transform", `
|
|
2828
|
+
_bamboocss_logger.logger.debug("vite:transform", `Compiled ${result.folded.length} call(s) in ${filePath}`);
|
|
2573
2829
|
return {
|
|
2574
2830
|
code: result.code,
|
|
2575
2831
|
map: result.map
|
|
2576
2832
|
};
|
|
2577
2833
|
},
|
|
2578
2834
|
buildEnd() {
|
|
2579
|
-
if (
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
byFile.set(entry.file, list);
|
|
2585
|
-
}
|
|
2586
|
-
const named = (e) => e.reason === "runtime-binding" || e.reason === "fold-failed" ? e.name : `${e.name}()`;
|
|
2587
|
-
const detail = (0, _bamboocss_shared.truncateList)(Array.from(byFile.entries(), ([file, entries]) => [` ${file}`, ...entries.map((e) => ` ${e.line}: ${named(e)} — ${e.reason}`)].join("\n")), {
|
|
2835
|
+
if (survivors.length) throw createSurvivorError(survivors);
|
|
2836
|
+
if (typeof this.getModuleInfo === "function") {
|
|
2837
|
+
if (!staticSession.cssLoaded) throw new Error(`bamboocss: compiled class values were produced, but ${JSON.stringify(VIRTUAL_CSS_ID)} was not imported. Import it once from the application entry so the compiled rules are emitted.`);
|
|
2838
|
+
const outsideExtraction = [...staticSession.transformedFiles].filter((file) => !staticSession.extractedFiles.has(file));
|
|
2839
|
+
if (outsideExtraction.length) throw new Error(`bamboocss: ${outsideExtraction.length} statically compiled module(s) are outside the CSS extraction graph:\n\n${(0, _bamboocss_shared.truncateList)(outsideExtraction.map((file) => ` ${file}`), {
|
|
2588
2840
|
unit: "file",
|
|
2589
2841
|
separator: "\n"
|
|
2590
|
-
});
|
|
2591
|
-
const threw = survivors.some((entry) => entry.reason === "fold-failed");
|
|
2592
|
-
throw new Error(`bamboocss: ${survivors.length} call(s) could not be folded, and \`failOnUnfolded\` is on.\n\n${detail}\n\n` + (threw ? "`fold-failed` is a module the fold threw on — see the error logged for it above. Nothing was established about its calls either way, so it cannot support the guarantee this option makes.\n\n" : "") + "Each one keeps `styled-system/css` in the bundle, so the engine cannot be dropped however many other calls folded. Make the values static, move the variation into a `cva` variant, or generate them with `staticCss` — or set `failOnUnfolded: false` to accept the runtime.");
|
|
2842
|
+
})}\n\nAdd them to \`include\` in bamboo.config, or no CSS rule can back their emitted classes.`);
|
|
2593
2843
|
}
|
|
2594
|
-
if (!
|
|
2844
|
+
if (!reportSummary) return;
|
|
2595
2845
|
const declined = Array.from(totals.skipped.values()).reduce((sum, count) => sum + count, 0);
|
|
2596
2846
|
const total = totals.folded + declined;
|
|
2597
2847
|
if (!total) return;
|
|
2598
2848
|
const share = Math.round(totals.folded / total * 100);
|
|
2599
2849
|
const reasons = Array.from(totals.skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
|
|
2600
|
-
_bamboocss_logger.logger.info("vite:transform", `
|
|
2850
|
+
_bamboocss_logger.logger.info("vite:transform", `Compiled ${totals.folded}/${total} (${share}%) across ${totals.filesWithFolds}/${totals.files} files` + (reasons ? ` — declined: ${reasons}` : ""));
|
|
2601
2851
|
}
|
|
2602
2852
|
}];
|
|
2603
2853
|
};
|
|
2604
2854
|
//#endregion
|
|
2605
2855
|
exports.VIRTUAL_CSS_ID = VIRTUAL_CSS_ID;
|
|
2606
2856
|
exports.bamboocss = bamboocss;
|
|
2607
|
-
exports.bamboocssCss = bamboocssCss;
|
|
2608
|
-
exports.createRuntimeCss = createRuntimeCss;
|
|
2609
2857
|
exports.default = bamboocss;
|
|
2610
|
-
exports.foldSource = foldSource;
|