@bamboocss/vite 1.45.5 → 1.46.1
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/chunk.cjs +28 -0
- package/dist/class-name.cjs +17 -0
- package/dist/class-name.mjs +12 -0
- package/dist/config-module.cjs +9 -0
- package/dist/config-module.mjs +2 -0
- package/dist/css-output-module.cjs +218 -0
- package/dist/css-output-module.mjs +211 -0
- package/dist/fold-module.cjs +2487 -0
- package/dist/fold-module.mjs +2483 -0
- package/dist/index.cjs +1215 -3078
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +1216 -3054
- package/dist/node-module.cjs +9 -0
- package/dist/node-module.mjs +2 -0
- package/package.json +15 -13
package/dist/index.cjs
CHANGED
|
@@ -2,161 +2,69 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
var __defProp = Object.defineProperty;
|
|
8
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
14
|
-
key = keys[i];
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
16
|
-
get: ((k) => from[k]).bind(null, key),
|
|
17
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
return to;
|
|
21
|
-
};
|
|
22
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
-
value: mod,
|
|
24
|
-
enumerable: true
|
|
25
|
-
}) : target, mod));
|
|
26
|
-
//#endregion
|
|
27
|
-
let _bamboocss_config = require("@bamboocss/config");
|
|
28
|
-
let _bamboocss_node = require("@bamboocss/node");
|
|
5
|
+
require("./chunk.cjs");
|
|
6
|
+
const require_class_name = require("./class-name.cjs");
|
|
29
7
|
let _bamboocss_logger = require("@bamboocss/logger");
|
|
30
8
|
let _bamboocss_shared = require("@bamboocss/shared");
|
|
31
|
-
let _ampproject_remapping = require("@ampproject/remapping");
|
|
32
|
-
_ampproject_remapping = __toESM(_ampproject_remapping);
|
|
33
|
-
let magic_string = require("magic-string");
|
|
34
|
-
magic_string = __toESM(magic_string);
|
|
35
|
-
let postcss = require("postcss");
|
|
36
|
-
postcss = __toESM(postcss);
|
|
37
|
-
let postcss_selector_parser = require("postcss-selector-parser");
|
|
38
|
-
postcss_selector_parser = __toESM(postcss_selector_parser);
|
|
39
9
|
let node_crypto = require("node:crypto");
|
|
40
10
|
let node_fs = require("node:fs");
|
|
41
11
|
let node_path = require("node:path");
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
let ts_morph = require("ts-morph");
|
|
45
|
-
//#region src/prune-static-css.ts
|
|
46
|
-
/** The generated declaration that identifies a Bamboo stylesheet after minification. */
|
|
47
|
-
const SENTINEL = "--made-with-bamboo";
|
|
12
|
+
let _bamboocss_node_static_compiler = require("@bamboocss/node/static-compiler");
|
|
13
|
+
//#region src/lazy-modules.ts
|
|
48
14
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* The same class reaches the stylesheet either escaped or not — `--bottom-mask-size_16px` is a
|
|
52
|
-
* valid selector as written, since a CSS ident may begin with `--`, while `esc` produces the
|
|
53
|
-
* escaped `\--…` form that reachability keys are stored in. Comparing raw spellings therefore
|
|
54
|
-
* missed a rule written the other way, and pruning removed an atom whose rule was in the sheet
|
|
55
|
-
* all along. It could only ever affect names needing an escape, which is why it presented as
|
|
56
|
-
* every custom property and vendor-prefixed declaration losing its rule at once.
|
|
15
|
+
* Keep one asynchronous initialization in flight, retain its fulfilled value, and forget only
|
|
16
|
+
* a rejected attempt so a later Vite rebuild can recover.
|
|
57
17
|
*
|
|
58
|
-
*
|
|
18
|
+
* Starting through a resolved promise also turns a synchronous loader throw into the same
|
|
19
|
+
* rejected-promise contract as a failed dynamic import.
|
|
20
|
+
*/
|
|
21
|
+
const createRetryableLazy = (load) => {
|
|
22
|
+
let pending;
|
|
23
|
+
return () => {
|
|
24
|
+
if (pending) return pending;
|
|
25
|
+
const attempt = Promise.resolve().then(load);
|
|
26
|
+
pending = attempt;
|
|
27
|
+
attempt.catch(() => {
|
|
28
|
+
if (pending === attempt) pending = void 0;
|
|
29
|
+
});
|
|
30
|
+
return attempt;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** Process-wide module loading; individual plugin instances still own their mutable state. */
|
|
34
|
+
const loadNodeModule = createRetryableLazy(() => Promise.resolve().then(() => require("./node-module.cjs")));
|
|
35
|
+
const loadConfigModule = createRetryableLazy(() => Promise.resolve().then(() => require("./config-module.cjs")));
|
|
36
|
+
/**
|
|
37
|
+
* Keep stylesheet parsing, selector inventory and late sourcemap rewriting behind one built
|
|
38
|
+
* chunk. The injectable loader proves sharing and retry without exposing a package subpath.
|
|
59
39
|
*/
|
|
60
|
-
const
|
|
40
|
+
const createLazyCssOutputModule = (loadCssOutput = () => Promise.resolve().then(() => require("./css-output-module.cjs"))) => createRetryableLazy(loadCssOutput);
|
|
41
|
+
/** One process-wide CSS-output load shared by every plugin instance and Vite environment. */
|
|
42
|
+
const loadCssOutputModule = createLazyCssOutputModule();
|
|
61
43
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* additions are absent and survive as a safelist; graph atoms are governed by the transformed
|
|
66
|
-
* module reachability set, regardless of whether they originated in `css()` or a recipe.
|
|
44
|
+
* Keep the AST fold behind its own built chunk. The injectable loader is an architectural
|
|
45
|
+
* seam: callers can prove sharing and retry without a production-only switch, while the
|
|
46
|
+
* default remains a statically discoverable dynamic import for both published formats.
|
|
67
47
|
*/
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
48
|
+
const createLazyFoldModule = (loadFold = () => Promise.resolve().then(() => require("./fold-module.cjs"))) => createRetryableLazy(loadFold);
|
|
49
|
+
/** One process-wide fold-module load shared by every plugin instance and Vite environment. */
|
|
50
|
+
const loadFoldModule = createLazyFoldModule();
|
|
51
|
+
/** One Builder per CSS plugin instance, created only when a hook first needs it. */
|
|
52
|
+
const createLazyBuilder = (loadNode = loadNodeModule) => createRetryableLazy(async () => {
|
|
53
|
+
const { Builder } = await loadNode();
|
|
54
|
+
return new Builder();
|
|
55
|
+
});
|
|
56
|
+
/** Build and publish one complete fold state; no caller can observe partial initialization. */
|
|
57
|
+
const createLazyCompilerState = (loadContext, loadFold) => createRetryableLazy(async () => {
|
|
58
|
+
const [context, fold] = await Promise.all([loadContext(), loadFold()]);
|
|
59
|
+
const runtimeCss = fold.createRuntimeCss(context);
|
|
60
|
+
const styleCompiler = fold.createStaticStyleSetCompiler(context, runtimeCss);
|
|
61
|
+
return {
|
|
62
|
+
context,
|
|
63
|
+
foldSource: fold.foldSource,
|
|
64
|
+
runtimeCss,
|
|
65
|
+
styleCompiler
|
|
83
66
|
};
|
|
84
|
-
|
|
85
|
-
if (!isUtilityRule(rule)) return;
|
|
86
|
-
let removedAny = false;
|
|
87
|
-
let selector;
|
|
88
|
-
try {
|
|
89
|
-
selector = (0, postcss_selector_parser.default)((selectors) => {
|
|
90
|
-
selectors.each((candidate) => {
|
|
91
|
-
const classes = /* @__PURE__ */ new Set();
|
|
92
|
-
candidate.walkClasses((classNode) => {
|
|
93
|
-
classes.add(bare(classNode.toString().slice(1)));
|
|
94
|
-
});
|
|
95
|
-
if (classes.size !== 1) return;
|
|
96
|
-
const [className] = classes;
|
|
97
|
-
if (!className || !prunable.has(className) || used.has(className)) return;
|
|
98
|
-
candidate.remove();
|
|
99
|
-
removedAny = true;
|
|
100
|
-
session.prunedClasses.add(className);
|
|
101
|
-
});
|
|
102
|
-
}).processSync(rule.selector);
|
|
103
|
-
} catch {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (!removedAny) return;
|
|
107
|
-
if (!selector.trim()) {
|
|
108
|
-
rule.remove();
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
rule.selector = selector;
|
|
112
|
-
});
|
|
113
|
-
let removed = true;
|
|
114
|
-
while (removed) {
|
|
115
|
-
removed = false;
|
|
116
|
-
root.walkAtRules((rule) => {
|
|
117
|
-
if (rule.nodes?.length !== 0) return;
|
|
118
|
-
rule.remove();
|
|
119
|
-
removed = true;
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
if (prune) {
|
|
123
|
-
const present = /* @__PURE__ */ new Set();
|
|
124
|
-
root.walkRules((rule) => {
|
|
125
|
-
if (!isUtilityRule(rule)) return;
|
|
126
|
-
try {
|
|
127
|
-
(0, postcss_selector_parser.default)((selectors) => {
|
|
128
|
-
selectors.walkClasses((classNode) => {
|
|
129
|
-
present.add(bare(classNode.toString().slice(1)));
|
|
130
|
-
});
|
|
131
|
-
}).processSync(rule.selector);
|
|
132
|
-
} catch {}
|
|
133
|
-
});
|
|
134
|
-
const orphaned = [];
|
|
135
|
-
for (const className of session.usedClasses) {
|
|
136
|
-
if (/\s/.test(className)) {
|
|
137
|
-
orphaned.push(className);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
if (!prunable.has(bare(className))) continue;
|
|
141
|
-
if (present.has(bare(className))) continue;
|
|
142
|
-
orphaned.push(className);
|
|
143
|
-
}
|
|
144
|
-
if (orphaned.length) {
|
|
145
|
-
const describe = (className) => {
|
|
146
|
-
if (/\s/.test(className)) return ` ${className}\n (malformed key: a class name cannot contain whitespace)`;
|
|
147
|
-
const extracted = session.prunableClasses.has(className) ? "in the extracted atoms" : "NOT extracted";
|
|
148
|
-
const bare = className.replaceAll("\\", "");
|
|
149
|
-
const near = [...present].filter((candidate) => candidate !== className && candidate.replaceAll("\\", "") === bare);
|
|
150
|
-
return ` ${className}\n (${extracted}; no rule in the sheet` + (near.length ? `; a rule exists under ${near.map((n) => JSON.stringify(n)).join(", ")}` : "") + `)`;
|
|
151
|
-
};
|
|
152
|
-
throw new Error(`bamboocss: ${orphaned.length} compiled class(es) have no rule in the emitted stylesheet. Elements carrying them would render unstyled.\n\n${(0, _bamboocss_shared.truncateList)(orphaned.map(describe), {
|
|
153
|
-
unit: "class",
|
|
154
|
-
separator: "\n"
|
|
155
|
-
})}\n\nThis is a compiler bug rather than anything to fix in your source. Please report it with the block above — the parenthesised part is what distinguishes an atom that was never emitted from one whose rule is present under a different spelling.`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return root.toString();
|
|
159
|
-
};
|
|
67
|
+
});
|
|
160
68
|
//#endregion
|
|
161
69
|
//#region src/static-session.ts
|
|
162
70
|
const createStaticCompilationSession = () => {
|
|
@@ -170,8 +78,19 @@ const createStaticCompilationSession = () => {
|
|
|
170
78
|
viewTransitionClasses: /* @__PURE__ */ new Set(),
|
|
171
79
|
usedClasses: /* @__PURE__ */ new Set(),
|
|
172
80
|
expectedEnvironments: void 0,
|
|
173
|
-
|
|
81
|
+
participatingEnvironments: /* @__PURE__ */ new Set(),
|
|
82
|
+
completedEnvironments: /* @__PURE__ */ new Set(),
|
|
174
83
|
prunedClasses: /* @__PURE__ */ new Set(),
|
|
84
|
+
beginOutputProjection(_environment, _outputOptions, _bundle, replacesGeneratedStylesheet) {
|
|
85
|
+
if (replacesGeneratedStylesheet) session.prunedClasses.clear();
|
|
86
|
+
const prunable = new Set([...session.prunableClasses].map((className) => className.replaceAll("\\", "")));
|
|
87
|
+
const requiredClasses = new Set([...session.usedClasses].filter((className) => prunable.has(className.replaceAll("\\", ""))));
|
|
88
|
+
return {
|
|
89
|
+
cssLoaded: session.cssLoaded,
|
|
90
|
+
requiredClasses,
|
|
91
|
+
restore() {}
|
|
92
|
+
};
|
|
93
|
+
},
|
|
175
94
|
markClassUsed(className) {
|
|
176
95
|
for (const token of className.split(" ")) {
|
|
177
96
|
if (!token) continue;
|
|
@@ -182,30 +101,28 @@ const createStaticCompilationSession = () => {
|
|
|
182
101
|
return session;
|
|
183
102
|
};
|
|
184
103
|
/**
|
|
185
|
-
*
|
|
104
|
+
* Expected or observed environments whose current generation has not completed yet.
|
|
186
105
|
*
|
|
187
106
|
* Empty means everything the run will contribute has been contributed, which is the condition
|
|
188
107
|
* every whole-run judgement here waits for: pruning the stylesheet against reachability, and
|
|
189
108
|
* the two guards that ask whether the compiled modules and the extraction graph agree. Each of
|
|
190
109
|
* those is false about a build in progress and true only about a finished one.
|
|
191
110
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
111
|
+
* The environment currently at `buildEnd` may be supplied as the candidate completing this
|
|
112
|
+
* call. That keeps publication transactional: whole-run checks can include its finished graph
|
|
113
|
+
* without marking it complete before those checks themselves succeed.
|
|
114
|
+
*
|
|
115
|
+
* Empty is also the answer for a single-environment build, where the only participant is the
|
|
116
|
+
* completing candidate — so that path is unchanged.
|
|
194
117
|
*
|
|
195
118
|
* An environment a run declares and then never builds leaves this permanently non-empty, and
|
|
196
119
|
* those judgements are skipped for the run. Every one of them errs towards shipping more CSS
|
|
197
120
|
* or asserting less, so that is the safe direction to be wrong in.
|
|
198
121
|
*/
|
|
199
|
-
const remainingEnvironments = (session
|
|
200
|
-
const
|
|
201
|
-
session.
|
|
202
|
-
session.
|
|
203
|
-
session.extractedFiles.clear();
|
|
204
|
-
session.prunableClasses.clear();
|
|
205
|
-
session.viewTransitionClasses.clear();
|
|
206
|
-
session.usedClasses.clear();
|
|
207
|
-
session.startedEnvironments.clear();
|
|
208
|
-
session.prunedClasses.clear();
|
|
122
|
+
const remainingEnvironments = (session, completingEnvironment) => {
|
|
123
|
+
const participating = new Set(session.expectedEnvironments ?? []);
|
|
124
|
+
for (const environment of session.participatingEnvironments) participating.add(environment);
|
|
125
|
+
return [...participating].filter((name) => name !== completingEnvironment && !session.completedEnvironments.has(name));
|
|
209
126
|
};
|
|
210
127
|
//#endregion
|
|
211
128
|
//#region src/css.ts
|
|
@@ -252,95 +169,6 @@ const queryOf = (id) => {
|
|
|
252
169
|
* `transform`, and both are answered by the same middleware.
|
|
253
170
|
*/
|
|
254
171
|
const asError = (error, context) => error instanceof Error ? error : new Error(`bamboocss: ${context}: ${String(error)}`, { cause: error });
|
|
255
|
-
const INLINE_SOURCE_MAP = /\n?\/\/# sourceMappingURL=data:application\/json[^\n]*$/;
|
|
256
|
-
/** Rewrite one generated chunk without invalidating all mappings after the changed string. */
|
|
257
|
-
const replaceChunkReference = (chunk, bundle, previous, next, sourcemap) => {
|
|
258
|
-
if (!chunk.code.includes(previous)) return;
|
|
259
|
-
const magic = new magic_string.default(chunk.code);
|
|
260
|
-
let index = chunk.code.indexOf(previous);
|
|
261
|
-
while (index !== -1) {
|
|
262
|
-
magic.overwrite(index, index + previous.length, next);
|
|
263
|
-
index = chunk.code.indexOf(previous, index + previous.length);
|
|
264
|
-
}
|
|
265
|
-
chunk.code = magic.toString();
|
|
266
|
-
if (!chunk.map) return;
|
|
267
|
-
const file = chunk.map.file;
|
|
268
|
-
const debugId = chunk.map.debugId;
|
|
269
|
-
const combined = (0, _ampproject_remapping.default)([magic.generateMap({
|
|
270
|
-
source: chunk.fileName,
|
|
271
|
-
hires: "boundary"
|
|
272
|
-
}), chunk.map], () => null);
|
|
273
|
-
if (file) combined.file = file;
|
|
274
|
-
if (debugId) combined.debugId = debugId;
|
|
275
|
-
const rollupMap = combined;
|
|
276
|
-
rollupMap.toUrl = () => `data:application/json;charset=utf-8;base64,${Buffer.from(combined.toString()).toString("base64")}`;
|
|
277
|
-
chunk.map = rollupMap;
|
|
278
|
-
if (sourcemap === "inline") {
|
|
279
|
-
chunk.code = chunk.code.replace(INLINE_SOURCE_MAP, "");
|
|
280
|
-
chunk.code += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from(combined.toString()).toString("base64")}`;
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
const mapAsset = bundle[`${chunk.fileName}.map`];
|
|
284
|
-
if (mapAsset?.type === "asset") mapAsset.source = combined.toString();
|
|
285
|
-
};
|
|
286
|
-
/** Replace an emitted filename wherever Vite or Rollup has already recorded it. */
|
|
287
|
-
const replaceAssetReferences = (bundle, previous, next, sourcemap) => {
|
|
288
|
-
const replace = (value) => value.replaceAll(previous, next);
|
|
289
|
-
for (const output of Object.values(bundle)) {
|
|
290
|
-
if (output.type === "asset") {
|
|
291
|
-
if (typeof output.source === "string") output.source = replace(output.source);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
replaceChunkReference(output, bundle, previous, next, sourcemap);
|
|
295
|
-
const referencedFiles = output.referencedFiles;
|
|
296
|
-
if (referencedFiles) output.referencedFiles = referencedFiles.map(replace);
|
|
297
|
-
const importedCss = output.viteMetadata?.importedCss;
|
|
298
|
-
if (importedCss?.delete(previous)) importedCss.add(next);
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
/**
|
|
302
|
-
* Could this bundle entry be the generated stylesheet?
|
|
303
|
-
*
|
|
304
|
-
* The filename is checked before the bytes because the alternative decodes every asset in the
|
|
305
|
-
* bundle to a UTF-8 string in order to search it — fonts, images and sourcemaps included. On an
|
|
306
|
-
* app with a large asset graph that is seconds of decode and a lot of garbage, twice over, to
|
|
307
|
-
* answer a question the extension already answers. The marker is a CSS custom property, so it
|
|
308
|
-
* cannot occur anywhere but CSS.
|
|
309
|
-
*/
|
|
310
|
-
const carriesGeneratedCss = (output) => output.type === "asset" && output.fileName.endsWith(".css");
|
|
311
|
-
/**
|
|
312
|
-
* Prune compiler-owned CSS, then give any sheet whose bytes changed a hash of those bytes.
|
|
313
|
-
*
|
|
314
|
-
* Rollup has already expanded `[hash]` when `generateBundle` runs. Mutating only `source`
|
|
315
|
-
* would therefore leave two different reachable subsets under one CDN key. The extra final
|
|
316
|
-
* hash is not cosmetic: it makes late graph reachability cache-safe.
|
|
317
|
-
*
|
|
318
|
-
* Renaming is therefore not a choice this takes. Pruned bytes under the unpruned sheet's name
|
|
319
|
-
* is the one outcome that must never be reachable, and a sheet nothing was removed from keeps
|
|
320
|
-
* its name because its bytes are unchanged — so "rename" is a consequence of "the bytes moved",
|
|
321
|
-
* not a second option. `prune` is the only knob.
|
|
322
|
-
*/
|
|
323
|
-
const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
324
|
-
const { prune = true, sourcemap = session.sourcemap } = options;
|
|
325
|
-
/** Assets in this bundle that carry the generated stylesheet, pruned or not. */
|
|
326
|
-
let sheets = 0;
|
|
327
|
-
for (const output of Object.values(bundle)) {
|
|
328
|
-
if (!carriesGeneratedCss(output)) continue;
|
|
329
|
-
const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString();
|
|
330
|
-
if (!source.includes("--made-with-bamboo")) continue;
|
|
331
|
-
sheets++;
|
|
332
|
-
if (!prune) continue;
|
|
333
|
-
const optimized = pruneStaticCss(source, session);
|
|
334
|
-
output.source = optimized;
|
|
335
|
-
if (optimized === source) continue;
|
|
336
|
-
const nextName = output.fileName.replace(/\.css$/, `.b-${(0, _bamboocss_shared.toHash)(optimized)}.css`);
|
|
337
|
-
if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
|
|
338
|
-
const previous = output.fileName;
|
|
339
|
-
output.fileName = nextName;
|
|
340
|
-
replaceAssetReferences(bundle, previous, nextName, sourcemap);
|
|
341
|
-
}
|
|
342
|
-
return { sheets };
|
|
343
|
-
};
|
|
344
172
|
/**
|
|
345
173
|
* Serve bamboo's stylesheet as a virtual module, in dev and in build.
|
|
346
174
|
*
|
|
@@ -355,28 +183,50 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
|
355
183
|
* process just wrote, which is a race on any watch rebuild.
|
|
356
184
|
*/
|
|
357
185
|
const bamboocssCss = (options) => {
|
|
358
|
-
const { configPath, cwd, session, pruneCss = true } = options;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
*/
|
|
367
|
-
const servedEnvironments = /* @__PURE__ */ new Set();
|
|
368
|
-
const builder = new _bamboocss_node.Builder();
|
|
186
|
+
const { configPath, cwd, loadCssOutput = loadCssOutputModule, session, pruneCss = true } = options;
|
|
187
|
+
let builder;
|
|
188
|
+
const loadBuilder = createLazyBuilder();
|
|
189
|
+
const ensureBuilder = async () => {
|
|
190
|
+
const loaded = await loadBuilder();
|
|
191
|
+
builder = loaded;
|
|
192
|
+
return loaded;
|
|
193
|
+
};
|
|
369
194
|
let server;
|
|
370
195
|
let command = "build";
|
|
371
196
|
/** The run's own `build` options, for a bundler with no per-environment config. */
|
|
372
197
|
let ssrBuildOptions;
|
|
198
|
+
/** Included owners plus local modules their extraction actually resolved and read. */
|
|
199
|
+
const extractedSourceFiles = () => {
|
|
200
|
+
const activeBuilder = builder;
|
|
201
|
+
const context = activeBuilder?.context;
|
|
202
|
+
if (!context) return [];
|
|
203
|
+
return [...new Set([...context.getFiles(), ...activeBuilder.getResolutionReadFiles()].map((file) => context.runtime.path.abs(context.config.cwd, file)))];
|
|
204
|
+
};
|
|
373
205
|
/**
|
|
374
206
|
* Serialised, because both `load` and the watcher can reach it and `Builder` keeps one
|
|
375
207
|
* context. Two overlapping passes would extract into the same encoder and emit the
|
|
376
208
|
* stylesheet twice over.
|
|
377
209
|
*/
|
|
378
210
|
let pending;
|
|
211
|
+
/**
|
|
212
|
+
* Which change the current `pending` was generated for, and the validated sheet it produced.
|
|
213
|
+
*
|
|
214
|
+
* Every environment loads the virtual stylesheet — a react-router dev server loads it once
|
|
215
|
+
* for the client graph and once for SSR — and each load used to run a complete extraction
|
|
216
|
+
* and optimization pass to produce byte-identical CSS. The sheet is a function of the source
|
|
217
|
+
* files alone, and the watcher below is the single point every event that can reach it
|
|
218
|
+
* passes through — Vite's own propagation only arrives via the watch edges `load` registers,
|
|
219
|
+
* over the same extracted files the watcher checks. A monotonic counter bumped there is
|
|
220
|
+
* therefore enough to know whether a build already reflects the world a load is asking about.
|
|
221
|
+
*
|
|
222
|
+
* Dev only. A production build has no dev watcher to advance the counter, so serving the
|
|
223
|
+
* memo there would hand `vite build --watch` a stale sheet; builds regenerate per load.
|
|
224
|
+
*/
|
|
225
|
+
let changeGeneration = 0;
|
|
226
|
+
let pendingGeneration = -1;
|
|
227
|
+
let servedCss;
|
|
379
228
|
const build = async () => {
|
|
229
|
+
const builder = await ensureBuilder();
|
|
380
230
|
await builder.setup({
|
|
381
231
|
configPath,
|
|
382
232
|
cwd,
|
|
@@ -388,7 +238,7 @@ const bamboocssCss = (options) => {
|
|
|
388
238
|
if (builder.context) {
|
|
389
239
|
session.utilityLayer = builder.context.config.layers?.utilities ?? "utilities";
|
|
390
240
|
session.extractedFiles.clear();
|
|
391
|
-
for (const file of
|
|
241
|
+
for (const file of extractedSourceFiles()) session.extractedFiles.add(file);
|
|
392
242
|
}
|
|
393
243
|
let graphAtomHashes;
|
|
394
244
|
if (builder.context) {
|
|
@@ -409,9 +259,11 @@ const bamboocssCss = (options) => {
|
|
|
409
259
|
session.prunableClasses.add((0, _bamboocss_shared.esc)(transition.className));
|
|
410
260
|
}
|
|
411
261
|
}
|
|
412
|
-
return
|
|
262
|
+
return css;
|
|
413
263
|
};
|
|
414
264
|
const generate = () => {
|
|
265
|
+
if (command === "serve" && pending && pendingGeneration === changeGeneration) return pending;
|
|
266
|
+
pendingGeneration = changeGeneration;
|
|
415
267
|
pending = Promise.resolve(pending).catch(() => void 0).then(build);
|
|
416
268
|
return pending;
|
|
417
269
|
};
|
|
@@ -435,10 +287,43 @@ const bamboocssCss = (options) => {
|
|
|
435
287
|
let prebuilt;
|
|
436
288
|
let prebuildStarted = false;
|
|
437
289
|
const prebuild = async () => {
|
|
438
|
-
if (prebuildStarted)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
290
|
+
if (!prebuildStarted) {
|
|
291
|
+
prebuildStarted = true;
|
|
292
|
+
prebuilt = generate();
|
|
293
|
+
}
|
|
294
|
+
const attempt = prebuilt ?? pending;
|
|
295
|
+
if (!attempt) return;
|
|
296
|
+
try {
|
|
297
|
+
await attempt;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (prebuilt === attempt || prebuilt === void 0 && pending === attempt) {
|
|
300
|
+
prebuildStarted = false;
|
|
301
|
+
prebuilt = void 0;
|
|
302
|
+
}
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* The dev config graph is independent of Builder setup and is needed one hook earlier. Keep
|
|
308
|
+
* its module load and graph walk single-flight as well: shared plugins may be resolved for
|
|
309
|
+
* client and SSR concurrently, but both describe the same Vite config.
|
|
310
|
+
*/
|
|
311
|
+
const configDependencyLoaders = /* @__PURE__ */ new Map();
|
|
312
|
+
const discoverConfigDependencies = (root) => {
|
|
313
|
+
const projectRoot = cwd ?? root;
|
|
314
|
+
let discover = configDependencyLoaders.get(projectRoot);
|
|
315
|
+
if (!discover) {
|
|
316
|
+
const created = createRetryableLazy(async () => {
|
|
317
|
+
const { findConfig, getConfigDependencies } = await loadConfigModule();
|
|
318
|
+
return getConfigDependencies(findConfig({
|
|
319
|
+
cwd: projectRoot,
|
|
320
|
+
file: configPath
|
|
321
|
+
})).deps;
|
|
322
|
+
});
|
|
323
|
+
configDependencyLoaders.set(projectRoot, created);
|
|
324
|
+
discover = created;
|
|
325
|
+
}
|
|
326
|
+
return discover();
|
|
442
327
|
};
|
|
443
328
|
return {
|
|
444
329
|
name: "bamboocss:css",
|
|
@@ -453,7 +338,7 @@ const bamboocssCss = (options) => {
|
|
|
453
338
|
* happened once per environment.
|
|
454
339
|
*/
|
|
455
340
|
sharedDuringBuild: true,
|
|
456
|
-
configResolved(config) {
|
|
341
|
+
async configResolved(config) {
|
|
457
342
|
command = config.command;
|
|
458
343
|
session.sourcemap = config.build.sourcemap;
|
|
459
344
|
ssrBuildOptions = {
|
|
@@ -487,10 +372,7 @@ const bamboocssCss = (options) => {
|
|
|
487
372
|
* that meant.
|
|
488
373
|
*/
|
|
489
374
|
if (config.command === "serve") try {
|
|
490
|
-
const
|
|
491
|
-
cwd: cwd ?? config.root,
|
|
492
|
-
file: configPath
|
|
493
|
-
}));
|
|
375
|
+
const deps = await discoverConfigDependencies(config.root);
|
|
494
376
|
config.configFileDependencies.push(...deps);
|
|
495
377
|
} catch {}
|
|
496
378
|
if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
|
|
@@ -519,2600 +401,153 @@ const bamboocssCss = (options) => {
|
|
|
519
401
|
} catch (error) {
|
|
520
402
|
throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
|
|
521
403
|
}
|
|
522
|
-
},
|
|
523
|
-
resolveId(id) {
|
|
524
|
-
const query = queryOf(id);
|
|
525
|
-
const base = id.slice(0, id.length - query.length);
|
|
526
|
-
if (base !== "virtual:bamboo.css" && base !== RESOLVED_ID) return null;
|
|
527
|
-
return `${RESOLVED_ID}${query}`;
|
|
528
|
-
},
|
|
529
|
-
async load(id) {
|
|
530
|
-
const query = queryOf(id);
|
|
531
|
-
if (id.slice(0, id.length - query.length) !== RESOLVED_ID) return null;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
prebuilt = void 0;
|
|
538
|
-
css = await (first ?? generate());
|
|
539
|
-
} catch (error) {
|
|
540
|
-
throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
|
|
541
|
-
}
|
|
542
|
-
if (this.addWatchFile) for (const file of builder.context?.getFiles() ?? []) this.addWatchFile(builder.context.runtime.path.abs(builder.context.config.cwd, file));
|
|
543
|
-
return css;
|
|
544
|
-
},
|
|
545
|
-
configureServer(devServer) {
|
|
546
|
-
server = devServer;
|
|
547
|
-
/**
|
|
548
|
-
* The graph the stylesheet's own module lives in, which is the one that has to reach it.
|
|
549
|
-
*
|
|
550
|
-
* `load` registers every extracted file with `addWatchFile`, and `vite:css-analysis`
|
|
551
|
-
* turns those into real importer edges — the virtual module ends up a direct importer of
|
|
552
|
-
* each file the extractor read. So an edit to any of them propagates to the stylesheet on
|
|
553
|
-
* Vite's own pass, in whichever environment holds that edge.
|
|
554
|
-
*
|
|
555
|
-
* The client one, because CSS is a client concern: an ssr environment never applies a
|
|
556
|
-
* stylesheet update, and asking whether *any* environment matched would skip the forced
|
|
557
|
-
* reload below for a server-only module whose styles the client still has to be told
|
|
558
|
-
* about. Vite 5 has one graph and no `environments`, where the question is exact.
|
|
559
|
-
*/
|
|
560
|
-
const clientGraph = devServer.environments?.client?.moduleGraph ?? devServer.moduleGraph;
|
|
561
|
-
const invalidate = (file) => {
|
|
562
|
-
const ctx = builder.context;
|
|
563
|
-
if (!ctx) return;
|
|
564
|
-
if (!ctx.getFiles().some((f) => ctx.runtime.path.abs(ctx.config.cwd, f) === file)) return;
|
|
565
|
-
prebuilt = void 0;
|
|
566
|
-
const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
|
|
567
|
-
if (!mod) return;
|
|
568
|
-
if (clientGraph.getModulesByFile(file)?.size) return;
|
|
569
|
-
server?.moduleGraph.invalidateModule(mod);
|
|
570
|
-
server?.reloadModule(mod);
|
|
571
|
-
_bamboocss_logger.logger.debug("vite", `styles invalidated by ${file}`);
|
|
572
|
-
};
|
|
573
|
-
devServer.watcher.on("change", invalidate);
|
|
574
|
-
devServer.watcher.on("add", invalidate);
|
|
575
|
-
devServer.watcher.on("unlink", invalidate);
|
|
576
|
-
},
|
|
577
|
-
generateBundle: {
|
|
578
|
-
order: "post",
|
|
579
|
-
handler(_, bundle) {
|
|
580
|
-
const environment = this.environment;
|
|
581
|
-
/**
|
|
582
|
-
* Every environment of this run has already had its modules compiled.
|
|
583
|
-
*
|
|
584
|
-
* Reachability is what pruning removes rules against, and it is only complete once
|
|
585
|
-
* nothing is left to contribute to it. The stylesheet is emitted and finalized by the
|
|
586
|
-
* environment that *imports* it, which in an SSR app is the client — and the client
|
|
587
|
-
* builds first, before the server environment has transformed a single module. Pruning
|
|
588
|
-
* there deletes every rule for a class only the server graph reaches, and the pages
|
|
589
|
-
* link the pruned copy: one project lost 39% of its atoms that way, presenting as
|
|
590
|
-
* rarely-used classes such as `md:{display:inline-block}` silently not applying.
|
|
591
|
-
*
|
|
592
|
-
* So the full extracted stylesheet ships instead, which is what `pruneCss: false` asks
|
|
593
|
-
* for by hand. Being the last environment is not the common case — frameworks build the
|
|
594
|
-
* client first — but it is the only one where the answer is knowable, and a framework
|
|
595
|
-
* that builds its server bundle first does get pruned output.
|
|
596
|
-
*/
|
|
597
|
-
const pending = remainingEnvironments(session);
|
|
598
|
-
const { sheets } = optimizeStaticCssAssets(bundle, session, {
|
|
599
|
-
prune: pruneCss && pending.length === 0,
|
|
600
|
-
sourcemap: environment?.config?.build?.sourcemap
|
|
601
|
-
});
|
|
602
|
-
if (sheets && !pruneCss) _bamboocss_logger.logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
|
|
603
|
-
else if (sheets && pending.length) _bamboocss_logger.logger.info("vite", `Reachability pruning skipped: the stylesheet is emitted by the ${JSON.stringify(environment?.name ?? "default")} environment, and ${(0, _bamboocss_shared.truncateList)(pending, {
|
|
604
|
-
unit: "environment",
|
|
605
|
-
separator: ", "
|
|
606
|
-
})} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
|
|
607
|
-
if (!servedEnvironments.has(environment?.name ?? "default")) return;
|
|
608
|
-
if (!session.transformedFiles.size) return;
|
|
609
|
-
/**
|
|
610
|
-
* An SSR bundle emits no CSS assets, and is not supposed to.
|
|
611
|
-
*
|
|
612
|
-
* `build.ssrEmitAssets` is off by default, so Vite discards them: the client build is
|
|
613
|
-
* what carries the stylesheet, and a server bundle that imports `virtual:bamboo.css`
|
|
614
|
-
* from shared code — a root component, a layout — still asks this plugin to load it.
|
|
615
|
-
* Which means the environment *served* the sheet and then emitted nothing, and the
|
|
616
|
-
* check below read that as the failure it exists to catch.
|
|
617
|
-
*
|
|
618
|
-
* It fails a build that is entirely correct. Qwik's `vite build --ssr` is the shape
|
|
619
|
-
* that showed it: 7/7 calls compiled, the client bundle carrying the stylesheet, and
|
|
620
|
-
* the server bundle refusing to finish. React Router does not hit it only because its
|
|
621
|
-
* plugin turns `ssrEmitAssets` on.
|
|
622
|
-
*
|
|
623
|
-
* Read per environment where that exists, falling back to the run's own config, so
|
|
624
|
-
* Vite 5's single-config builds are answered by the same question.
|
|
625
|
-
*/
|
|
626
|
-
const buildOptions = environment?.config?.build ?? ssrBuildOptions;
|
|
627
|
-
if (buildOptions?.ssr && !buildOptions.ssrEmitAssets) return;
|
|
628
|
-
if (!Object.values(bundle).some((output) => {
|
|
629
|
-
if (!carriesGeneratedCss(output)) return false;
|
|
630
|
-
return (typeof output.source === "string" ? output.source : Buffer.from(output.source).toString()).includes("--made-with-bamboo");
|
|
631
|
-
})) throw new Error(`bamboocss: ${session.transformedFiles.size} module(s) were compiled to Bamboo class values, but no emitted asset carries the generated stylesheet. The build would ship unstyled.\n\nThis happens when another plugin, or the bundler itself, drops or replaces the CSS asset after it is emitted. If you are on Rolldown, report this — the rename that used to cause it is already disabled there. Otherwise look for a plugin running in \`generateBundle\` that rewrites CSS assets.`);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
};
|
|
635
|
-
};
|
|
636
|
-
//#endregion
|
|
637
|
-
//#region src/fold-analysis.ts
|
|
638
|
-
/**
|
|
639
|
-
* Nodes that *compose* a value out of their children rather than computing one.
|
|
640
|
-
*
|
|
641
|
-
* The boundary of the walk below, and the whole of its precision. Climbing through these
|
|
642
|
-
* keeps a value's provenance: `'red.300'` inside `{ color: 'red.300' }` inside a default is
|
|
643
|
-
* still the default's. Anything else — a call, a function body, a JSX element — produces its
|
|
644
|
-
* value by being evaluated, so what is written inside it is an argument to that evaluation
|
|
645
|
-
* and not the enclosing default's value.
|
|
646
|
-
*
|
|
647
|
-
* Without the boundary, `({ cls = css({ color: 'red.300' }) }) => cls` is rejected: the call's
|
|
648
|
-
* own literal argument is syntactically inside a default, so an unbounded walk calls it one.
|
|
649
|
-
* That is correct code, and rejecting it fails the build.
|
|
650
|
-
*/
|
|
651
|
-
const composesValue = (node) => ts_morph.Node.isObjectLiteralExpression(node) || ts_morph.Node.isArrayLiteralExpression(node) || ts_morph.Node.isPropertyAssignment(node) || ts_morph.Node.isShorthandPropertyAssignment(node) || ts_morph.Node.isSpreadAssignment(node) || ts_morph.Node.isSpreadElement(node) || ts_morph.Node.isAsExpression(node) || ts_morph.Node.isParenthesizedExpression(node) || ts_morph.Node.isNonNullExpression(node) || ts_morph.Node.isTypeAssertion(node) || ts_morph.Node.isSatisfiesExpression(node);
|
|
652
|
-
/** Is `inner` written within `outer`? Positions rather than a walk, so it is O(1). */
|
|
653
|
-
const contains = (outer, inner) => outer.getSourceFile() === inner.getSourceFile() && outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
|
|
654
|
-
/**
|
|
655
|
-
* Did this value come from the `= …` of a destructuring binding?
|
|
656
|
-
*
|
|
657
|
-
* `const { tone = 'red.300' } = source` boxes as the literal `'red.300'`: the extractor's
|
|
658
|
-
* `maybeDefinitionValue` tests for an initializer first and returns the boxed default, never
|
|
659
|
-
* reaching the branch that would read `source`. So the default is reported as the value whether
|
|
660
|
-
* or not it is the one that applies.
|
|
661
|
-
*
|
|
662
|
-
* For extraction that is merely optimistic, and deliberately so: a CLI or PostCSS build ships a
|
|
663
|
-
* runtime `css()`, where the default genuinely does apply when the caller omits the key, and it
|
|
664
|
-
* needs a rule behind it. Folding is where the same resolution turns into a wrong answer,
|
|
665
|
-
* because the call is *replaced* by that value.
|
|
666
|
-
*
|
|
667
|
-
* Stops at the first non-composing parent, so a call written inside a default keeps its own
|
|
668
|
-
* provenance, and checks that the binding element was reached through its initializer, so
|
|
669
|
-
* `{ tone = X }`'s name node is not mistaken for its default.
|
|
670
|
-
*/
|
|
671
|
-
const isBindingElementDefault = (node) => {
|
|
672
|
-
if (node && ts_morph.Node.isCallExpression(node)) return false;
|
|
673
|
-
let current = node;
|
|
674
|
-
while (current) {
|
|
675
|
-
const parent = current.getParent();
|
|
676
|
-
if (!parent) return false;
|
|
677
|
-
if (ts_morph.Node.isBindingElement(parent)) return parent.getInitializer() === current;
|
|
678
|
-
if (!composesValue(parent)) return false;
|
|
679
|
-
current = parent;
|
|
680
|
-
}
|
|
681
|
-
return false;
|
|
682
|
-
};
|
|
683
|
-
/**
|
|
684
|
-
* The same question asked of a whole box, including how it was resolved.
|
|
685
|
-
*
|
|
686
|
-
* The node a box reports is not always the one its value came from — an empty `{}` default
|
|
687
|
-
* boxes against the call rather than against the `{}` — so the resolution stack is consulted
|
|
688
|
-
* too. A binding element reaches that stack by having been resolved *through*, which is the
|
|
689
|
-
* signal the extractor itself reads when one of these is a conditional's test.
|
|
690
|
-
*
|
|
691
|
-
* Only the entries that are binding elements are examined. Walking up from every other entry
|
|
692
|
-
* was tried and never once changed a verdict across the default spellings or the sandbox's own
|
|
693
|
-
* modules, while accounting for most of the parent hops this does — the stack carries nodes
|
|
694
|
-
* that were never resolved through, including a call's own arguments, so walking from them is
|
|
695
|
-
* both the expensive half and the one that reaches conclusions it has no basis for.
|
|
696
|
-
*
|
|
697
|
-
* A binding element without an initializer carries no default to mistrust: `const { tone } =
|
|
698
|
-
* source` either resolves from `source` or does not resolve at all.
|
|
699
|
-
*/
|
|
700
|
-
const isFromBindingDefault = (node) => {
|
|
701
|
-
const own = node.getNode?.();
|
|
702
|
-
if (isBindingElementDefault(own)) return true;
|
|
703
|
-
for (const entry of node.getStack?.() ?? []) {
|
|
704
|
-
if (!ts_morph.Node.isBindingElement(entry) || !entry.getInitializer()) continue;
|
|
705
|
-
if (own && contains(entry, own)) continue;
|
|
706
|
-
return true;
|
|
707
|
-
}
|
|
708
|
-
return false;
|
|
709
|
-
};
|
|
710
|
-
/**
|
|
711
|
-
* Statically resolvable means: every box in the tree carries a known value.
|
|
712
|
-
*
|
|
713
|
-
* `unresolvable` is the extractor saying it could not evaluate a node.
|
|
714
|
-
* `conditional` is a ternary — two possible values, so there is no single string to
|
|
715
|
-
* fold to. `box.fallback` produces an object with no `type` at all, which is likewise
|
|
716
|
-
* not something we can trust.
|
|
717
|
-
*/
|
|
718
|
-
const isStaticBox = (node, seen = /* @__PURE__ */ new Set()) => {
|
|
719
|
-
if (!node) return false;
|
|
720
|
-
if (seen.has(node)) return true;
|
|
721
|
-
seen.add(node);
|
|
722
|
-
if (_bamboocss_extractor.box.isUnresolvable(node) || _bamboocss_extractor.box.isConditional(node)) return false;
|
|
723
|
-
if (!("type" in node) || node.type == null) return false;
|
|
724
|
-
if (isFromBindingDefault(node)) return false;
|
|
725
|
-
if (_bamboocss_extractor.box.isLiteral(node) && node.value === void 0) {
|
|
726
|
-
const source = node.getNode?.();
|
|
727
|
-
return Boolean(source && ts_morph.Node.isIdentifier(source) && source.getText() === "undefined");
|
|
728
|
-
}
|
|
729
|
-
if (_bamboocss_extractor.box.isMap(node)) {
|
|
730
|
-
for (const child of node.value.values()) if (!isStaticBox(child, seen)) return false;
|
|
731
|
-
return true;
|
|
732
|
-
}
|
|
733
|
-
if (_bamboocss_extractor.box.isArray(node)) {
|
|
734
|
-
for (const child of node.value) if (!isStaticBox(child, seen)) return false;
|
|
735
|
-
return true;
|
|
736
|
-
}
|
|
737
|
-
return true;
|
|
738
|
-
};
|
|
739
|
-
/**
|
|
740
|
-
* Strip the wrappers the extractor strips before it builds a box, so the source node
|
|
741
|
-
* compared against a box is the same node the box was built from.
|
|
742
|
-
*
|
|
743
|
-
* A local copy of the extractor's `unwrapExpression`, which is not part of its public
|
|
744
|
-
* surface. Recognising fewer wrappers than it does is not a cosmetic difference: an
|
|
745
|
-
* unrecognised one leaves an object literal wrapped, and the object checks below skip it.
|
|
746
|
-
*/
|
|
747
|
-
const unwrapExpression = (node) => ts_morph.Node.isAsExpression(node) || ts_morph.Node.isParenthesizedExpression(node) || ts_morph.Node.isNonNullExpression(node) || ts_morph.Node.isTypeAssertion(node) || ts_morph.Node.isSatisfiesExpression(node) ? unwrapExpression(node.getExpression()) : node;
|
|
748
|
-
/**
|
|
749
|
-
* Mirrors the parser's evaluator environment, so re-boxing an operand here gets the same
|
|
750
|
-
* answer the extraction did. Left to its default, ts-evaluator presets to `NODE` and
|
|
751
|
-
* would resolve expressions the parser cannot see — making this check *more* permissive
|
|
752
|
-
* than the extraction it is auditing, which is the one thing it must never be.
|
|
753
|
-
*/
|
|
754
|
-
const REBOXED = { getEvaluateOptions: () => ({ environment: { preset: "ECMA" } }) };
|
|
755
|
-
const rebox = (node) => (0, _bamboocss_extractor.maybeBoxNode)(unwrapExpression(node), [], REBOXED);
|
|
756
|
-
/**
|
|
757
|
-
* Did this operand resolve to a value the program will actually produce?
|
|
758
|
-
*
|
|
759
|
-
* Producing a box is not enough when the operand is itself a choice: `a || b || c` parses
|
|
760
|
-
* as `(a || b) || c`, so the outer operator is handed whatever the inner one answered —
|
|
761
|
-
* including an arm the extractor invented. Asking only "is there a box" reads that
|
|
762
|
-
* invention as an ordinary literal.
|
|
763
|
-
*/
|
|
764
|
-
const resolvesExactly = (node) => {
|
|
765
|
-
const inner = unwrapExpression(node);
|
|
766
|
-
if (!rebox(inner)) return false;
|
|
767
|
-
return ts_morph.Node.isConditionalExpression(inner) || isCollapsedBinary(inner) ? decidedAtBuildTime(inner) : true;
|
|
768
|
-
};
|
|
769
|
-
/**
|
|
770
|
-
* Is this operand's value written here, rather than named?
|
|
771
|
-
*
|
|
772
|
-
* A box records what the extractor resolved a name *through* — a `let`'s initializer, a
|
|
773
|
-
* parameter's default — none of which is what the operand holds when the call runs.
|
|
774
|
-
* `let m = '1'; m = undefined` still boxes as `'1'`, and `({ c = 'red.300' })` still boxes
|
|
775
|
-
* as `'red.300'` for a caller that passed something else. Only a value written at the call
|
|
776
|
-
* site is what it appears to be, so only that can be judged truthy or nullish here.
|
|
777
|
-
*/
|
|
778
|
-
const isWrittenHere = (node) => {
|
|
779
|
-
const inner = unwrapExpression(node);
|
|
780
|
-
return ts_morph.Node.isStringLiteral(inner) || ts_morph.Node.isNumericLiteral(inner) || ts_morph.Node.isNoSubstitutionTemplateLiteral(inner) || ts_morph.Node.isObjectLiteralExpression(inner) || ts_morph.Node.isArrayLiteralExpression(inner) || ts_morph.Node.isTrueLiteral(inner) || ts_morph.Node.isFalseLiteral(inner) || inner.getKind() === ts_morph.SyntaxKind.NullKeyword || ts_morph.Node.isPrefixUnaryExpression(inner) && ts_morph.Node.isNumericLiteral(inner.getOperand());
|
|
781
|
-
};
|
|
782
|
-
/** An inline value's truthiness, which its box carries directly. */
|
|
783
|
-
const isTruthy = (boxNode) => _bamboocss_extractor.box.isLiteral(boxNode) ? Boolean(boxNode.value) : _bamboocss_extractor.box.isMap(boxNode) || _bamboocss_extractor.box.isArray(boxNode) || _bamboocss_extractor.box.isObject(boxNode);
|
|
784
|
-
/**
|
|
785
|
-
* Did the extractor *decide* this choice, or guess at it?
|
|
786
|
-
*
|
|
787
|
-
* `a ? b : c`, `a || b` and `a && b` are asked "what styles could this produce", and when
|
|
788
|
-
* one arm does not evaluate the extractor answers with the other rather than refusing
|
|
789
|
-
* (`maybe-box-node.ts`, `whenTrueValue && !whenFalseValue`). That is right for generating
|
|
790
|
-
* CSS — emit rules for whatever might be used — and wrong for rewriting source, where the
|
|
791
|
-
* arm it kept becomes the only one that runs.
|
|
792
|
-
*
|
|
793
|
-
* For a ternary the tell is the arms: it guessed exactly when one produced a box and the
|
|
794
|
-
* other did not. For a short-circuit the answer is always the left operand, so what has to
|
|
795
|
-
* be established is that the left is the side that wins.
|
|
796
|
-
*/
|
|
797
|
-
const decidedAtBuildTime = (node) => {
|
|
798
|
-
if (ts_morph.Node.isConditionalExpression(node)) return resolvesExactly(node.getWhenTrue()) && resolvesExactly(node.getWhenFalse());
|
|
799
|
-
const operator = node.getOperatorToken().getKind();
|
|
800
|
-
if (!SHORT_CIRCUIT.includes(operator)) return false;
|
|
801
|
-
if (!isWrittenHere(node.getLeft())) return false;
|
|
802
|
-
const left = rebox(node.getLeft());
|
|
803
|
-
if (!left) return false;
|
|
804
|
-
if (operator === ts_morph.SyntaxKind.BarBarToken) return isTruthy(left);
|
|
805
|
-
if (operator === ts_morph.SyntaxKind.QuestionQuestionToken) return !_bamboocss_extractor.box.isLiteral(left) || left.value != null;
|
|
806
|
-
return isTruthy(left) ? resolvesExactly(node.getRight()) : true;
|
|
807
|
-
};
|
|
808
|
-
const SHORT_CIRCUIT = [
|
|
809
|
-
ts_morph.SyntaxKind.AmpersandAmpersandToken,
|
|
810
|
-
ts_morph.SyntaxKind.BarBarToken,
|
|
811
|
-
ts_morph.SyntaxKind.QuestionQuestionToken
|
|
812
|
-
];
|
|
813
|
-
/**
|
|
814
|
-
* Every binary form the extractor collapses to one operand — the short-circuits plus the
|
|
815
|
-
* comparisons, which `isLogicalSyntax` sends down the same path even though their value is
|
|
816
|
-
* a boolean rather than either side.
|
|
817
|
-
*
|
|
818
|
-
* Hoisted rather than built per call: `accountsForSource` asks this for every property of
|
|
819
|
-
* every candidate, and rebuilding a thirteen-element list each time is not free.
|
|
820
|
-
*/
|
|
821
|
-
const COLLAPSED_BINARY = new Set([
|
|
822
|
-
...SHORT_CIRCUIT,
|
|
823
|
-
ts_morph.SyntaxKind.EqualsEqualsToken,
|
|
824
|
-
ts_morph.SyntaxKind.EqualsEqualsEqualsToken,
|
|
825
|
-
ts_morph.SyntaxKind.ExclamationEqualsToken,
|
|
826
|
-
ts_morph.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
827
|
-
ts_morph.SyntaxKind.GreaterThanToken,
|
|
828
|
-
ts_morph.SyntaxKind.GreaterThanEqualsToken,
|
|
829
|
-
ts_morph.SyntaxKind.LessThanToken,
|
|
830
|
-
ts_morph.SyntaxKind.LessThanEqualsToken,
|
|
831
|
-
ts_morph.SyntaxKind.InKeyword,
|
|
832
|
-
ts_morph.SyntaxKind.InstanceOfKeyword
|
|
833
|
-
]);
|
|
834
|
-
const isCollapsedBinary = (node) => ts_morph.Node.isBinaryExpression(node) && COLLAPSED_BINARY.has(node.getOperatorToken().getKind());
|
|
835
|
-
/**
|
|
836
|
-
* Does the extracted box account for every property the source declares?
|
|
837
|
-
*
|
|
838
|
-
* `isStaticBox` is not sufficient on its own. The extractor *omits* what it cannot
|
|
839
|
-
* evaluate rather than marking it unresolvable, so `css({ color: 'red.300', ...rest })`
|
|
840
|
-
* yields a perfectly static-looking map holding only `color`. Folding that produces
|
|
841
|
-
* `"c_red.300"` and silently drops everything `rest` contributed.
|
|
842
|
-
*
|
|
843
|
-
* So the source is the authority on what the call contains, and anything the box does
|
|
844
|
-
* not account for disqualifies the fold:
|
|
845
|
-
*
|
|
846
|
-
* - a declared property missing from the map (its value did not evaluate)
|
|
847
|
-
* - a computed key, which we cannot match against the map by name
|
|
848
|
-
* - a spread, unless it is an inline object literal
|
|
849
|
-
*
|
|
850
|
-
* Spreads are the conservative case. `{ ...base }` where `base` is a static local
|
|
851
|
-
* object *is* resolved by the extractor, but a resolved spread and an unresolved one
|
|
852
|
-
* are indistinguishable once flattened into the map — both just contribute keys, or
|
|
853
|
-
* fail to. Rather than guess and erase evaluation the compiler cannot reproduce, the call
|
|
854
|
-
* is rejected.
|
|
855
|
-
*/
|
|
856
|
-
/**
|
|
857
|
-
* What a property's value is written as. A shorthand names it, so the name *is* the
|
|
858
|
-
* expression — reading an initializer that is not there reports the property as having no
|
|
859
|
-
* source, and everything hidden behind the name goes unchecked.
|
|
860
|
-
*/
|
|
861
|
-
const valueOf = (property) => ts_morph.Node.isPropertyAssignment(property) ? property.getInitializer() : ts_morph.Node.isShorthandPropertyAssignment(property) ? property.getNameNode() : void 0;
|
|
862
|
-
const accountsForSource = (node, boxNode) => {
|
|
863
|
-
if (!node) return true;
|
|
864
|
-
const unwrapped = unwrapExpression(node);
|
|
865
|
-
if ((ts_morph.Node.isConditionalExpression(unwrapped) || isCollapsedBinary(unwrapped)) && !_bamboocss_extractor.box.isConditional(boxNode) && !decidedAtBuildTime(unwrapped)) return false;
|
|
866
|
-
if (ts_morph.Node.isArrayLiteralExpression(unwrapped)) {
|
|
867
|
-
if (!_bamboocss_extractor.box.isArray(boxNode)) return false;
|
|
868
|
-
const elements = unwrapped.getElements();
|
|
869
|
-
if (elements.length !== boxNode.value.length) return false;
|
|
870
|
-
return elements.every((element, index) => accountsForSource(element, boxNode.value[index]));
|
|
871
|
-
}
|
|
872
|
-
if (!ts_morph.Node.isObjectLiteralExpression(unwrapped)) {
|
|
873
|
-
const origin = _bamboocss_extractor.box.isMap(boxNode) ? boxNode.getNode() : void 0;
|
|
874
|
-
return !origin || origin === unwrapped || !ts_morph.Node.isObjectLiteralExpression(origin) ? true : accountsForSource(origin, boxNode);
|
|
875
|
-
}
|
|
876
|
-
if (!_bamboocss_extractor.box.isMap(boxNode)) return false;
|
|
877
|
-
for (const property of unwrapped.getProperties()) {
|
|
878
|
-
if (ts_morph.Node.isSpreadAssignment(property)) {
|
|
879
|
-
const expression = unwrapExpression(property.getExpression());
|
|
880
|
-
if (ts_morph.Node.isObjectLiteralExpression(expression)) continue;
|
|
881
|
-
const walked = boxNode.resolvedSpreads?.find((entry) => entry.node === expression);
|
|
882
|
-
if (!walked) return false;
|
|
883
|
-
if (!accountsForSource(walked.box.getNode(), walked.box)) return false;
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
if (ts_morph.Node.isMethodDeclaration(property) || ts_morph.Node.isGetAccessorDeclaration(property) || ts_morph.Node.isSetAccessorDeclaration(property)) return false;
|
|
887
|
-
if (!ts_morph.Node.isPropertyAssignment(property) && !ts_morph.Node.isShorthandPropertyAssignment(property)) return false;
|
|
888
|
-
const nameNode = property.getNameNode();
|
|
889
|
-
if (ts_morph.Node.isComputedPropertyName(nameNode)) return false;
|
|
890
|
-
const key = ts_morph.Node.isStringLiteral(nameNode) || ts_morph.Node.isNumericLiteral(nameNode) ? String(nameNode.getLiteralValue()) : nameNode.getText();
|
|
891
|
-
const value = valueOf(property);
|
|
892
|
-
if (value && ts_morph.Node.isIdentifier(value) && value.getText() === "undefined") continue;
|
|
893
|
-
if (!boxNode.value.has(key)) return false;
|
|
894
|
-
if (!accountsForSource(value, boxNode.value.get(key))) return false;
|
|
895
|
-
}
|
|
896
|
-
return true;
|
|
897
|
-
};
|
|
898
|
-
/**
|
|
899
|
-
* Memo keyed on a file, thrown away when its text is replaced.
|
|
900
|
-
*
|
|
901
|
-
* A plain `WeakMap<SourceFile, …>` is wrong here: ts-morph reuses the wrapper when a path
|
|
902
|
-
* is re-added with new text — which is what a watch rebuild does — so it would answer for
|
|
903
|
-
* the previous revision. Comparing against the text it was computed from costs a
|
|
904
|
-
* reference check while the file is unchanged, since `getFullText()` hands back the same
|
|
905
|
-
* string instance.
|
|
906
|
-
*/
|
|
907
|
-
const byText = (cache, sourceFile, compute) => {
|
|
908
|
-
const text = sourceFile.getFullText();
|
|
909
|
-
const hit = cache.get(sourceFile);
|
|
910
|
-
if (hit && hit.text === text) return hit.value;
|
|
911
|
-
const value = compute();
|
|
912
|
-
cache.set(sourceFile, {
|
|
913
|
-
text,
|
|
914
|
-
value
|
|
915
|
-
});
|
|
916
|
-
return value;
|
|
917
|
-
};
|
|
918
|
-
/**
|
|
919
|
-
* Every name declared at module scope, which is what an added import could collide with.
|
|
920
|
-
*
|
|
921
|
-
* This replaced `sourceFile.getLocals()`. That was precise, but it goes through the
|
|
922
|
-
* compiler's symbol table, and reaching for it binds the program — including every
|
|
923
|
-
* `.d.ts` the module's imports pull in. It cost ~8ms on a ten-line file and grew with the
|
|
924
|
-
* project, which was invisible while only a partial split reached it and became the
|
|
925
|
-
* dominant cost once open-ended values started lowering too.
|
|
926
|
-
*
|
|
927
|
-
* A syntactic walk answers the same question: a binding in a nested *function* cannot
|
|
928
|
-
* collide with a module-scope import, and one that shadows it *at the call site* is what
|
|
929
|
-
* `isShadowed` is for. Memoized against the file's text rather than the file, since
|
|
930
|
-
* ts-morph reuses the wrapper across a re-add and a plain `WeakMap` would answer for the
|
|
931
|
-
* previous revision. Uncached it is re-walked per candidate, which is quadratic in a
|
|
932
|
-
* module of many elements.
|
|
933
|
-
*/
|
|
934
|
-
const moduleScopeCache = /* @__PURE__ */ new WeakMap();
|
|
935
|
-
const declaredAtModuleScope = (sourceFile) => byText(moduleScopeCache, sourceFile, () => collectModuleScopeNames(sourceFile));
|
|
936
|
-
const collectModuleScopeNames = (sourceFile) => {
|
|
937
|
-
const names = /* @__PURE__ */ new Set();
|
|
938
|
-
const addBinding = (node) => {
|
|
939
|
-
if (!node) return;
|
|
940
|
-
if (ts_morph.Node.isObjectBindingPattern(node) || ts_morph.Node.isArrayBindingPattern(node)) {
|
|
941
|
-
for (const element of node.getElements()) if (ts_morph.Node.isBindingElement(element)) addBinding(element.getNameNode());
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
if (ts_morph.Node.isIdentifier(node)) names.add(node.getText());
|
|
945
|
-
};
|
|
946
|
-
const addDeclarations = (list) => {
|
|
947
|
-
if (ts_morph.Node.isVariableStatement(list)) {
|
|
948
|
-
for (const declaration of list.getDeclarations()) addBinding(declaration.getNameNode());
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
if (ts_morph.Node.isVariableDeclarationList(list)) for (const declaration of list.getDeclarations()) addBinding(declaration.getNameNode());
|
|
952
|
-
};
|
|
953
|
-
const isVar = (node) => (ts_morph.Node.isVariableStatement(node) || ts_morph.Node.isVariableDeclarationList(node)) && node.getDeclarationKind() === ts_morph.VariableDeclarationKind.Var;
|
|
954
|
-
/**
|
|
955
|
-
* `var` is scoped to the enclosing *function*, not the enclosing block, so one written
|
|
956
|
-
* inside any statement at the top level still binds at module scope. Walking only the
|
|
957
|
-
* top-level statements missed every one of them, and each emitted a duplicate binding.
|
|
958
|
-
*
|
|
959
|
-
* Only statement containers are followed. A function or class body opens a new variable
|
|
960
|
-
* scope, so a `var` inside one cannot collide with a module-level import.
|
|
961
|
-
*/
|
|
962
|
-
const addHoistedVars = (node) => {
|
|
963
|
-
if (isVar(node)) {
|
|
964
|
-
addDeclarations(node);
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
if (ts_morph.Node.isBlock(node)) {
|
|
968
|
-
for (const statement of node.getStatements()) addHoistedVars(statement);
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
if (ts_morph.Node.isIfStatement(node)) {
|
|
972
|
-
addHoistedVars(node.getThenStatement());
|
|
973
|
-
const otherwise = node.getElseStatement();
|
|
974
|
-
if (otherwise) addHoistedVars(otherwise);
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
if (ts_morph.Node.isForStatement(node)) {
|
|
978
|
-
const initializer = node.getInitializer();
|
|
979
|
-
if (initializer) addHoistedVars(initializer);
|
|
980
|
-
addHoistedVars(node.getStatement());
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
if (ts_morph.Node.isForInStatement(node) || ts_morph.Node.isForOfStatement(node)) {
|
|
984
|
-
addHoistedVars(node.getInitializer());
|
|
985
|
-
addHoistedVars(node.getStatement());
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
if (ts_morph.Node.isWhileStatement(node) || ts_morph.Node.isDoStatement(node) || ts_morph.Node.isWithStatement(node)) {
|
|
989
|
-
addHoistedVars(node.getStatement());
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
if (ts_morph.Node.isLabeledStatement(node)) {
|
|
993
|
-
addHoistedVars(node.getStatement());
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
if (ts_morph.Node.isTryStatement(node)) {
|
|
997
|
-
addHoistedVars(node.getTryBlock());
|
|
998
|
-
const caught = node.getCatchClause();
|
|
999
|
-
if (caught) addHoistedVars(caught.getBlock());
|
|
1000
|
-
const finally_ = node.getFinallyBlock();
|
|
1001
|
-
if (finally_) addHoistedVars(finally_);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
if (ts_morph.Node.isSwitchStatement(node)) for (const clause of node.getCaseBlock().getClauses()) for (const statement of clause.getStatements()) addHoistedVars(statement);
|
|
1005
|
-
};
|
|
1006
|
-
for (const statement of sourceFile.getStatements()) {
|
|
1007
|
-
if (ts_morph.Node.isVariableStatement(statement)) {
|
|
1008
|
-
addDeclarations(statement);
|
|
1009
|
-
continue;
|
|
1010
|
-
}
|
|
1011
|
-
if (ts_morph.Node.isImportDeclaration(statement)) {
|
|
1012
|
-
addBinding(statement.getDefaultImport());
|
|
1013
|
-
addBinding(statement.getNamespaceImport());
|
|
1014
|
-
for (const named of statement.getNamedImports()) addBinding(named.getAliasNode() ?? named.getNameNode());
|
|
1015
|
-
continue;
|
|
1016
|
-
}
|
|
1017
|
-
if (ts_morph.Node.isImportEqualsDeclaration(statement)) {
|
|
1018
|
-
addBinding(statement.getNameNode());
|
|
1019
|
-
continue;
|
|
1020
|
-
}
|
|
1021
|
-
if (ts_morph.Node.isFunctionDeclaration(statement) || ts_morph.Node.isClassDeclaration(statement) || ts_morph.Node.isEnumDeclaration(statement) || ts_morph.Node.isModuleDeclaration(statement) || ts_morph.Node.isTypeAliasDeclaration(statement) || ts_morph.Node.isInterfaceDeclaration(statement)) {
|
|
1022
|
-
addBinding(statement.getNameNode());
|
|
1023
|
-
continue;
|
|
1024
|
-
}
|
|
1025
|
-
addHoistedVars(statement);
|
|
1026
|
-
}
|
|
1027
|
-
return names;
|
|
1028
|
-
};
|
|
1029
|
-
/**
|
|
1030
|
-
* Every identifier in the module, grouped by the name it spells.
|
|
1031
|
-
*
|
|
1032
|
-
* Built once per pass and handed to each lookup, because walking the whole tree per binding
|
|
1033
|
-
* made that O(bindings x identifiers): a module declaring ten recipes walked its identifiers
|
|
1034
|
-
* ten times.
|
|
1035
|
-
*
|
|
1036
|
-
* ## Why this is a raw walk rather than `getDescendantsOfKind`
|
|
1037
|
-
*
|
|
1038
|
-
* `SyntaxKind.Identifier` sorts *below* `SyntaxKind.FirstNode`, which is what ts-morph tests to
|
|
1039
|
-
* decide whether it may search the parse tree. For a kind below that line it falls back to
|
|
1040
|
-
* materialising the whole **token** tree — every brace, comma and keyword becomes a ts-morph node
|
|
1041
|
-
* on the way to collecting the identifiers. On 55 KB of real tsx that measured 22ms against
|
|
1042
|
-
* 0.22ms for the same collection over compiler nodes, and the node cache does not help: a second
|
|
1043
|
-
* call cost the same 22ms.
|
|
1044
|
-
*
|
|
1045
|
-
* Wrapping is what costs, so only the buckets a caller actually reads are wrapped, on the first
|
|
1046
|
-
* read and cached after. Nothing enumerates this index — both callers ask for one name — so the
|
|
1047
|
-
* rest is never built. `_getNodeFromCompilerNode` is ts-morph's own memoized wrapper factory, so
|
|
1048
|
-
* a node handed back here is the very object `getDescendantsOfKind` would have returned, which
|
|
1049
|
-
* `localReferencesTo` depends on: it compares against the declaration by identity.
|
|
1050
|
-
*
|
|
1051
|
-
* JSDoc is walked explicitly. `ts.forEachChild` does not descend into it, while the token path
|
|
1052
|
-
* this replaces does, so a name mentioned only in a `@type` annotation was previously found and
|
|
1053
|
-
* would otherwise stop being — a silent narrowing of what counts as a surviving reference.
|
|
1054
|
-
* Keyed on `escapedText` because that is what ts-morph's `Identifier.getText()` returns: the name
|
|
1055
|
-
* as the compiler resolves it, so `\u0062adge` and `badge` share a bucket exactly as before.
|
|
1056
|
-
*
|
|
1057
|
-
* Deliberately *not* memoized across passes, unlike the module-scope names beside it. That
|
|
1058
|
-
* cache holds strings, which outlive anything; this one holds nodes, and a node does not
|
|
1059
|
-
* survive its source file being replaced — `addSourceFile` overwrites, which forgets every
|
|
1060
|
-
* node previously taken from it. Keying on the source text does not help, because identical
|
|
1061
|
-
* text re-parsed is a fresh tree: the cache hits and returns nodes that throw
|
|
1062
|
-
* `Attempted to get information from a node that was removed or forgotten` on the next read.
|
|
1063
|
-
*/
|
|
1064
|
-
const identifierIndex = (sourceFile) => {
|
|
1065
|
-
const compilerNodes = /* @__PURE__ */ new Map();
|
|
1066
|
-
const collect = (node) => {
|
|
1067
|
-
if (node.kind === ts_morph.ts.SyntaxKind.Identifier) {
|
|
1068
|
-
const name = String(node.escapedText);
|
|
1069
|
-
const known = compilerNodes.get(name);
|
|
1070
|
-
if (known) known.push(node);
|
|
1071
|
-
else compilerNodes.set(name, [node]);
|
|
1072
|
-
}
|
|
1073
|
-
const jsDoc = node.jsDoc;
|
|
1074
|
-
if (jsDoc) for (const doc of jsDoc) collect(doc);
|
|
1075
|
-
ts_morph.ts.forEachChild(node, collect);
|
|
1076
|
-
};
|
|
1077
|
-
ts_morph.ts.forEachChild(sourceFile.compilerNode, collect);
|
|
1078
|
-
const wrapped = /* @__PURE__ */ new Map();
|
|
1079
|
-
const wrap = sourceFile;
|
|
1080
|
-
return { get: (name) => {
|
|
1081
|
-
const known = wrapped.get(name);
|
|
1082
|
-
if (known) return known;
|
|
1083
|
-
const nodes = (compilerNodes.get(name) ?? []).map((node) => wrap._getNodeFromCompilerNode(node));
|
|
1084
|
-
wrapped.set(name, nodes);
|
|
1085
|
-
return nodes;
|
|
1086
|
-
} };
|
|
1087
|
-
};
|
|
1088
|
-
const localReferencesTo = (index, name, declaration) => {
|
|
1089
|
-
const references = [];
|
|
1090
|
-
for (const identifier of index.get(name)) {
|
|
1091
|
-
if (identifier === declaration) continue;
|
|
1092
|
-
const parent = identifier.getParent();
|
|
1093
|
-
if (!parent) continue;
|
|
1094
|
-
if (ts_morph.Node.isPropertyAccessExpression(parent) && parent.getNameNode() === identifier) continue;
|
|
1095
|
-
if (ts_morph.Node.isPropertyAssignment(parent) && parent.getNameNode() === identifier) continue;
|
|
1096
|
-
if (ts_morph.Node.isBindingElement(parent) && parent.getPropertyNameNode() === identifier) continue;
|
|
1097
|
-
if ((ts_morph.Node.isJsxOpeningElement(parent) || ts_morph.Node.isJsxSelfClosingElement(parent) || ts_morph.Node.isJsxClosingElement(parent)) && parent.getTagNameNode() === identifier) continue;
|
|
1098
|
-
if (ts_morph.Node.isJsxAttribute(parent) && parent.getNameNode() === identifier) continue;
|
|
1099
|
-
if ((ts_morph.Node.isMethodDeclaration(parent) || ts_morph.Node.isPropertyDeclaration(parent) || ts_morph.Node.isGetAccessorDeclaration(parent) || ts_morph.Node.isSetAccessorDeclaration(parent) || ts_morph.Node.isMethodSignature(parent) || ts_morph.Node.isPropertySignature(parent) || ts_morph.Node.isEnumMember(parent)) && parent.getNameNode() === identifier) continue;
|
|
1100
|
-
if ((ts_morph.Node.isVariableDeclaration(parent) || ts_morph.Node.isParameterDeclaration(parent) || ts_morph.Node.isFunctionDeclaration(parent) || ts_morph.Node.isClassDeclaration(parent) || ts_morph.Node.isBindingElement(parent)) && parent.getNameNode() === identifier) continue;
|
|
1101
|
-
if (ts_morph.Node.isImportSpecifier(parent) || ts_morph.Node.isExportSpecifier(parent)) {
|
|
1102
|
-
if (parent.getNameNode() !== identifier) continue;
|
|
1103
|
-
if (ts_morph.Node.isExportSpecifier(parent)) {
|
|
1104
|
-
references.push(identifier);
|
|
1105
|
-
continue;
|
|
1106
|
-
}
|
|
1107
|
-
continue;
|
|
1108
|
-
}
|
|
1109
|
-
references.push(identifier);
|
|
1110
|
-
}
|
|
1111
|
-
return references;
|
|
1112
|
-
};
|
|
1113
|
-
//#endregion
|
|
1114
|
-
//#region src/fold-recipe.ts
|
|
1115
|
-
/**
|
|
1116
|
-
* Binding name → the config it was declared with.
|
|
1117
|
-
*
|
|
1118
|
-
* Built from the definitions the parser already recorded, walking each one to the declaration
|
|
1119
|
-
* that names it. The parser records a definition under the name it was *imported* as (`cva`),
|
|
1120
|
-
* and a call under the name the file *bound* (`badge`); this is what joins the two.
|
|
1121
|
-
*
|
|
1122
|
-
* Slot and ordinary recipes share one representation.
|
|
1123
|
-
*/
|
|
1124
|
-
const collectRecipeConfigs = (parserResult) => {
|
|
1125
|
-
const configs = /* @__PURE__ */ new Map();
|
|
1126
|
-
const definitions = [...parserResult.cva, ...parserResult.sva];
|
|
1127
|
-
for (const definition of definitions) {
|
|
1128
|
-
const node = definition.box?.getNode?.();
|
|
1129
|
-
if (!node) continue;
|
|
1130
|
-
const nameNode = ((ts_morph.Node.isCallExpression(node) ? node : node.getFirstAncestorByKind(ts_morph.SyntaxKind.CallExpression))?.getFirstAncestorByKind(ts_morph.SyntaxKind.VariableDeclaration))?.getNameNode();
|
|
1131
|
-
if (!nameNode || !ts_morph.Node.isIdentifier(nameNode)) continue;
|
|
1132
|
-
if (definition.data?.length !== 1) {
|
|
1133
|
-
configs.set(nameNode.getText(), AMBIGUOUS);
|
|
1134
|
-
continue;
|
|
1135
|
-
}
|
|
1136
|
-
const config = definition.data[0];
|
|
1137
|
-
if (!config || typeof config !== "object") continue;
|
|
1138
|
-
if (configs.has(nameNode.getText())) {
|
|
1139
|
-
configs.set(nameNode.getText(), AMBIGUOUS);
|
|
1140
|
-
continue;
|
|
1141
|
-
}
|
|
1142
|
-
configs.set(nameNode.getText(), {
|
|
1143
|
-
config,
|
|
1144
|
-
box: definition.box
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
return configs;
|
|
1148
|
-
};
|
|
1149
|
-
/** Pick a complete precompiled StyleSet for one or more runtime recipe axes. */
|
|
1150
|
-
const RECIPE_MAP_HELPER = "cvaMap";
|
|
1151
|
-
/** Guard the exact compiler against accidentally materialising an enormous Cartesian product. */
|
|
1152
|
-
const DEFAULT_MAX_RECIPE_STATES = 65536;
|
|
1153
|
-
/** What `recipe.splitVariantProps` calls, reached directly once the call is lowered. */
|
|
1154
|
-
const SPLIT_PROPS_HELPER = "splitProps";
|
|
1155
|
-
/** Marker for a binding the fold must never resolve — declared twice, or unresolvable. */
|
|
1156
|
-
const AMBIGUOUS = Object.freeze({
|
|
1157
|
-
config: {},
|
|
1158
|
-
box: void 0
|
|
1159
|
-
});
|
|
1160
|
-
/** `.size`, or `["x-large"]` when the variant is not a valid identifier. */
|
|
1161
|
-
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
1162
|
-
const propertyAccess = (key) => IDENTIFIER.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
|
|
1163
|
-
const LITERAL_KINDS = new Set([
|
|
1164
|
-
ts_morph.SyntaxKind.StringLiteral,
|
|
1165
|
-
ts_morph.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
1166
|
-
ts_morph.SyntaxKind.NumericLiteral,
|
|
1167
|
-
ts_morph.SyntaxKind.TrueKeyword,
|
|
1168
|
-
ts_morph.SyntaxKind.FalseKeyword
|
|
1169
|
-
]);
|
|
1170
|
-
/**
|
|
1171
|
-
* The value a literal node denotes, or `undefined` for anything else.
|
|
1172
|
-
*
|
|
1173
|
-
* Read off the node rather than from the extractor's resolved data, because that data is lossy
|
|
1174
|
-
* in the direction that matters: a property it could not resolve is *dropped*, so `badge({ tone })`
|
|
1175
|
-
* and `badge({})` are identical there. Folding the first as if it were the second emits a class
|
|
1176
|
-
* string missing the variant — the element renders, wrongly, with no report.
|
|
1177
|
-
*/
|
|
1178
|
-
const literalValue = (node) => {
|
|
1179
|
-
if (!node || !LITERAL_KINDS.has(node.getKind())) return void 0;
|
|
1180
|
-
if (ts_morph.Node.isStringLiteral(node) || ts_morph.Node.isNoSubstitutionTemplateLiteral(node)) return node.getLiteralValue();
|
|
1181
|
-
if (ts_morph.Node.isNumericLiteral(node)) return node.getLiteralValue();
|
|
1182
|
-
if (node.getKind() === ts_morph.SyntaxKind.TrueKeyword) return true;
|
|
1183
|
-
if (node.getKind() === ts_morph.SyntaxKind.FalseKeyword) return false;
|
|
1184
|
-
};
|
|
1185
|
-
/**
|
|
1186
|
-
* The property name a key node denotes.
|
|
1187
|
-
*
|
|
1188
|
-
* Read off the node rather than unquoted from its text. `{ '\\u0074one': 'a' }` names the
|
|
1189
|
-
* variant `tone`, and stripping the surrounding quotes leaves the escape uninterpreted — so
|
|
1190
|
-
* the variant did not match, its class was dropped, and the element rendered without it. A
|
|
1191
|
-
* numeric key normalises the same way: `{ 0x10: 'a' }` is the key `16`.
|
|
1192
|
-
*/
|
|
1193
|
-
const propertyKey = (nameNode) => {
|
|
1194
|
-
if (ts_morph.Node.isIdentifier(nameNode)) return nameNode.getText();
|
|
1195
|
-
if (ts_morph.Node.isStringLiteral(nameNode) || ts_morph.Node.isNoSubstitutionTemplateLiteral(nameNode)) return nameNode.getLiteralValue();
|
|
1196
|
-
if (ts_morph.Node.isNumericLiteral(nameNode)) return String(nameNode.getLiteralValue());
|
|
1197
|
-
};
|
|
1198
|
-
/**
|
|
1199
|
-
* Make a generated compile helper callable at this call site, by whatever name the file gives it.
|
|
1200
|
-
*
|
|
1201
|
-
* Unlike `cx`, an inline recipe's callee is a local binding, so
|
|
1202
|
-
* there is nothing to match — the host here is any import of the generated css module, which
|
|
1203
|
-
* a file defining a recipe necessarily has, since `cva` came from it.
|
|
1204
|
-
*/
|
|
1205
|
-
const ensureRecipeHelperImport = (imported, call, isBambooCssModule, isGeneratedCssModule, isShadowed, newImportModule, helperModuleFromSubpath) => {
|
|
1206
|
-
const sourceFile = call.getSourceFile();
|
|
1207
|
-
let host;
|
|
1208
|
-
let subpathModule;
|
|
1209
|
-
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
1210
|
-
const mod = declaration.getModuleSpecifierValue();
|
|
1211
|
-
if (declaration.isTypeOnly()) continue;
|
|
1212
|
-
for (const named of declaration.getNamedImports()) {
|
|
1213
|
-
if (named.isTypeOnly()) continue;
|
|
1214
|
-
if (named.getNameNode().getText() === imported) {
|
|
1215
|
-
if (!isBambooCssModule(mod)) return void 0;
|
|
1216
|
-
const local = (named.getAliasNode() ?? named.getNameNode()).getText();
|
|
1217
|
-
return isShadowed(call, local) ? void 0 : { name: local };
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
if (!host && isGeneratedCssModule(mod) && declaration.getNamedImports().length > 0) host = declaration;
|
|
1221
|
-
if (!subpathModule) subpathModule = helperModuleFromSubpath?.(mod);
|
|
1222
|
-
}
|
|
1223
|
-
const fallbackModule = newImportModule ?? subpathModule;
|
|
1224
|
-
if (!host && !fallbackModule) return void 0;
|
|
1225
|
-
if (declaredAtModuleScope(sourceFile).has(imported)) return void 0;
|
|
1226
|
-
if (isShadowed(call, imported)) return void 0;
|
|
1227
|
-
if (!host) {
|
|
1228
|
-
const anchor = sourceFile.getImportDeclarations().at(-1);
|
|
1229
|
-
if (!anchor) return void 0;
|
|
1230
|
-
return {
|
|
1231
|
-
name: imported,
|
|
1232
|
-
insert: {
|
|
1233
|
-
pos: anchor.getEnd(),
|
|
1234
|
-
names: [imported],
|
|
1235
|
-
module: fallbackModule
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
const last = host.getNamedImports().at(-1);
|
|
1240
|
-
if (!last) return void 0;
|
|
1241
|
-
return {
|
|
1242
|
-
name: imported,
|
|
1243
|
-
insert: {
|
|
1244
|
-
pos: last.getEnd(),
|
|
1245
|
-
names: [imported]
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
};
|
|
1249
|
-
/**
|
|
1250
|
-
* Lower one invocation, or say why not.
|
|
1251
|
-
*
|
|
1252
|
-
* Every property written at the call site has to be a literal. A selection is not additive —
|
|
1253
|
-
* an unresolved variant does not merely omit a class, it can change which of several the
|
|
1254
|
-
* recipe applies — so a partially-known selection is not foldable at all.
|
|
1255
|
-
*/
|
|
1256
|
-
const lowerRecipeCall = (call, entry, styleCompiler, isInert, resolvedSelection, slot, maxRecipeStates = DEFAULT_MAX_RECIPE_STATES) => {
|
|
1257
|
-
if (!entry || entry === AMBIGUOUS) return {
|
|
1258
|
-
kind: "decline",
|
|
1259
|
-
reason: "unknown-recipe"
|
|
1260
|
-
};
|
|
1261
|
-
const { config } = entry;
|
|
1262
|
-
if (config.slots !== void 0) {
|
|
1263
|
-
if (!Array.isArray(config.slots) || slot !== void 0 && !config.slots.includes(slot)) return {
|
|
1264
|
-
kind: "decline",
|
|
1265
|
-
reason: "unsupported-shape"
|
|
1266
|
-
};
|
|
1267
|
-
} else if (slot) return {
|
|
1268
|
-
kind: "decline",
|
|
1269
|
-
reason: "unsupported-shape"
|
|
1270
|
-
};
|
|
1271
|
-
if (!config.base && !config.variants && !config.className) return {
|
|
1272
|
-
kind: "decline",
|
|
1273
|
-
reason: "unknown-recipe"
|
|
1274
|
-
};
|
|
1275
|
-
if (!ts_morph.Node.isCallExpression(call)) return {
|
|
1276
|
-
kind: "decline",
|
|
1277
|
-
reason: "unsupported-shape"
|
|
1278
|
-
};
|
|
1279
|
-
const args = call.getArguments();
|
|
1280
|
-
if (args.length > 1) return {
|
|
1281
|
-
kind: "decline",
|
|
1282
|
-
reason: "unsupported-shape"
|
|
1283
|
-
};
|
|
1284
|
-
const selection = {};
|
|
1285
|
-
/** Variant → the source expression selecting it, for axes that stay runtime decisions. */
|
|
1286
|
-
const dynamicAxes = /* @__PURE__ */ new Map();
|
|
1287
|
-
/**
|
|
1288
|
-
* Variants whose expression could run something, in the order the source evaluates them.
|
|
1289
|
-
*
|
|
1290
|
-
* The text is kept, not just the key: a later property writing the same key replaces the
|
|
1291
|
-
* entry in `dynamicAxes`, and the expression recorded here would then never be emitted.
|
|
1292
|
-
*/
|
|
1293
|
-
const effectful = [];
|
|
1294
|
-
if (args.length === 1) {
|
|
1295
|
-
const arg = args[0];
|
|
1296
|
-
if (!arg) return {
|
|
1297
|
-
kind: "decline",
|
|
1298
|
-
reason: "dynamic"
|
|
1299
|
-
};
|
|
1300
|
-
/**
|
|
1301
|
-
* `input(variantProps)` — a selection the build cannot see inside.
|
|
1302
|
-
*
|
|
1303
|
-
* The compiled recipe contract accepts scalar declared variant values. A conditional
|
|
1304
|
-
* object is not a finite selection value; responsiveness belongs inside a variant's style
|
|
1305
|
-
* declaration, where the compiler can materialize its conditions ahead of time.
|
|
1306
|
-
*
|
|
1307
|
-
* The complete StyleSets are knowable: the config declares every scalar value each axis
|
|
1308
|
-
* accepts. This is the shape a wrapper component takes, where variants are its public API
|
|
1309
|
-
* and therefore cannot be literals by definition.
|
|
1310
|
-
*
|
|
1311
|
-
* An identifier only. Each variant reads the binding again, and re-reading anything else —
|
|
1312
|
-
* a call, a property access — would evaluate it once per axis instead of once.
|
|
1313
|
-
*/
|
|
1314
|
-
if (ts_morph.Node.isIdentifier(arg)) {
|
|
1315
|
-
const binding = arg.getText();
|
|
1316
|
-
for (const key of Object.keys(config.variants ?? {})) dynamicAxes.set(key, `${binding}${propertyAccess(key)}`);
|
|
1317
|
-
} else if (!ts_morph.Node.isObjectLiteralExpression(arg)) return {
|
|
1318
|
-
kind: "decline",
|
|
1319
|
-
reason: "dynamic"
|
|
1320
|
-
};
|
|
1321
|
-
else for (const property of arg.getProperties()) {
|
|
1322
|
-
if (ts_morph.Node.isSpreadAssignment(property)) return {
|
|
1323
|
-
kind: "decline",
|
|
1324
|
-
reason: "dynamic"
|
|
1325
|
-
};
|
|
1326
|
-
if (ts_morph.Node.isShorthandPropertyAssignment(property)) {
|
|
1327
|
-
dynamicAxes.set(property.getName(), property.getName());
|
|
1328
|
-
delete selection[property.getName()];
|
|
1329
|
-
continue;
|
|
1330
|
-
}
|
|
1331
|
-
if (!ts_morph.Node.isPropertyAssignment(property)) return {
|
|
1332
|
-
kind: "decline",
|
|
1333
|
-
reason: "dynamic"
|
|
1334
|
-
};
|
|
1335
|
-
const nameNode = property.getNameNode();
|
|
1336
|
-
if (ts_morph.Node.isComputedPropertyName(nameNode)) return {
|
|
1337
|
-
kind: "decline",
|
|
1338
|
-
reason: "dynamic"
|
|
1339
|
-
};
|
|
1340
|
-
const key = propertyKey(nameNode);
|
|
1341
|
-
if (key === void 0) return {
|
|
1342
|
-
kind: "decline",
|
|
1343
|
-
reason: "dynamic"
|
|
1344
|
-
};
|
|
1345
|
-
const initializer = property.getInitializer();
|
|
1346
|
-
if (initializer && !isInert(initializer)) {
|
|
1347
|
-
if (!Object.hasOwn(config.variants ?? {}, key)) return {
|
|
1348
|
-
kind: "decline",
|
|
1349
|
-
reason: "dynamic"
|
|
1350
|
-
};
|
|
1351
|
-
effectful.push({
|
|
1352
|
-
key,
|
|
1353
|
-
text: initializer.getText()
|
|
1354
|
-
});
|
|
1355
|
-
dynamicAxes.set(key, initializer.getText());
|
|
1356
|
-
delete selection[key];
|
|
1357
|
-
continue;
|
|
1358
|
-
}
|
|
1359
|
-
const literal = literalValue(initializer);
|
|
1360
|
-
if (literal !== void 0) {
|
|
1361
|
-
selection[key] = literal;
|
|
1362
|
-
dynamicAxes.delete(key);
|
|
1363
|
-
continue;
|
|
1364
|
-
}
|
|
1365
|
-
if (!resolvedSelection || !Object.hasOwn(resolvedSelection, key)) {
|
|
1366
|
-
if (!initializer) return {
|
|
1367
|
-
kind: "decline",
|
|
1368
|
-
reason: "dynamic"
|
|
1369
|
-
};
|
|
1370
|
-
dynamicAxes.set(key, initializer.getText());
|
|
1371
|
-
delete selection[key];
|
|
1372
|
-
continue;
|
|
1373
|
-
}
|
|
1374
|
-
const value = resolvedSelection[key];
|
|
1375
|
-
if (value !== null && typeof value === "object") return {
|
|
1376
|
-
kind: "decline",
|
|
1377
|
-
reason: "dynamic"
|
|
1378
|
-
};
|
|
1379
|
-
selection[key] = value;
|
|
1380
|
-
dynamicAxes.delete(key);
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
/**
|
|
1384
|
-
* Every expression that could run something has to reach the output carrying its own text.
|
|
1385
|
-
*
|
|
1386
|
-
* A later property writing the same key replaces it in `dynamicAxes` — `badge({ tone: a(),
|
|
1387
|
-
* tone: 'b' })` is last-wins for the *value*, but `a()` still runs, and emitting only the
|
|
1388
|
-
* literal would delete it. Duplicate keys are a type error in TypeScript; the fold does not
|
|
1389
|
-
* typecheck and does transform `.js`, so this is reachable.
|
|
1390
|
-
*/
|
|
1391
|
-
const everyEffectSurvives = () => effectful.every(({ key, text }) => dynamicAxes.get(key) === text);
|
|
1392
|
-
const compiledSelection = (selected) => {
|
|
1393
|
-
if (Array.isArray(config.slots) && slot === void 0) {
|
|
1394
|
-
const slots = {};
|
|
1395
|
-
const classNames = /* @__PURE__ */ new Set();
|
|
1396
|
-
for (const slotName of config.slots) {
|
|
1397
|
-
const styles = styleCompiler.resolveRecipe(config, selected, slotName);
|
|
1398
|
-
if (!styles) return void 0;
|
|
1399
|
-
const className = styleCompiler.className(styles);
|
|
1400
|
-
slots[slotName] = className;
|
|
1401
|
-
for (const token of className.split(" ")) if (token) classNames.add(token);
|
|
1402
|
-
}
|
|
1403
|
-
return {
|
|
1404
|
-
value: slots,
|
|
1405
|
-
classNames: [...classNames]
|
|
1406
|
-
};
|
|
1407
|
-
}
|
|
1408
|
-
const styles = styleCompiler.resolveRecipe(config, selected, slot);
|
|
1409
|
-
if (!styles) return void 0;
|
|
1410
|
-
const className = styleCompiler.className(styles);
|
|
1411
|
-
return {
|
|
1412
|
-
value: className,
|
|
1413
|
-
classNames: className.split(" ").filter(Boolean),
|
|
1414
|
-
styles
|
|
1415
|
-
};
|
|
1416
|
-
};
|
|
1417
|
-
if (dynamicAxes.size === 0) {
|
|
1418
|
-
if (!everyEffectSurvives()) return {
|
|
1419
|
-
kind: "decline",
|
|
1420
|
-
reason: "dynamic"
|
|
1421
|
-
};
|
|
1422
|
-
const compiled = compiledSelection(selection);
|
|
1423
|
-
if (!compiled) return {
|
|
1424
|
-
kind: "decline",
|
|
1425
|
-
reason: "dynamic"
|
|
1426
|
-
};
|
|
1427
|
-
if (typeof compiled.value === "string") return {
|
|
1428
|
-
kind: "class",
|
|
1429
|
-
className: compiled.value,
|
|
1430
|
-
styles: compiled.styles
|
|
1431
|
-
};
|
|
1432
|
-
return {
|
|
1433
|
-
kind: "slots",
|
|
1434
|
-
expression: JSON.stringify(compiled.value),
|
|
1435
|
-
classNames: compiled.classNames,
|
|
1436
|
-
dynamic: false
|
|
1437
|
-
};
|
|
1438
|
-
}
|
|
1439
|
-
if (!everyEffectSurvives()) return {
|
|
1440
|
-
kind: "decline",
|
|
1441
|
-
reason: "dynamic"
|
|
1442
|
-
};
|
|
1443
|
-
if (effectful.length > 1) {
|
|
1444
|
-
const variantOrder = Object.keys(config.variants ?? {});
|
|
1445
|
-
const keys = effectful.map((entry) => entry.key);
|
|
1446
|
-
if ([...keys].sort((a, b) => variantOrder.indexOf(a) - variantOrder.indexOf(b)).join("\0") !== keys.join("\0")) return {
|
|
1447
|
-
kind: "decline",
|
|
1448
|
-
reason: "dynamic"
|
|
1449
|
-
};
|
|
1450
|
-
}
|
|
1451
|
-
for (const key of [...dynamicAxes.keys()]) if (!Object.hasOwn(config.variants ?? {}, key)) dynamicAxes.delete(key);
|
|
1452
|
-
if (dynamicAxes.size === 0) {
|
|
1453
|
-
const compiled = compiledSelection(selection);
|
|
1454
|
-
if (!compiled) return {
|
|
1455
|
-
kind: "decline",
|
|
1456
|
-
reason: "dynamic"
|
|
1457
|
-
};
|
|
1458
|
-
if (typeof compiled.value === "string") return {
|
|
1459
|
-
kind: "class",
|
|
1460
|
-
className: compiled.value,
|
|
1461
|
-
styles: compiled.styles
|
|
1462
|
-
};
|
|
1463
|
-
return {
|
|
1464
|
-
kind: "slots",
|
|
1465
|
-
expression: JSON.stringify(compiled.value),
|
|
1466
|
-
classNames: compiled.classNames,
|
|
1467
|
-
dynamic: false
|
|
1468
|
-
};
|
|
1469
|
-
}
|
|
1470
|
-
/**
|
|
1471
|
-
* Compile the finite recipe state space into a reduced decision table.
|
|
1472
|
-
*
|
|
1473
|
-
* Each leaf is a *complete* final StyleSet. This matters for declarations overridden by
|
|
1474
|
-
* variants and compounds: selecting independent per-axis atoms would put both values in
|
|
1475
|
-
* the utility layer and let stylesheet order, rather than the recipe's merge order, pick
|
|
1476
|
-
* the winner. Complete leaves retain the same precedence while sharing their atoms with
|
|
1477
|
-
* every `css()` and recipe in the build.
|
|
1478
|
-
*
|
|
1479
|
-
* `undefined` is its own edge because it restores a default variant. `null` and any
|
|
1480
|
-
* undeclared value take the miss edge and explicitly suppress that default. Declared
|
|
1481
|
-
* values use string keys, matching JavaScript's property-key coercion in the recipe
|
|
1482
|
-
* runtime. A flat alternating key/value array avoids the special `__proto__` semantics
|
|
1483
|
-
* of an object literal.
|
|
1484
|
-
*/
|
|
1485
|
-
const axes = Object.keys(config.variants ?? {}).filter((key) => dynamicAxes.has(key));
|
|
1486
|
-
const stateCount = axes.reduce((product, axis) => product * (Object.keys(config.variants?.[axis] ?? {}).length + 2), 1);
|
|
1487
|
-
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.`);
|
|
1488
|
-
const expressions = axes.map((axis) => dynamicAxes.get(axis));
|
|
1489
|
-
const wholeSlots = Array.isArray(config.slots) && slot === void 0;
|
|
1490
|
-
return {
|
|
1491
|
-
kind: "dynamic-style",
|
|
1492
|
-
map: {
|
|
1493
|
-
outputKind: wholeSlots ? "slots" : "class",
|
|
1494
|
-
compile(before = [], after = []) {
|
|
1495
|
-
const nodes = [];
|
|
1496
|
-
const nodeByShape = /* @__PURE__ */ new Map();
|
|
1497
|
-
const leaves = [];
|
|
1498
|
-
const leafByShape = /* @__PURE__ */ new Map();
|
|
1499
|
-
const emittedClasses = /* @__PURE__ */ new Set();
|
|
1500
|
-
const leaf = (dynamicSelection) => {
|
|
1501
|
-
const selected = {
|
|
1502
|
-
...selection,
|
|
1503
|
-
...dynamicSelection
|
|
1504
|
-
};
|
|
1505
|
-
if (wholeSlots) {
|
|
1506
|
-
const compiled = compiledSelection(selected);
|
|
1507
|
-
if (!compiled || typeof compiled.value === "string") return internLeaf("");
|
|
1508
|
-
for (const token of compiled.classNames) emittedClasses.add(token);
|
|
1509
|
-
return internLeaf(compiled.value);
|
|
1510
|
-
}
|
|
1511
|
-
const styles = styleCompiler.resolveRecipe(config, selected, slot);
|
|
1512
|
-
if (!styles) return internLeaf("");
|
|
1513
|
-
const className = styleCompiler.className(styleCompiler.compose(...before, styles, ...after));
|
|
1514
|
-
for (const token of className.split(" ")) if (token) emittedClasses.add(token);
|
|
1515
|
-
return internLeaf(className);
|
|
1516
|
-
};
|
|
1517
|
-
function internLeaf(value) {
|
|
1518
|
-
const shape = JSON.stringify(value);
|
|
1519
|
-
const known = leafByShape.get(shape);
|
|
1520
|
-
if (known !== void 0) return ~known;
|
|
1521
|
-
const id = leaves.length;
|
|
1522
|
-
leaves.push(value);
|
|
1523
|
-
leafByShape.set(shape, id);
|
|
1524
|
-
return ~id;
|
|
1525
|
-
}
|
|
1526
|
-
const buildNode = (index, dynamicSelection) => {
|
|
1527
|
-
if (index === axes.length) return leaf(dynamicSelection);
|
|
1528
|
-
const axis = axes[index];
|
|
1529
|
-
const values = Object.keys(config.variants?.[axis] ?? {});
|
|
1530
|
-
const miss = buildNode(index + 1, {
|
|
1531
|
-
...dynamicSelection,
|
|
1532
|
-
[axis]: null
|
|
1533
|
-
});
|
|
1534
|
-
const absentSelection = { ...dynamicSelection };
|
|
1535
|
-
delete absentSelection[axis];
|
|
1536
|
-
const absent = buildNode(index + 1, absentSelection);
|
|
1537
|
-
const byValue = [];
|
|
1538
|
-
for (const value of values) byValue.push(value, buildNode(index + 1, {
|
|
1539
|
-
...dynamicSelection,
|
|
1540
|
-
[axis]: value
|
|
1541
|
-
}));
|
|
1542
|
-
const refs = [
|
|
1543
|
-
miss,
|
|
1544
|
-
absent,
|
|
1545
|
-
...byValue.filter((_, valueIndex) => valueIndex % 2 === 1)
|
|
1546
|
-
];
|
|
1547
|
-
if (refs.every((ref) => ref === refs[0])) return refs[0];
|
|
1548
|
-
const node = [
|
|
1549
|
-
miss,
|
|
1550
|
-
absent,
|
|
1551
|
-
byValue
|
|
1552
|
-
];
|
|
1553
|
-
const shape = JSON.stringify(node);
|
|
1554
|
-
const known = nodeByShape.get(shape);
|
|
1555
|
-
if (known !== void 0) return known;
|
|
1556
|
-
const id = nodes.length;
|
|
1557
|
-
nodes.push(node);
|
|
1558
|
-
nodeByShape.set(shape, id);
|
|
1559
|
-
return id;
|
|
1560
|
-
};
|
|
1561
|
-
const root = buildNode(0, {});
|
|
1562
|
-
const staticLeaf = root < 0 ? leaves[~root] : void 0;
|
|
1563
|
-
return {
|
|
1564
|
-
expression: root < 0 && effectful.length === 0 ? JSON.stringify(staticLeaf) : `${RECIPE_MAP_HELPER}([${expressions.join(", ")}], ${JSON.stringify(nodes)}, ${JSON.stringify(leaves)}, ${root})`,
|
|
1565
|
-
classNames: [...emittedClasses],
|
|
1566
|
-
staticClasses: typeof staticLeaf === "string" ? staticLeaf : "",
|
|
1567
|
-
outputKind: wholeSlots ? "slots" : "class",
|
|
1568
|
-
usesHelper: !(root < 0 && effectful.length === 0)
|
|
1569
|
-
};
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
};
|
|
1573
|
-
};
|
|
1574
|
-
//#endregion
|
|
1575
|
-
//#region src/runtime-css.ts
|
|
1576
|
-
/** The shape `createCss` and `createMergeCss` both take, derived from a resolved context. */
|
|
1577
|
-
const createCssContext = (ctx) => ({
|
|
1578
|
-
hash: Boolean(ctx.hash.className),
|
|
1579
|
-
conditions: {
|
|
1580
|
-
shift: ctx.conditions.shift,
|
|
1581
|
-
finalize: ctx.conditions.finalize
|
|
1582
|
-
},
|
|
1583
|
-
utility: {
|
|
1584
|
-
prefix: ctx.utility.prefix,
|
|
1585
|
-
hasShorthand: ctx.utility.hasShorthand,
|
|
1586
|
-
resolveShorthand: ctx.utility.resolveShorthand.bind(ctx.utility),
|
|
1587
|
-
transform: ctx.utility.transform.bind(ctx.utility),
|
|
1588
|
-
toHash: ctx.utility.toHash.bind(ctx.utility)
|
|
1589
|
-
}
|
|
1590
|
-
});
|
|
1591
|
-
const createRuntimeCss = (ctx) => {
|
|
1592
|
-
const cssContext = createCssContext(ctx);
|
|
1593
|
-
const cssFn = (0, _bamboocss_shared.createCssUncached)(cssContext);
|
|
1594
|
-
const { mergeCssUncached } = (0, _bamboocss_shared.createMergeCss)(cssContext);
|
|
1595
|
-
return (0, _bamboocss_shared.memo)((...styles) => cssFn(mergeCssUncached(...styles)));
|
|
1596
|
-
};
|
|
1597
|
-
/**
|
|
1598
|
-
* The map is every token in the project, so it is built once per context and shared by
|
|
1599
|
-
* every module in the build — not once per `foldSource`, which would price a whole token
|
|
1600
|
-
* table into each of the overwhelming majority of modules that call `token()` zero times.
|
|
1601
|
-
* Keyed weakly so a context that goes out of scope takes its table with it.
|
|
1602
|
-
*
|
|
1603
|
-
* Both halves of the generated entry are stored, because `token()` and `token.value()` read
|
|
1604
|
-
* different ones and building a second table would pay the same per-project cost twice.
|
|
1605
|
-
*/
|
|
1606
|
-
const tokenValues = /* @__PURE__ */ new WeakMap();
|
|
1607
|
-
const tokenValuesFor = (ctx) => {
|
|
1608
|
-
let values = tokenValues.get(ctx);
|
|
1609
|
-
if (values) return values;
|
|
1610
|
-
values = /* @__PURE__ */ new Map();
|
|
1611
|
-
for (const token of ctx.tokens.allTokens) {
|
|
1612
|
-
const { varRef, isVirtual, condition } = token.extensions;
|
|
1613
|
-
values.set(token.name, {
|
|
1614
|
-
value: ctx.tokens.view.get(token.name) ?? (isVirtual || condition !== "base" ? varRef : token.value),
|
|
1615
|
-
variable: ctx.tokens.view.getVar(token.name) ?? varRef
|
|
1616
|
-
});
|
|
1617
|
-
}
|
|
1618
|
-
tokenValues.set(ctx, values);
|
|
1619
|
-
return values;
|
|
1620
|
-
};
|
|
1621
|
-
const createRuntimeTokenValue = (ctx) => (path) => {
|
|
1622
|
-
const value = tokenValuesFor(ctx).get(path)?.value;
|
|
1623
|
-
return typeof value === "string" ? value : void 0;
|
|
1624
|
-
};
|
|
1625
|
-
/**
|
|
1626
|
-
* The generated runtime's `token()`, rebuilt in-process.
|
|
1627
|
-
*
|
|
1628
|
-
* Reads the `variable` half of the same entry `token.value()` reads the `value` half of.
|
|
1629
|
-
* That half is `varRef` for every token regardless of condition, so unlike
|
|
1630
|
-
* `createRuntimeTokenValue` there is no split to get wrong and no non-string case to
|
|
1631
|
-
* decline: a `var()` reference is a string or the token does not exist. Which is what makes
|
|
1632
|
-
* the default form the trivially foldable one.
|
|
1633
|
-
*/
|
|
1634
|
-
const createRuntimeToken = (ctx) => (path) => tokenValuesFor(ctx).get(path)?.variable || void 0;
|
|
1635
|
-
//#endregion
|
|
1636
|
-
//#region src/fold.ts
|
|
1637
|
-
/**
|
|
1638
|
-
* `cva`/`sva` return a function, so their definitions are compile-time declarations rather
|
|
1639
|
-
* than class-producing calls; once their uses are lowered, the factory calls are erased.
|
|
1640
|
-
* `token` also resolves to no class, but it does resolve to a literal, so it compiles through
|
|
1641
|
-
* its own path rather than being declined outright. A static `viewTransition` bag resolves to
|
|
1642
|
-
* its extracted class and uses the ordinary class candidate path.
|
|
1643
|
-
*
|
|
1644
|
-
* Recipe invocations compile through `fold-recipe`. Inline calls
|
|
1645
|
-
* are recorded under the name the file bound; config calls arrive as `recipe`. Routing both
|
|
1646
|
-
* through one exact finite-state lowering keeps their selection contract identical.
|
|
1647
|
-
*/
|
|
1648
|
-
const FOLDABLE_TYPES = new Set([
|
|
1649
|
-
"css",
|
|
1650
|
-
"pattern",
|
|
1651
|
-
"viewTransition"
|
|
1652
|
-
]);
|
|
1653
|
-
/**
|
|
1654
|
-
* The skip reasons that leave a `css()`-family call in the output.
|
|
1655
|
-
*
|
|
1656
|
-
* `overlapping` is handled by the enclosing fold, and `not-imported` is somebody else's
|
|
1657
|
-
* function of the same name — neither leaves a call of ours.
|
|
1658
|
-
*/
|
|
1659
|
-
const SURVIVES_TO_RUNTIME = new Set([
|
|
1660
|
-
"dynamic",
|
|
1661
|
-
"runtime-binding",
|
|
1662
|
-
"raw-call",
|
|
1663
|
-
"unsupported-kind",
|
|
1664
|
-
"no-call-expression",
|
|
1665
|
-
"unresolved-token",
|
|
1666
|
-
"compile-failed"
|
|
1667
|
-
]);
|
|
1668
|
-
/**
|
|
1669
|
-
* A call of a recipe the file bound itself: `const badge = cva(...)`, then `badge({ tone })`.
|
|
1670
|
-
*
|
|
1671
|
-
* Folded when the whole selection resolves, reported under this reason when it does not.
|
|
1672
|
-
* Deliberately all-or-nothing: an unresolved variant does not merely omit its own class, so a
|
|
1673
|
-
* partially-known selection is not foldable at all.
|
|
1674
|
-
*
|
|
1675
|
-
* Visible at all because it used to not be. The parser matched calls by imported name, so a
|
|
1676
|
-
* local binding was never recorded, and an unfoldable invocation looked identical to code
|
|
1677
|
-
* nothing had parsed.
|
|
1678
|
-
*/
|
|
1679
|
-
const RECIPE_CALL_TYPE = "cva-call";
|
|
1680
|
-
/**
|
|
1681
|
-
* An identifier that actually reads the binding.
|
|
1682
|
-
*
|
|
1683
|
-
* `getDescendantsOfKind(Identifier)` yields every name in the file, and most of them bind or
|
|
1684
|
-
* label rather than read: a JSX tag (`<button/>` against a recipe called `button`), an object
|
|
1685
|
-
* key, a property name, a declaration. Counting those failed builds on modules that had
|
|
1686
|
-
* folded completely — and `button`, `input`, `label`, `select`, `table`, `dialog` and `form`
|
|
1687
|
-
* are all ordinary recipe names as well as intrinsic elements.
|
|
1688
|
-
*
|
|
1689
|
-
* A type position is excluded for a different reason: it is erased, and with it the import.
|
|
1690
|
-
*/
|
|
1691
|
-
const isValueReference = (identifier) => {
|
|
1692
|
-
const parent = identifier.getParent();
|
|
1693
|
-
if (!parent) return false;
|
|
1694
|
-
if (ts_morph.Node.isImportSpecifier(parent) || ts_morph.Node.isExportSpecifier(parent)) return false;
|
|
1695
|
-
if (ts_morph.Node.isImportClause(parent) || ts_morph.Node.isNamespaceImport(parent)) return false;
|
|
1696
|
-
if (ts_morph.Node.isPropertyAccessExpression(parent) && parent.getNameNode() === identifier) return false;
|
|
1697
|
-
if (ts_morph.Node.isQualifiedName(parent) && parent.getRight() === identifier) return false;
|
|
1698
|
-
if (ts_morph.Node.isPropertyAssignment(parent) && parent.getNameNode() === identifier) return false;
|
|
1699
|
-
if (ts_morph.Node.isMethodDeclaration(parent) || ts_morph.Node.isPropertyDeclaration(parent) || ts_morph.Node.isGetAccessorDeclaration(parent) || ts_morph.Node.isSetAccessorDeclaration(parent) || ts_morph.Node.isMethodSignature(parent) || ts_morph.Node.isPropertySignature(parent) || ts_morph.Node.isEnumMember(parent)) {
|
|
1700
|
-
if (parent.getNameNode() === identifier) return false;
|
|
1701
|
-
}
|
|
1702
|
-
if (ts_morph.Node.isLabeledStatement(parent) || ts_morph.Node.isBreakStatement(parent) || ts_morph.Node.isContinueStatement(parent)) return false;
|
|
1703
|
-
if (ts_morph.Node.isJsxOpeningElement(parent) || ts_morph.Node.isJsxSelfClosingElement(parent) || ts_morph.Node.isJsxClosingElement(parent)) {
|
|
1704
|
-
if (parent.getTagNameNode() === identifier) return identifier.getText()[0] === identifier.getText()[0]?.toUpperCase();
|
|
1705
|
-
}
|
|
1706
|
-
if (ts_morph.Node.isJsxAttribute(parent)) return false;
|
|
1707
|
-
if (ts_morph.Node.isBindingElement(parent) && parent.getPropertyNameNode() === identifier) return false;
|
|
1708
|
-
if ((ts_morph.Node.isVariableDeclaration(parent) || ts_morph.Node.isParameterDeclaration(parent) || ts_morph.Node.isBindingElement(parent) || ts_morph.Node.isFunctionDeclaration(parent) || ts_morph.Node.isClassDeclaration(parent)) && parent.getNameNode() === identifier) return false;
|
|
1709
|
-
return !identifier.getFirstAncestor((ancestor) => ts_morph.Node.isTypeNode(ancestor) || ts_morph.Node.isTypeAliasDeclaration(ancestor) || ts_morph.Node.isInterfaceDeclaration(ancestor));
|
|
1710
|
-
};
|
|
1711
|
-
/**
|
|
1712
|
-
* Imports a surviving reference to is not a failure.
|
|
1713
|
-
*
|
|
1714
|
-
* These are what the compiler itself writes; all live in `cx` and pull no engine, so a
|
|
1715
|
-
* reference to one is the fold having worked.
|
|
1716
|
-
*/
|
|
1717
|
-
const PERMITTED_BINDINGS = new Set([
|
|
1718
|
-
"cx",
|
|
1719
|
-
RECIPE_MAP_HELPER,
|
|
1720
|
-
SPLIT_PROPS_HELPER
|
|
1721
|
-
]);
|
|
1722
|
-
/**
|
|
1723
|
-
* Whether a module's text could hold a `splitVariantProps` property access.
|
|
1724
|
-
*
|
|
1725
|
-
* A necessary condition, deliberately not a sufficient one: the name inside a string or a
|
|
1726
|
-
* comment opens the walk, which costs what the walk always cost. What it must never do is
|
|
1727
|
-
* close on a module that has one, and an identifier may be spelled with unicode escapes —
|
|
1728
|
-
* `badge.splitVariantProps(p)` reads as the name to the compiler and contains none of it
|
|
1729
|
-
* as text. Both escape forms start `\u`, so one test covers every spelling.
|
|
1730
|
-
*/
|
|
1731
|
-
const mayNameSplitVariantProps = (text) => text.includes("splitVariantProps") || text.includes("\\u");
|
|
1732
|
-
/**
|
|
1733
|
-
* The pieces `trim` reduces a module specifier by, hoisted because a regex literal
|
|
1734
|
-
* constructs a new object every time it is evaluated and `trim` runs per specifier per
|
|
1735
|
-
* import declaration per module.
|
|
1736
|
-
*/
|
|
1737
|
-
const LEADING_RELATIVE = /^(?:\.\.?\/)+/;
|
|
1738
|
-
const TRAILING_SLASH = /\/$/;
|
|
1739
|
-
const MODULE_EXTENSION = /\.[mc]?[jt]sx?$/;
|
|
1740
|
-
const TRAILING_INDEX = /\/index$/;
|
|
1741
|
-
/**
|
|
1742
|
-
* An argument that cannot run anything when it is evaluated.
|
|
1743
|
-
*
|
|
1744
|
-
* `token()` takes one argument, but javascript evaluates every argument a call site passes
|
|
1745
|
-
* before the call — so a fold that drops an extra one also drops whatever evaluating it would
|
|
1746
|
-
* have done. `token('x', compute())` is pathological and no longer type-checks, and the fold's
|
|
1747
|
-
* contract is behaviour preservation regardless: a literal is the cheap way to prove it, since
|
|
1748
|
-
* it means no call, no property read, no getter.
|
|
1749
|
-
*/
|
|
1750
|
-
const isInertArgument = (node) => ts_morph.Node.isStringLiteral(node) || ts_morph.Node.isNumericLiteral(node) || ts_morph.Node.isNoSubstitutionTemplateLiteral(node) || node.getKind() === ts_morph.SyntaxKind.TrueKeyword || node.getKind() === ts_morph.SyntaxKind.FalseKeyword || node.getKind() === ts_morph.SyntaxKind.NullKeyword || ts_morph.Node.isIdentifier(node) && node.getText() === "undefined";
|
|
1751
|
-
/**
|
|
1752
|
-
* An expression whose evaluation cannot do anything observable, so deleting it preserves
|
|
1753
|
-
* behaviour.
|
|
1754
|
-
*
|
|
1755
|
-
* `isInertArgument` covers the leaves; this walks the object and array literals a recipe
|
|
1756
|
-
* call is actually written with. A spread runs the source's getters, a computed key runs an
|
|
1757
|
-
* expression, a getter or method definition is a function — none of those are safe to
|
|
1758
|
-
* delete, so they are declined rather than enumerated.
|
|
1759
|
-
*/
|
|
1760
|
-
const isInertExpression = (node) => {
|
|
1761
|
-
if (isInertArgument(node)) return true;
|
|
1762
|
-
if (ts_morph.Node.isIdentifier(node)) return true;
|
|
1763
|
-
if (ts_morph.Node.isAsExpression(node) || ts_morph.Node.isSatisfiesExpression(node) || ts_morph.Node.isNonNullExpression(node) || ts_morph.Node.isTypeAssertion(node) || ts_morph.Node.isParenthesizedExpression(node)) return isInertExpression(node.getExpression());
|
|
1764
|
-
if (ts_morph.Node.isArrowFunction(node) || ts_morph.Node.isFunctionExpression(node)) return true;
|
|
1765
|
-
if (ts_morph.Node.isRegularExpressionLiteral(node) || ts_morph.Node.isBigIntLiteral(node)) return true;
|
|
1766
|
-
if (ts_morph.Node.isPrefixUnaryExpression(node)) {
|
|
1767
|
-
const operator = node.getOperatorToken();
|
|
1768
|
-
return (operator === ts_morph.SyntaxKind.MinusToken || operator === ts_morph.SyntaxKind.PlusToken || operator === ts_morph.SyntaxKind.ExclamationToken || operator === ts_morph.SyntaxKind.TildeToken) && isInertExpression(node.getOperand());
|
|
1769
|
-
}
|
|
1770
|
-
if (ts_morph.Node.isBinaryExpression(node)) {
|
|
1771
|
-
const operator = node.getOperatorToken().getKind();
|
|
1772
|
-
return (operator === ts_morph.SyntaxKind.QuestionQuestionToken || operator === ts_morph.SyntaxKind.BarBarToken || operator === ts_morph.SyntaxKind.AmpersandAmpersandToken) && isInertExpression(node.getLeft()) && isInertExpression(node.getRight());
|
|
1773
|
-
}
|
|
1774
|
-
if (ts_morph.Node.isObjectLiteralExpression(node)) return node.getProperties().every((property) => {
|
|
1775
|
-
if (ts_morph.Node.isShorthandPropertyAssignment(property)) return true;
|
|
1776
|
-
if (!ts_morph.Node.isPropertyAssignment(property)) return false;
|
|
1777
|
-
if (ts_morph.Node.isComputedPropertyName(property.getNameNode())) return false;
|
|
1778
|
-
const initializer = property.getInitializer();
|
|
1779
|
-
return initializer !== void 0 && isInertExpression(initializer);
|
|
1780
|
-
});
|
|
1781
|
-
if (ts_morph.Node.isArrayLiteralExpression(node)) return node.getElements().every(isInertExpression);
|
|
1782
|
-
return false;
|
|
1783
|
-
};
|
|
1784
|
-
/**
|
|
1785
|
-
* Source files a box tree reaches, other than the one being folded.
|
|
1786
|
-
*
|
|
1787
|
-
* When the extractor resolves an imported identifier it boxes the *declaration's*
|
|
1788
|
-
* node, which lives in the defining module. Walking the tree and reading each node's
|
|
1789
|
-
* source file therefore recovers exactly the files a fold depended on — narrower and
|
|
1790
|
-
* more accurate than treating every import of the module as a dependency.
|
|
1791
|
-
*/
|
|
1792
|
-
const collectSourceFiles = (node, ctx, seen = /* @__PURE__ */ new Set()) => {
|
|
1793
|
-
if (!node || seen.has(node)) return;
|
|
1794
|
-
seen.add(node);
|
|
1795
|
-
ctx.record(node.getNode?.());
|
|
1796
|
-
if (_bamboocss_extractor.box.isMap(node)) {
|
|
1797
|
-
for (const child of node.value.values()) collectSourceFiles(child, ctx, seen);
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
if (_bamboocss_extractor.box.isArray(node)) for (const child of node.value) collectSourceFiles(child, ctx, seen);
|
|
1801
|
-
};
|
|
1802
|
-
const createDependencyScan = (ownFile) => {
|
|
1803
|
-
const results = /* @__PURE__ */ new Set();
|
|
1804
|
-
const paths = /* @__PURE__ */ new Map();
|
|
1805
|
-
return {
|
|
1806
|
-
results,
|
|
1807
|
-
record(node) {
|
|
1808
|
-
if (!node) return;
|
|
1809
|
-
const sourceFile = node.getSourceFile();
|
|
1810
|
-
if (sourceFile === ownFile) return;
|
|
1811
|
-
let path = paths.get(sourceFile);
|
|
1812
|
-
if (path === void 0) {
|
|
1813
|
-
path = sourceFile.getFilePath();
|
|
1814
|
-
paths.set(sourceFile, path);
|
|
1815
|
-
}
|
|
1816
|
-
if (path) results.add(path);
|
|
1817
|
-
}
|
|
1818
|
-
};
|
|
1819
|
-
};
|
|
1820
|
-
/**
|
|
1821
|
-
* The call expression to replace.
|
|
1822
|
-
*
|
|
1823
|
-
* `extractCallExpressionArguments` boxes the argument list against the call node and
|
|
1824
|
-
* pushes `[callNode, argNode]` onto each argument's stack, so the call is reachable
|
|
1825
|
-
* from either shape the parser stores: the argument array (multi-arg) or the first
|
|
1826
|
-
* argument's map (single-arg).
|
|
1827
|
-
*/
|
|
1828
|
-
const findCallExpression = (node) => {
|
|
1829
|
-
const own = node.getNode?.();
|
|
1830
|
-
if (own && ts_morph.Node.isCallExpression(own)) return own;
|
|
1831
|
-
const stack = node.getStack?.() ?? [];
|
|
1832
|
-
for (const entry of stack) if (ts_morph.Node.isCallExpression(entry)) return entry;
|
|
1833
|
-
let current = own;
|
|
1834
|
-
for (let depth = 0; current && depth < 3; depth++) {
|
|
1835
|
-
if (ts_morph.Node.isCallExpression(current)) return current;
|
|
1836
|
-
current = current.getParent();
|
|
1837
|
-
}
|
|
1838
|
-
};
|
|
1839
|
-
/**
|
|
1840
|
-
* `css.raw(...)` must keep returning a style object — folding it to a class string
|
|
1841
|
-
* breaks every caller composing those styles. The file matcher strips `.raw` when it
|
|
1842
|
-
* normalizes function names, so the parser result cannot tell us; the callee text can.
|
|
1843
|
-
*/
|
|
1844
|
-
const isRawCall = (call) => {
|
|
1845
|
-
if (!ts_morph.Node.isCallExpression(call)) return false;
|
|
1846
|
-
const callee = call.getExpression().getText();
|
|
1847
|
-
return callee === "raw" || callee.endsWith(".raw");
|
|
1848
|
-
};
|
|
1849
|
-
/** The identifier a callee is rooted at: `css` for `css(…)`, `panda` for `panda.css(…)`. */
|
|
1850
|
-
const calleeRootName = (call) => {
|
|
1851
|
-
if (!ts_morph.Node.isCallExpression(call)) return void 0;
|
|
1852
|
-
let current = call.getExpression();
|
|
1853
|
-
while (ts_morph.Node.isPropertyAccessExpression(current)) current = current.getExpression();
|
|
1854
|
-
return ts_morph.Node.isIdentifier(current) ? current.getText() : void 0;
|
|
1855
|
-
};
|
|
1856
|
-
/**
|
|
1857
|
-
* Local names a module binds to an import of bamboo's own generated system.
|
|
1858
|
-
*
|
|
1859
|
-
* The parser matches by name and asks neither question this does — deliberately, since
|
|
1860
|
-
* for CSS extraction the worst case is a few unused rules. A transform cannot be that
|
|
1861
|
-
* relaxed, and it needs both halves:
|
|
1862
|
-
*
|
|
1863
|
-
* - imported at all, or a user's `const css = (s) => JSON.stringify(s)` gets rewritten
|
|
1864
|
-
* - imported *from bamboo*, or `import { css } from '@emotion/css'` does, which is the
|
|
1865
|
-
* likelier accident of the two since a migrating project has both in the tree
|
|
1866
|
-
*
|
|
1867
|
-
* Answered together and once per file, because the scan is the expensive part and both
|
|
1868
|
-
* answers fall out of the same pass. Per call site instead of per file, this scan
|
|
1869
|
-
* measured +74% on the largest sandbox module.
|
|
1870
|
-
*/
|
|
1871
|
-
const bambooImportedNames = (sourceFile, ctx) => {
|
|
1872
|
-
const names = /* @__PURE__ */ new Set();
|
|
1873
|
-
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
1874
|
-
const mod = declaration.getModuleSpecifierValue();
|
|
1875
|
-
for (const named of declaration.getNamedImports()) {
|
|
1876
|
-
const name = named.getNameNode().getText();
|
|
1877
|
-
const alias = named.getAliasNode()?.getText() ?? name;
|
|
1878
|
-
if (ctx.imports.match({
|
|
1879
|
-
mod,
|
|
1880
|
-
name,
|
|
1881
|
-
alias
|
|
1882
|
-
})) names.add(alias);
|
|
1883
|
-
}
|
|
1884
|
-
const namespace = declaration.getNamespaceImport();
|
|
1885
|
-
if (namespace) {
|
|
1886
|
-
const alias = namespace.getText();
|
|
1887
|
-
if (ctx.imports.match({
|
|
1888
|
-
mod,
|
|
1889
|
-
name: alias,
|
|
1890
|
-
alias,
|
|
1891
|
-
kind: "namespace"
|
|
1892
|
-
})) names.add(alias);
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
return names;
|
|
1896
|
-
};
|
|
1897
|
-
/**
|
|
1898
|
-
* Is the callee the imported binding, or a local one that shadows it?
|
|
1899
|
-
*
|
|
1900
|
-
* A block-scoped binding of the same name is legal alongside the import, and it is
|
|
1901
|
-
* the one the call actually reaches. Walking ancestors is the precise answer; the
|
|
1902
|
-
* cost is kept off the common path by only inspecting the two node kinds that can
|
|
1903
|
-
* introduce a binding. Ancestors of a call in JSX are overwhelmingly elements and
|
|
1904
|
-
* attributes, which match neither and cost nothing.
|
|
1905
|
-
*/
|
|
1906
|
-
const isShadowed = (call, name) => {
|
|
1907
|
-
for (let node = call.getParent(); node; node = node.getParent()) {
|
|
1908
|
-
if (ts_morph.Node.isSourceFile(node)) return false;
|
|
1909
|
-
if (bindsName(node, name)) return true;
|
|
1910
|
-
}
|
|
1911
|
-
return false;
|
|
1912
|
-
};
|
|
1913
|
-
/**
|
|
1914
|
-
* Does a binding name introduce `name`?
|
|
1915
|
-
*
|
|
1916
|
-
* A plain identifier check is not enough: destructuring is the likeliest way a
|
|
1917
|
-
* same-named local reaches a call, since `({ css }) => css(…)` is what a component
|
|
1918
|
-
* taking a `css` prop looks like. Nested and rest elements bind too, so the pattern
|
|
1919
|
-
* is walked rather than inspected at the top level.
|
|
1920
|
-
*/
|
|
1921
|
-
const bindingIntroduces = (nameNode, name) => {
|
|
1922
|
-
if (!nameNode) return false;
|
|
1923
|
-
if (ts_morph.Node.isIdentifier(nameNode)) return nameNode.getText() === name;
|
|
1924
|
-
if (ts_morph.Node.isObjectBindingPattern(nameNode) || ts_morph.Node.isArrayBindingPattern(nameNode)) return nameNode.getElements().some((element) => ts_morph.Node.isBindingElement(element) && bindingIntroduces(element.getNameNode(), name));
|
|
1925
|
-
return false;
|
|
1926
|
-
};
|
|
1927
|
-
const declarationsBind = (list, name) => ts_morph.Node.isVariableDeclarationList(list) && list.getDeclarations().some((declaration) => bindingIntroduces(declaration.getNameNode(), name));
|
|
1928
|
-
const bindsName = (scope, name) => {
|
|
1929
|
-
if (ts_morph.Node.isBlock(scope)) return scope.getStatements().some((statement) => statementBinds(statement, name));
|
|
1930
|
-
if (ts_morph.Node.isFunctionDeclaration(scope) || ts_morph.Node.isArrowFunction(scope) || ts_morph.Node.isFunctionExpression(scope) || ts_morph.Node.isMethodDeclaration(scope)) return scope.getParameters().some((parameter) => bindingIntroduces(parameter.getNameNode(), name));
|
|
1931
|
-
if (ts_morph.Node.isCatchClause(scope)) return bindingIntroduces(scope.getVariableDeclaration()?.getNameNode(), name);
|
|
1932
|
-
if (ts_morph.Node.isForStatement(scope) || ts_morph.Node.isForOfStatement(scope) || ts_morph.Node.isForInStatement(scope)) return declarationsBind(scope.getInitializer(), name);
|
|
1933
|
-
return false;
|
|
1934
|
-
};
|
|
1935
|
-
const statementBinds = (statement, name) => {
|
|
1936
|
-
if (ts_morph.Node.isVariableStatement(statement)) return statement.getDeclarations().some((declaration) => bindingIntroduces(declaration.getNameNode(), name));
|
|
1937
|
-
if (ts_morph.Node.isFunctionDeclaration(statement) || ts_morph.Node.isClassDeclaration(statement)) return statement.getNameNode()?.getText() === name;
|
|
1938
|
-
return false;
|
|
1939
|
-
};
|
|
1940
|
-
const hasStyles = (data) => data.length > 0 && data.every((entry) => entry != null && typeof entry === "object");
|
|
1941
|
-
/**
|
|
1942
|
-
* Pair each source argument with the box the parser stored for it, and require the
|
|
1943
|
-
* box to account for all of it. The parser keeps either the whole argument array
|
|
1944
|
-
* (multi-arg calls) or just the first argument's map (single-arg calls).
|
|
1945
|
-
*/
|
|
1946
|
-
const argumentsAccountedFor = (call, boxNode) => {
|
|
1947
|
-
if (!ts_morph.Node.isCallExpression(call)) return false;
|
|
1948
|
-
const args = call.getArguments();
|
|
1949
|
-
if (args.length === 0) return true;
|
|
1950
|
-
if (_bamboocss_extractor.box.isArray(boxNode) && boxNode.getNode() === call) {
|
|
1951
|
-
if (boxNode.value.length !== args.length) return false;
|
|
1952
|
-
return args.every((arg, index) => accountsForSource(arg, boxNode.value[index]));
|
|
1953
|
-
}
|
|
1954
|
-
if (args.length !== 1) return false;
|
|
1955
|
-
return accountsForSource(args[0], boxNode);
|
|
1956
|
-
};
|
|
1957
|
-
const foldSource = (options) => {
|
|
1958
|
-
const { ctx, code, parserResult, runtimeCss = createRuntimeCss(ctx), styleCompiler, maxRecipeStates, parseModule, recipeConfigCache, reportSurvivors, sourceFile: ownSourceFile } = options;
|
|
1959
|
-
const runtimeToken = createRuntimeToken(ctx);
|
|
1960
|
-
const runtimeTokenValue = createRuntimeTokenValue(ctx);
|
|
1961
|
-
/**
|
|
1962
|
-
* Does this specifier name a module that exports the css API, exactly?
|
|
1963
|
-
*
|
|
1964
|
-
* `ImportMap.match` is substring-based, which is right for deciding whether a call is
|
|
1965
|
-
* bamboo's and wrong for deciding whether a module can be imported *from*:
|
|
1966
|
-
* `styled-system/css/css` matches while exporting no `cx`. So the comparison is
|
|
1967
|
-
* equality, not containment.
|
|
1968
|
-
*
|
|
1969
|
-
* A tsconfig path alias is resolved first, the same way `ImportMap.match` does. Without
|
|
1970
|
-
* that, `@site/styled-system/css` — the spelling this repo's own website uses — fails
|
|
1971
|
-
* the check and silently loses helper lowering, which is indistinguishable in the
|
|
1972
|
-
* diagnostics from a genuinely dynamic call.
|
|
1973
|
-
*/
|
|
1974
|
-
const cssModules = ctx.imports.matchers.css?.mods ?? [];
|
|
1975
|
-
/**
|
|
1976
|
-
* The generated css module, the only one whose exports are known.
|
|
1977
|
-
*
|
|
1978
|
-
* A configured `importMap.css` points at the user's own wrapper, and a wrapper that
|
|
1979
|
-
* re-exports `css` need not re-export `cx` — adding one there imports a binding that
|
|
1980
|
-
* may not exist. Reusing a `cx` the user already imported from it stays fine, since
|
|
1981
|
-
* that binding demonstrably resolves; only *adding* one is restricted.
|
|
1982
|
-
*/
|
|
1983
|
-
const generatedCssModule = [ctx.imports.outdir, "css"].join("/");
|
|
1984
|
-
const pathMappings = ctx.conf.tsOptions?.pathMappings;
|
|
1985
|
-
/**
|
|
1986
|
-
* The spelling reduced to the module it names.
|
|
1987
|
-
*
|
|
1988
|
-
* The extension and `/index` are stripped because bamboo's own output makes a file
|
|
1989
|
-
* import them: `outExtension: 'js'` under NodeNext resolution is written
|
|
1990
|
-
* `styled-system/css/index.js`, which is neither equal to `styled-system/css` nor a
|
|
1991
|
-
* tail of it. Extraction admitted such a file anyway — `ImportMap.match` is
|
|
1992
|
-
* substring-based — so the call was folded while the *insert* was refused, and the
|
|
1993
|
-
* result was reported as `dynamic`: the same silent downgrade the alias case above
|
|
1994
|
-
* describes, reached through the extension instead.
|
|
1995
|
-
*
|
|
1996
|
-
* This does not weaken the equality the comment above insists on. `styled-system/css/css`
|
|
1997
|
-
* still names neither, because only a trailing `/index` is a module's own directory.
|
|
1998
|
-
*
|
|
1999
|
-
* `.d.ts` is deliberately not stripped. A declaration file exports no runtime binding, so
|
|
2000
|
-
* matching one would authorise inserting an import that resolves to nothing — and a value
|
|
2001
|
-
* import cannot name one anyway, which is what makes leaving it out free.
|
|
2002
|
-
*/
|
|
2003
|
-
const trim = (value) => value.replaceAll("\\", "/").replace(LEADING_RELATIVE, "").replace(TRAILING_SLASH, "").replace(MODULE_EXTENSION, "").replace(TRAILING_INDEX, "");
|
|
2004
|
-
const matchesModule = (mod, entries) => {
|
|
2005
|
-
const candidates = [mod];
|
|
2006
|
-
if (pathMappings) {
|
|
2007
|
-
const resolved = (0, _bamboocss_config_ts_path.resolveTsPathPattern)(pathMappings, mod);
|
|
2008
|
-
if (resolved) candidates.push(resolved);
|
|
2009
|
-
}
|
|
2010
|
-
return candidates.some((candidate) => {
|
|
2011
|
-
const normalized = trim(candidate);
|
|
2012
|
-
return entries.some((entry) => {
|
|
2013
|
-
const target = trim(entry);
|
|
2014
|
-
return normalized === target || normalized.endsWith(`/${target}`);
|
|
2015
|
-
});
|
|
2016
|
-
});
|
|
2017
|
-
};
|
|
2018
|
-
const isBambooCssModule = (mod) => matchesModule(mod, cssModules);
|
|
2019
|
-
const isGeneratedCssModule = (mod) => matchesModule(mod, [generatedCssModule]);
|
|
2020
|
-
/**
|
|
2021
|
-
* Where the compiler's helpers can be imported from, for a file that imports the generated
|
|
2022
|
-
* css module by *subpath* rather than through its barrel.
|
|
2023
|
-
*
|
|
2024
|
-
* `styled-system/css/cva.js` is a real spelling, and it cannot host the helper: that module
|
|
2025
|
-
* exports `cva`, not `cvaMap`. Matching only the barrel meant no host was found and every
|
|
2026
|
-
* runtime recipe selection in the file declined — a failure whose reported reason said
|
|
2027
|
-
* nothing about import spelling, and whose suggested remedies all pointed elsewhere.
|
|
2028
|
-
*
|
|
2029
|
-
* The sibling `cx` module is what actually exports them. The prefix is verified against the
|
|
2030
|
-
* configured output before anything is derived, so an unrelated `foo/css/bar.js` is left
|
|
2031
|
-
* alone, and the caller's extension is preserved rather than guessed at.
|
|
2032
|
-
*/
|
|
2033
|
-
const helperModuleFromSubpath = (mod) => {
|
|
2034
|
-
const normalized = mod.replaceAll("\\", "/");
|
|
2035
|
-
const at = normalized.lastIndexOf("/css/");
|
|
2036
|
-
if (at < 0) return void 0;
|
|
2037
|
-
const prefix = normalized.slice(0, at + 5 - 1);
|
|
2038
|
-
if (!isGeneratedCssModule(prefix)) return void 0;
|
|
2039
|
-
const rest = normalized.slice(at + 5);
|
|
2040
|
-
if (!rest || rest.includes("/")) return void 0;
|
|
2041
|
-
const dot = rest.lastIndexOf(".");
|
|
2042
|
-
const extension = dot > 0 ? rest.slice(dot) : "";
|
|
2043
|
-
if (rest === `cx${extension}`) return void 0;
|
|
2044
|
-
return `${prefix}/cx${extension}`;
|
|
2045
|
-
};
|
|
2046
|
-
/**
|
|
2047
|
-
* The generated css entry as spelled beside an imported config recipe.
|
|
2048
|
-
*
|
|
2049
|
-
* A decision table needs only `cvaMap`, but a module importing a config recipe often has
|
|
2050
|
-
* no css import to extend. Preserve a relative/aliased styled-system spelling by replacing
|
|
2051
|
-
* its `/recipes` suffix; falling back to the configured generated entry covers bare imports.
|
|
2052
|
-
*/
|
|
2053
|
-
const configRecipeCssSpecifier = (call, binding) => {
|
|
2054
|
-
for (const declaration of call.getSourceFile().getImportDeclarations()) {
|
|
2055
|
-
if (declaration.isTypeOnly()) continue;
|
|
2056
|
-
if (!declaration.getNamedImports().some((named) => {
|
|
2057
|
-
if (named.isTypeOnly()) return false;
|
|
2058
|
-
return (named.getAliasNode() ?? named.getNameNode()).getText() === binding;
|
|
2059
|
-
})) continue;
|
|
2060
|
-
const mod = declaration.getModuleSpecifierValue().replaceAll("\\", "/");
|
|
2061
|
-
const at = mod.lastIndexOf("/recipes");
|
|
2062
|
-
if (at >= 0) return `${mod.slice(0, at)}/css`;
|
|
2063
|
-
}
|
|
2064
|
-
return generatedCssModule;
|
|
2065
|
-
};
|
|
2066
|
-
/**
|
|
2067
|
-
* How *this* module would have to spell the css module, learnt from one that already does.
|
|
2068
|
-
*
|
|
2069
|
-
* A file calling an imported recipe need not import the css module at all, so when the
|
|
2070
|
-
* lowering needs a decision-table helper there is no spelling in the file to copy. The declaring module
|
|
2071
|
-
* necessarily has one — `cva` came from it — and that is the spelling reused here.
|
|
2072
|
-
*
|
|
2073
|
-
* A bare or aliased specifier resolves identically from any file, so it is taken as
|
|
2074
|
-
* written. A relative one is re-based: resolved against the module that wrote it, then
|
|
2075
|
-
* expressed from the module being folded.
|
|
2076
|
-
*/
|
|
2077
|
-
const cssModuleSpecifierFrom = (declaring) => {
|
|
2078
|
-
for (const declaration of declaring.getImportDeclarations()) {
|
|
2079
|
-
if (declaration.isTypeOnly()) continue;
|
|
2080
|
-
const mod = declaration.getModuleSpecifierValue();
|
|
2081
|
-
if (isGeneratedCssModule(mod)) return mod;
|
|
2082
|
-
}
|
|
2083
|
-
};
|
|
2084
|
-
/**
|
|
2085
|
-
* That spelling, said from the module being folded.
|
|
2086
|
-
*
|
|
2087
|
-
* A bare or aliased specifier resolves identically from any file, so it is taken as
|
|
2088
|
-
* written. A relative one is re-based: resolved against the module that wrote it, then
|
|
2089
|
-
* expressed from the module being folded. Pure path arithmetic, so it holds a string
|
|
2090
|
-
* rather than a node — a cached node does not survive the next `addSourceFile`, which
|
|
2091
|
-
* ts-morph implements by forgetting the file's whole tree.
|
|
2092
|
-
*/
|
|
2093
|
-
const rebaseSpecifier = (specifier, declaringPath, consumingPath) => {
|
|
2094
|
-
if (!specifier.startsWith(".")) return specifier;
|
|
2095
|
-
const absolute = (0, node_path.resolve)((0, node_path.dirname)(declaringPath), specifier);
|
|
2096
|
-
const rebased = (0, node_path.relative)((0, node_path.dirname)(consumingPath), absolute).replaceAll("\\", "/");
|
|
2097
|
-
if (!rebased) return void 0;
|
|
2098
|
-
return rebased.startsWith(".") ? rebased : `./${rebased}`;
|
|
2099
|
-
};
|
|
2100
|
-
/**
|
|
2101
|
-
* Configs of one foreign module, parsed once however many of its recipes are called.
|
|
2102
|
-
*
|
|
2103
|
-
* Falls back to a per-call map when the caller supplies none, so the fold stays correct
|
|
2104
|
-
* standalone — only repeated, which is what the shared cache exists to avoid.
|
|
2105
|
-
*/
|
|
2106
|
-
const configsByModule = recipeConfigCache ?? /* @__PURE__ */ new Map();
|
|
2107
|
-
/** The specifier each imported recipe's module used for the css module, when it needs one. */
|
|
2108
|
-
const helperModules = /* @__PURE__ */ new Map();
|
|
2109
|
-
/** Declaring modules a fold read, recorded as paths because their nodes do not persist. */
|
|
2110
|
-
const foreignDependencies = /* @__PURE__ */ new Set();
|
|
2111
|
-
/** Resolutions for this module's own call sites, keyed by the name the call site writes. */
|
|
2112
|
-
const importedRecipes = /* @__PURE__ */ new Map();
|
|
2113
|
-
/**
|
|
2114
|
-
* The config of a recipe this module imports.
|
|
2115
|
-
*
|
|
2116
|
-
* The binding is followed with ts-morph's symbol aliasing rather than by re-reading import
|
|
2117
|
-
* declarations, because that is what already understands the shapes these are reached
|
|
2118
|
-
* through: `export { badge } from './styles'`, `export * from './styles'`, and an alias at
|
|
2119
|
-
* either end. Each hop is an alias symbol, so following them to a non-alias lands on the
|
|
2120
|
-
* declaration wherever it lives.
|
|
2121
|
-
*
|
|
2122
|
-
* The selected declarations do not depend on which module the call is in. A recipe lowered
|
|
2123
|
-
* here therefore reaches the same globally shared atoms as a call in its declaring module.
|
|
2124
|
-
*/
|
|
2125
|
-
const resolveImportedRecipe = (call, name, origin) => {
|
|
2126
|
-
if (importedRecipes.has(name)) return importedRecipes.get(name);
|
|
2127
|
-
const resolve = () => {
|
|
2128
|
-
if (!parseModule) return void 0;
|
|
2129
|
-
const consuming = call.getSourceFile();
|
|
2130
|
-
if (origin.filePath === consuming.getFilePath()) return void 0;
|
|
2131
|
-
let foreign = configsByModule.get(origin.filePath);
|
|
2132
|
-
if (!foreign) {
|
|
2133
|
-
const result = parseModule(origin.filePath);
|
|
2134
|
-
if (!result) return void 0;
|
|
2135
|
-
const collected = collectRecipeConfigs(result);
|
|
2136
|
-
const declaring = [...collected.values()].find((entry) => entry.box)?.box?.getNode?.()?.getSourceFile();
|
|
2137
|
-
const configs = /* @__PURE__ */ new Map();
|
|
2138
|
-
for (const [key, entry] of collected) configs.set(key, entry === AMBIGUOUS ? entry : {
|
|
2139
|
-
config: entry.config,
|
|
2140
|
-
box: void 0
|
|
2141
|
-
});
|
|
2142
|
-
foreign = {
|
|
2143
|
-
configs,
|
|
2144
|
-
cssSpecifier: declaring ? cssModuleSpecifierFrom(declaring) : void 0
|
|
2145
|
-
};
|
|
2146
|
-
configsByModule.set(origin.filePath, foreign);
|
|
2147
|
-
}
|
|
2148
|
-
const entry = foreign.configs.get(origin.name);
|
|
2149
|
-
if (!entry || entry === AMBIGUOUS) return void 0;
|
|
2150
|
-
foreignDependencies.add(origin.filePath);
|
|
2151
|
-
helperModules.set(name, foreign.cssSpecifier ? rebaseSpecifier(foreign.cssSpecifier, origin.filePath, consuming.getFilePath()) : void 0);
|
|
2152
|
-
return entry;
|
|
2153
|
-
};
|
|
2154
|
-
const resolved = resolve();
|
|
2155
|
-
importedRecipes.set(name, resolved);
|
|
2156
|
-
return resolved;
|
|
2157
|
-
};
|
|
2158
|
-
const folded = [];
|
|
2159
|
-
const skipped = [];
|
|
2160
|
-
const candidates = [];
|
|
2161
|
-
const seenRanges = /* @__PURE__ */ new Set();
|
|
2162
|
-
const recipeConfigs = collectRecipeConfigs(parserResult);
|
|
2163
|
-
const recipeDefinitions = [];
|
|
2164
|
-
for (const [name, entry] of recipeConfigs) {
|
|
2165
|
-
if (entry === AMBIGUOUS) continue;
|
|
2166
|
-
const definition = entry.box?.getNode?.();
|
|
2167
|
-
if (!definition) continue;
|
|
2168
|
-
const call = ts_morph.Node.isCallExpression(definition) ? definition : definition.getFirstAncestorByKind(ts_morph.SyntaxKind.CallExpression);
|
|
2169
|
-
if (!call || code.slice(call.getStart(), call.getEnd()) !== call.getText()) continue;
|
|
2170
|
-
recipeDefinitions.push({
|
|
2171
|
-
name,
|
|
2172
|
-
call
|
|
2173
|
-
});
|
|
2174
|
-
}
|
|
2175
|
-
/** Ranges already reported as declined, so one call is never counted twice. */
|
|
2176
|
-
const reportedRanges = /* @__PURE__ */ new Set();
|
|
2177
|
-
const importCache = /* @__PURE__ */ new Map();
|
|
2178
|
-
const importsFor = (sourceFile) => {
|
|
2179
|
-
let names = importCache.get(sourceFile);
|
|
2180
|
-
if (!names) {
|
|
2181
|
-
names = bambooImportedNames(sourceFile, ctx);
|
|
2182
|
-
importCache.set(sourceFile, names);
|
|
2183
|
-
}
|
|
2184
|
-
return names;
|
|
2185
|
-
};
|
|
2186
|
-
for (const item of parserResult.toArray()) {
|
|
2187
|
-
const type = item.type ?? "";
|
|
2188
|
-
const name = item.name ?? type;
|
|
2189
|
-
if (!item.box) continue;
|
|
2190
|
-
const call = findCallExpression(item.box);
|
|
2191
|
-
if (type === "token" || type === "tokenValue") {
|
|
2192
|
-
if (!call) {
|
|
2193
|
-
skipped.push({
|
|
2194
|
-
name,
|
|
2195
|
-
reason: "no-call-expression",
|
|
2196
|
-
start: 0,
|
|
2197
|
-
end: 0
|
|
2198
|
-
});
|
|
2199
|
-
continue;
|
|
2200
|
-
}
|
|
2201
|
-
const start = call.getStart();
|
|
2202
|
-
const end = call.getEnd();
|
|
2203
|
-
if (code.slice(start, end) !== call.getText()) {
|
|
2204
|
-
skipped.push({
|
|
2205
|
-
name,
|
|
2206
|
-
reason: "no-call-expression",
|
|
2207
|
-
start: 0,
|
|
2208
|
-
end: 0
|
|
2209
|
-
});
|
|
2210
|
-
continue;
|
|
2211
|
-
}
|
|
2212
|
-
const rangeKey = `${start}:${end}`;
|
|
2213
|
-
if (seenRanges.has(rangeKey)) continue;
|
|
2214
|
-
seenRanges.add(rangeKey);
|
|
2215
|
-
const rootName = calleeRootName(call);
|
|
2216
|
-
if (!rootName || !importsFor(call.getSourceFile()).has(rootName) || isShadowed(call, rootName)) {
|
|
2217
|
-
skipped.push({
|
|
2218
|
-
name,
|
|
2219
|
-
reason: "not-imported",
|
|
2220
|
-
start,
|
|
2221
|
-
end
|
|
2222
|
-
});
|
|
2223
|
-
continue;
|
|
2224
|
-
}
|
|
2225
|
-
const callee = ts_morph.Node.isCallExpression(call) ? call.getExpression() : void 0;
|
|
2226
|
-
const propertyName = ts_morph.Node.isPropertyAccessExpression(callee) ? callee.getNameNode().getText() : void 0;
|
|
2227
|
-
const wantsValue = type === "tokenValue";
|
|
2228
|
-
if (wantsValue !== (propertyName === "value")) {
|
|
2229
|
-
skipped.push({
|
|
2230
|
-
name,
|
|
2231
|
-
reason: "unsupported-kind",
|
|
2232
|
-
start,
|
|
2233
|
-
end
|
|
2234
|
-
});
|
|
2235
|
-
continue;
|
|
2236
|
-
}
|
|
2237
|
-
if (!wantsValue && propertyName !== void 0 && !ctx.imports.matchers.tokens.match(propertyName)) {
|
|
2238
|
-
skipped.push({
|
|
2239
|
-
name,
|
|
2240
|
-
reason: "unsupported-kind",
|
|
2241
|
-
start,
|
|
2242
|
-
end
|
|
2243
|
-
});
|
|
2244
|
-
continue;
|
|
2245
|
-
}
|
|
2246
|
-
if (!isStaticBox(item.box) || item.data.length !== 1) {
|
|
2247
|
-
skipped.push({
|
|
2248
|
-
name,
|
|
2249
|
-
reason: "dynamic",
|
|
2250
|
-
start,
|
|
2251
|
-
end
|
|
2252
|
-
});
|
|
2253
|
-
continue;
|
|
2254
|
-
}
|
|
2255
|
-
const path = item.data[0];
|
|
2256
|
-
if (typeof path !== "string") {
|
|
2257
|
-
skipped.push({
|
|
2258
|
-
name,
|
|
2259
|
-
reason: "dynamic",
|
|
2260
|
-
start,
|
|
2261
|
-
end
|
|
2262
|
-
});
|
|
2263
|
-
continue;
|
|
2264
|
-
}
|
|
2265
|
-
if (!(ts_morph.Node.isCallExpression(call) ? call.getArguments().slice(1) : []).every(isInertArgument)) {
|
|
2266
|
-
skipped.push({
|
|
2267
|
-
name,
|
|
2268
|
-
reason: "dynamic",
|
|
2269
|
-
start,
|
|
2270
|
-
end
|
|
2271
|
-
});
|
|
2272
|
-
continue;
|
|
2273
|
-
}
|
|
2274
|
-
const value = wantsValue ? runtimeTokenValue(path) : runtimeToken(path);
|
|
2275
|
-
if (!value) {
|
|
2276
|
-
skipped.push({
|
|
2277
|
-
name,
|
|
2278
|
-
reason: "unresolved-token",
|
|
2279
|
-
start,
|
|
2280
|
-
end
|
|
2281
|
-
});
|
|
2282
|
-
continue;
|
|
2283
|
-
}
|
|
2284
|
-
candidates.push({
|
|
2285
|
-
item,
|
|
2286
|
-
call,
|
|
2287
|
-
node: call,
|
|
2288
|
-
start,
|
|
2289
|
-
end,
|
|
2290
|
-
value
|
|
2291
|
-
});
|
|
2292
|
-
continue;
|
|
2293
|
-
}
|
|
2294
|
-
if (!FOLDABLE_TYPES.has(type)) {
|
|
2295
|
-
if (call && (type === RECIPE_CALL_TYPE || type === "recipe") && !isShadowed(call, name)) {
|
|
2296
|
-
const start = call.getStart();
|
|
2297
|
-
const end = call.getEnd();
|
|
2298
|
-
const rangeKey = `${start}:${end}`;
|
|
2299
|
-
if (!reportedRanges.has(rangeKey)) {
|
|
2300
|
-
reportedRanges.add(rangeKey);
|
|
2301
|
-
if (code.slice(start, end) !== call.getText()) {
|
|
2302
|
-
skipped.push({
|
|
2303
|
-
name,
|
|
2304
|
-
reason: "no-call-expression",
|
|
2305
|
-
start: 0,
|
|
2306
|
-
end: 0
|
|
2307
|
-
});
|
|
2308
|
-
continue;
|
|
2309
|
-
}
|
|
2310
|
-
if (isRawCall(call)) {
|
|
2311
|
-
skipped.push({
|
|
2312
|
-
name,
|
|
2313
|
-
reason: "raw-call",
|
|
2314
|
-
start,
|
|
2315
|
-
end
|
|
2316
|
-
});
|
|
2317
|
-
continue;
|
|
2318
|
-
}
|
|
2319
|
-
if (!recipeConfigs.has(name)) {
|
|
2320
|
-
if (type === "recipe") {
|
|
2321
|
-
const config = ctx.recipes.getConfig(name);
|
|
2322
|
-
if (config) {
|
|
2323
|
-
recipeConfigs.set(name, {
|
|
2324
|
-
config,
|
|
2325
|
-
box: void 0
|
|
2326
|
-
});
|
|
2327
|
-
helperModules.set(name, configRecipeCssSpecifier(call, name));
|
|
2328
|
-
}
|
|
2329
|
-
} else if (item.origin) {
|
|
2330
|
-
const imported = resolveImportedRecipe(call, name, item.origin);
|
|
2331
|
-
if (imported) recipeConfigs.set(name, imported);
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
const resolvedSelection = item.data?.length === 1 ? item.data[0] : void 0;
|
|
2335
|
-
const entry = recipeConfigs.get(name);
|
|
2336
|
-
let inlineSlot;
|
|
2337
|
-
let inlineEnd = end;
|
|
2338
|
-
if (Array.isArray(entry?.config.slots)) {
|
|
2339
|
-
const parent = call.getParent();
|
|
2340
|
-
if (ts_morph.Node.isPropertyAccessExpression(parent) && parent.getExpression() === call) {
|
|
2341
|
-
const accessed = parent.getName();
|
|
2342
|
-
if (entry.config.slots.includes(accessed)) {
|
|
2343
|
-
inlineSlot = accessed;
|
|
2344
|
-
inlineEnd = parent.getEnd();
|
|
2345
|
-
}
|
|
2346
|
-
} else if (ts_morph.Node.isElementAccessExpression(parent) && parent.getExpression() === call) {
|
|
2347
|
-
const argument = parent.getArgumentExpression();
|
|
2348
|
-
const accessed = argument && (ts_morph.Node.isStringLiteral(argument) || ts_morph.Node.isNoSubstitutionTemplateLiteral(argument)) ? argument.getLiteralValue() : void 0;
|
|
2349
|
-
if (typeof accessed === "string" && entry.config.slots.includes(accessed)) {
|
|
2350
|
-
inlineSlot = accessed;
|
|
2351
|
-
inlineEnd = parent.getEnd();
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
const lowered = lowerRecipeCall(call, entry, styleCompiler, isInertExpression, resolvedSelection, inlineSlot, maxRecipeStates);
|
|
2356
|
-
if (lowered.kind === "dynamic-style") {
|
|
2357
|
-
const helper = ensureRecipeHelperImport(RECIPE_MAP_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed, helperModules.get(name), helperModuleFromSubpath);
|
|
2358
|
-
if (helper) {
|
|
2359
|
-
candidates.push({
|
|
2360
|
-
item,
|
|
2361
|
-
call,
|
|
2362
|
-
node: call,
|
|
2363
|
-
start,
|
|
2364
|
-
end: inlineEnd,
|
|
2365
|
-
className: "",
|
|
2366
|
-
classNames: [],
|
|
2367
|
-
styleMap: lowered.map,
|
|
2368
|
-
mapHelperName: helper.name,
|
|
2369
|
-
insert: helper.insert,
|
|
2370
|
-
configBox: entry?.box,
|
|
2371
|
-
outputKind: lowered.map.outputKind === "slots" ? "slots" : void 0
|
|
2372
|
-
});
|
|
2373
|
-
continue;
|
|
2374
|
-
}
|
|
2375
|
-
skipped.push({
|
|
2376
|
-
name,
|
|
2377
|
-
reason: "recipe-call",
|
|
2378
|
-
start,
|
|
2379
|
-
end
|
|
2380
|
-
});
|
|
2381
|
-
continue;
|
|
2382
|
-
}
|
|
2383
|
-
if (lowered.kind === "slots") {
|
|
2384
|
-
const helper = lowered.helper ? ensureRecipeHelperImport(lowered.helper, call, isBambooCssModule, isGeneratedCssModule, isShadowed, helperModules.get(name), helperModuleFromSubpath) : void 0;
|
|
2385
|
-
if (!lowered.helper || helper) {
|
|
2386
|
-
const replacement = lowered.helper && helper && helper.name !== lowered.helper ? lowered.expression.replaceAll(`${lowered.helper}(`, `${helper.name}(`) : lowered.expression;
|
|
2387
|
-
candidates.push({
|
|
2388
|
-
item,
|
|
2389
|
-
call,
|
|
2390
|
-
node: call,
|
|
2391
|
-
start,
|
|
2392
|
-
end: inlineEnd,
|
|
2393
|
-
replacement,
|
|
2394
|
-
className: "",
|
|
2395
|
-
classNames: lowered.classNames,
|
|
2396
|
-
insert: helper?.insert,
|
|
2397
|
-
configBox: entry?.box,
|
|
2398
|
-
outputKind: "slots"
|
|
2399
|
-
});
|
|
2400
|
-
continue;
|
|
2401
|
-
}
|
|
2402
|
-
skipped.push({
|
|
2403
|
-
name,
|
|
2404
|
-
reason: "recipe-call",
|
|
2405
|
-
start,
|
|
2406
|
-
end
|
|
2407
|
-
});
|
|
2408
|
-
continue;
|
|
2409
|
-
}
|
|
2410
|
-
if (lowered.kind === "class") {
|
|
2411
|
-
candidates.push({
|
|
2412
|
-
item,
|
|
2413
|
-
call,
|
|
2414
|
-
node: call,
|
|
2415
|
-
start,
|
|
2416
|
-
end: inlineEnd,
|
|
2417
|
-
replacement: JSON.stringify(lowered.className),
|
|
2418
|
-
className: lowered.className,
|
|
2419
|
-
classNames: lowered.className.split(" ").filter(Boolean),
|
|
2420
|
-
styleSet: lowered.styles,
|
|
2421
|
-
configBox: entry?.box
|
|
2422
|
-
});
|
|
2423
|
-
continue;
|
|
2424
|
-
}
|
|
2425
|
-
skipped.push({
|
|
2426
|
-
name,
|
|
2427
|
-
reason: "recipe-call",
|
|
2428
|
-
start,
|
|
2429
|
-
end
|
|
2430
|
-
});
|
|
2431
|
-
}
|
|
2432
|
-
}
|
|
2433
|
-
continue;
|
|
2434
|
-
}
|
|
2435
|
-
if (!call) {
|
|
2436
|
-
skipped.push({
|
|
2437
|
-
name,
|
|
2438
|
-
reason: "no-call-expression",
|
|
2439
|
-
start: 0,
|
|
2440
|
-
end: 0
|
|
2441
|
-
});
|
|
2442
|
-
continue;
|
|
2443
|
-
}
|
|
2444
|
-
const start = call.getStart();
|
|
2445
|
-
const end = call.getEnd();
|
|
2446
|
-
const memberParent = call.getParent();
|
|
2447
|
-
const accessed = type === "recipe" && ts_morph.Node.isPropertyAccessExpression(memberParent) && memberParent.getExpression() === call ? memberParent : void 0;
|
|
2448
|
-
const accessedName = accessed?.getNameNode().getText();
|
|
2449
|
-
const declaredSlots = accessed ? ctx.recipes.getConfig(name)?.slots ?? [] : [];
|
|
2450
|
-
const memberAccess = accessedName && declaredSlots.includes(accessedName) ? accessed : void 0;
|
|
2451
|
-
const slot = memberAccess ? accessedName : void 0;
|
|
2452
|
-
const foldEnd = memberAccess ? memberAccess.getEnd() : end;
|
|
2453
|
-
if (code.slice(start, end) !== call.getText()) {
|
|
2454
|
-
skipped.push({
|
|
2455
|
-
name,
|
|
2456
|
-
reason: "no-call-expression",
|
|
2457
|
-
start: 0,
|
|
2458
|
-
end: 0
|
|
2459
|
-
});
|
|
2460
|
-
continue;
|
|
2461
|
-
}
|
|
2462
|
-
const rangeKey = `${start}:${foldEnd}`;
|
|
2463
|
-
if (seenRanges.has(rangeKey)) continue;
|
|
2464
|
-
seenRanges.add(rangeKey);
|
|
2465
|
-
if (isRawCall(call)) {
|
|
2466
|
-
skipped.push({
|
|
2467
|
-
name,
|
|
2468
|
-
reason: "raw-call",
|
|
2469
|
-
start,
|
|
2470
|
-
end
|
|
2471
|
-
});
|
|
2472
|
-
continue;
|
|
2473
|
-
}
|
|
2474
|
-
const rootName = calleeRootName(call);
|
|
2475
|
-
if (!rootName || !importsFor(call.getSourceFile()).has(rootName) || isShadowed(call, rootName)) {
|
|
2476
|
-
skipped.push({
|
|
2477
|
-
name,
|
|
2478
|
-
reason: "not-imported",
|
|
2479
|
-
start,
|
|
2480
|
-
end
|
|
2481
|
-
});
|
|
2482
|
-
continue;
|
|
2483
|
-
}
|
|
2484
|
-
if (!(ts_morph.Node.isCallExpression(call) && call.getArguments().length === 0 && item.data.length === 1) && !isStaticBox(item.box) || !hasStyles(item.data) || !argumentsAccountedFor(call, item.box)) {
|
|
2485
|
-
skipped.push({
|
|
2486
|
-
name,
|
|
2487
|
-
reason: "dynamic",
|
|
2488
|
-
start,
|
|
2489
|
-
end
|
|
2490
|
-
});
|
|
2491
|
-
continue;
|
|
2492
|
-
}
|
|
2493
|
-
candidates.push({
|
|
2494
|
-
item,
|
|
2495
|
-
call,
|
|
2496
|
-
node: call,
|
|
2497
|
-
start,
|
|
2498
|
-
end: foldEnd,
|
|
2499
|
-
slot
|
|
2500
|
-
});
|
|
2501
|
-
}
|
|
2502
|
-
/**
|
|
2503
|
-
* Resolve every fully static candidate to symbolic declarations before allocating a class.
|
|
2504
|
-
*
|
|
2505
|
-
* The normal fold can wait until the rewrite loop to compute a class string. Semantic
|
|
2506
|
-
* composition cannot: an enclosing `cx()` needs the declarations of its arguments so it can
|
|
2507
|
-
* discard overridden values before any string exists.
|
|
2508
|
-
*/
|
|
2509
|
-
{
|
|
2510
|
-
for (const candidate of candidates) {
|
|
2511
|
-
if (candidate.styleSet || candidate.value !== void 0 || candidate.replacement) continue;
|
|
2512
|
-
const { item } = candidate;
|
|
2513
|
-
if (item.type === "css") {
|
|
2514
|
-
candidate.styleSet = styleCompiler.compose(...item.data);
|
|
2515
|
-
continue;
|
|
2516
|
-
}
|
|
2517
|
-
if (item.type === "pattern") {
|
|
2518
|
-
candidate.styleSet = styleCompiler.compose(...item.data.map((entry) => ctx.patterns.transform(item.name ?? "", entry)));
|
|
2519
|
-
continue;
|
|
2520
|
-
}
|
|
2521
|
-
if (item.type === "viewTransition") {
|
|
2522
|
-
const semantic = (0, _bamboocss_shared.viewTransitionClassName)(item.data[0], ctx.utility.prefix);
|
|
2523
|
-
candidate.className = styleCompiler.allocateClassString(semantic);
|
|
2524
|
-
candidate.classNames = [candidate.className];
|
|
2525
|
-
candidate.replacement = JSON.stringify(candidate.className);
|
|
2526
|
-
continue;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile();
|
|
2530
|
-
if (sourceFile) {
|
|
2531
|
-
const cxBindings = /* @__PURE__ */ new Set();
|
|
2532
|
-
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
2533
|
-
if (declaration.isTypeOnly() || !isBambooCssModule(declaration.getModuleSpecifierValue())) continue;
|
|
2534
|
-
for (const named of declaration.getNamedImports()) {
|
|
2535
|
-
if (named.isTypeOnly() || named.getNameNode().getText() !== "cx") continue;
|
|
2536
|
-
cxBindings.add((named.getAliasNode() ?? named.getNameNode()).getText());
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
const byRange = new Map(candidates.map((candidate) => [`${candidate.start}:${candidate.end}`, candidate]));
|
|
2540
|
-
for (const call of cxBindings.size ? sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.CallExpression) : []) {
|
|
2541
|
-
const callee = call.getExpression();
|
|
2542
|
-
if (!ts_morph.Node.isIdentifier(callee) || !cxBindings.has(callee.getText()) || isShadowed(call, callee.getText())) continue;
|
|
2543
|
-
const matched = [];
|
|
2544
|
-
const parts = [];
|
|
2545
|
-
const dynamic = [];
|
|
2546
|
-
const constantCandidates = [];
|
|
2547
|
-
let supported = true;
|
|
2548
|
-
const take = (arg) => {
|
|
2549
|
-
const candidate = byRange.get(`${arg.getStart()}:${arg.getEnd()}`);
|
|
2550
|
-
if (candidate?.styleMap?.outputKind === "class") {
|
|
2551
|
-
dynamic.push(candidate);
|
|
2552
|
-
parts.push({
|
|
2553
|
-
kind: "dynamic",
|
|
2554
|
-
candidate
|
|
2555
|
-
});
|
|
2556
|
-
return true;
|
|
2557
|
-
}
|
|
2558
|
-
if (candidate?.styleSet) {
|
|
2559
|
-
matched.push(candidate);
|
|
2560
|
-
parts.push({
|
|
2561
|
-
kind: "style",
|
|
2562
|
-
candidate
|
|
2563
|
-
});
|
|
2564
|
-
return true;
|
|
2565
|
-
}
|
|
2566
|
-
if (candidate?.item.type === "viewTransition" && candidate.replacement && candidate.className) {
|
|
2567
|
-
constantCandidates.push(candidate);
|
|
2568
|
-
parts.push({
|
|
2569
|
-
kind: "class",
|
|
2570
|
-
value: candidate.className,
|
|
2571
|
-
candidate
|
|
2572
|
-
});
|
|
2573
|
-
return true;
|
|
2574
|
-
}
|
|
2575
|
-
if (ts_morph.Node.isStringLiteral(arg) || ts_morph.Node.isNoSubstitutionTemplateLiteral(arg)) {
|
|
2576
|
-
parts.push({
|
|
2577
|
-
kind: "class",
|
|
2578
|
-
value: arg.getLiteralValue()
|
|
2579
|
-
});
|
|
2580
|
-
return true;
|
|
2581
|
-
}
|
|
2582
|
-
if (ts_morph.Node.isArrayLiteralExpression(arg)) {
|
|
2583
|
-
for (const element of arg.getElements()) if (ts_morph.Node.isSpreadElement(element) || !take(element)) return false;
|
|
2584
|
-
return true;
|
|
2585
|
-
}
|
|
2586
|
-
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;
|
|
2587
|
-
return false;
|
|
2588
|
-
};
|
|
2589
|
-
for (const arg of call.getArguments()) {
|
|
2590
|
-
if (take(arg)) continue;
|
|
2591
|
-
supported = false;
|
|
2592
|
-
break;
|
|
2593
|
-
}
|
|
2594
|
-
if (dynamic.length > 1) {
|
|
2595
|
-
skipped.push({
|
|
2596
|
-
name: "cx",
|
|
2597
|
-
reason: "dynamic",
|
|
2598
|
-
start: call.getStart(),
|
|
2599
|
-
end: call.getEnd()
|
|
2600
|
-
});
|
|
2601
|
-
continue;
|
|
2602
|
-
}
|
|
2603
|
-
if (!supported) {
|
|
2604
|
-
skipped.push({
|
|
2605
|
-
name: "cx",
|
|
2606
|
-
reason: "dynamic",
|
|
2607
|
-
start: call.getStart(),
|
|
2608
|
-
end: call.getEnd()
|
|
2609
|
-
});
|
|
2610
|
-
continue;
|
|
2611
|
-
}
|
|
2612
|
-
if (dynamic.length === 1 && matched.length > 0) {
|
|
2613
|
-
const dynamicCandidate = dynamic[0];
|
|
2614
|
-
const styleParts = parts.filter((part) => part.kind !== "class");
|
|
2615
|
-
const dynamicIndex = styleParts.findIndex((part) => part.kind === "dynamic");
|
|
2616
|
-
const before = styleParts.slice(0, dynamicIndex).filter((part) => part.kind === "style").map((part) => part.candidate.styleSet);
|
|
2617
|
-
const after = styleParts.slice(dynamicIndex + 1).filter((part) => part.kind === "style").map((part) => part.candidate.styleSet);
|
|
2618
|
-
const compiled = dynamicCandidate.styleMap.compile(before, after);
|
|
2619
|
-
const expression = compiled.usesHelper && dynamicCandidate.mapHelperName && dynamicCandidate.mapHelperName !== "cvaMap" ? compiled.expression.replaceAll(`${RECIPE_MAP_HELPER}(`, `${dynamicCandidate.mapHelperName}(`) : compiled.expression;
|
|
2620
|
-
const arguments_ = [];
|
|
2621
|
-
let wroteCompiled = false;
|
|
2622
|
-
for (const part of parts) {
|
|
2623
|
-
if (part.kind === "class") {
|
|
2624
|
-
if (part.value) arguments_.push(JSON.stringify(part.value));
|
|
2625
|
-
continue;
|
|
2626
|
-
}
|
|
2627
|
-
if (!wroteCompiled) {
|
|
2628
|
-
arguments_.push(expression);
|
|
2629
|
-
wroteCompiled = true;
|
|
2630
|
-
}
|
|
2631
|
-
}
|
|
2632
|
-
dynamicCandidate.subsumed = true;
|
|
2633
|
-
const first = styleParts[0].candidate;
|
|
2634
|
-
candidates.push({
|
|
2635
|
-
...first,
|
|
2636
|
-
call,
|
|
2637
|
-
node: call,
|
|
2638
|
-
start: call.getStart(),
|
|
2639
|
-
end: call.getEnd(),
|
|
2640
|
-
displayName: "cx",
|
|
2641
|
-
replacement: arguments_.length === 1 ? arguments_[0] : `${callee.getText()}(${arguments_.join(", ")})`,
|
|
2642
|
-
className: "",
|
|
2643
|
-
classNames: [...compiled.classNames, ...parts.filter((part) => part.kind === "class").flatMap((part) => part.value.split(" "))].filter(Boolean),
|
|
2644
|
-
styleSet: void 0,
|
|
2645
|
-
styleMap: void 0,
|
|
2646
|
-
outputKind: void 0,
|
|
2647
|
-
insert: compiled.usesHelper ? dynamicCandidate.insert : void 0,
|
|
2648
|
-
sourceBoxes: styleParts.flatMap((part) => [part.candidate.item.box, part.candidate.configBox]).concat(constantCandidates.map((candidate) => candidate.item.box)).filter(Boolean)
|
|
2649
|
-
});
|
|
2650
|
-
continue;
|
|
2651
|
-
}
|
|
2652
|
-
if (matched.length === 0) {
|
|
2653
|
-
if (constantCandidates.length === 0) continue;
|
|
2654
|
-
const className = parts.filter((part) => part.kind === "class").map((part) => part.value).filter(Boolean).join(" ");
|
|
2655
|
-
const first = constantCandidates[0];
|
|
2656
|
-
candidates.push({
|
|
2657
|
-
...first,
|
|
2658
|
-
call,
|
|
2659
|
-
node: call,
|
|
2660
|
-
start: call.getStart(),
|
|
2661
|
-
end: call.getEnd(),
|
|
2662
|
-
displayName: "cx",
|
|
2663
|
-
replacement: JSON.stringify(className),
|
|
2664
|
-
className,
|
|
2665
|
-
classNames: className.split(" ").filter(Boolean),
|
|
2666
|
-
sourceBoxes: constantCandidates.map((candidate) => candidate.item.box).filter(Boolean)
|
|
2667
|
-
});
|
|
2668
|
-
continue;
|
|
2669
|
-
}
|
|
2670
|
-
const merged = styleCompiler.compose(...matched.map((candidate) => candidate.styleSet));
|
|
2671
|
-
const compiled = styleCompiler.className(merged);
|
|
2672
|
-
const classParts = [];
|
|
2673
|
-
let wroteCompiled = false;
|
|
2674
|
-
for (const part of parts) {
|
|
2675
|
-
if (part.kind === "class") {
|
|
2676
|
-
if (part.value) classParts.push(part.value);
|
|
2677
|
-
continue;
|
|
2678
|
-
}
|
|
2679
|
-
if (!wroteCompiled && compiled) {
|
|
2680
|
-
classParts.push(compiled);
|
|
2681
|
-
wroteCompiled = true;
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
const first = matched[0];
|
|
2685
|
-
candidates.push({
|
|
2686
|
-
...first,
|
|
2687
|
-
call,
|
|
2688
|
-
node: call,
|
|
2689
|
-
start: call.getStart(),
|
|
2690
|
-
end: call.getEnd(),
|
|
2691
|
-
displayName: "cx",
|
|
2692
|
-
replacement: JSON.stringify(classParts.join(" ")),
|
|
2693
|
-
className: classParts.join(" "),
|
|
2694
|
-
classNames: classParts.flatMap((part) => part.split(" ")).filter(Boolean),
|
|
2695
|
-
styleSet: merged,
|
|
2696
|
-
sourceBoxes: [...matched.flatMap((candidate) => [candidate.item.box, candidate.configBox]), ...constantCandidates.map((candidate) => candidate.item.box)].filter(Boolean)
|
|
2697
|
-
});
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
for (const candidate of candidates) {
|
|
2701
|
-
if (!candidate.styleMap || candidate.subsumed || candidate.replacement) continue;
|
|
2702
|
-
const compiled = candidate.styleMap.compile();
|
|
2703
|
-
candidate.replacement = compiled.usesHelper && candidate.mapHelperName && candidate.mapHelperName !== "cvaMap" ? compiled.expression.replaceAll(`${RECIPE_MAP_HELPER}(`, `${candidate.mapHelperName}(`) : compiled.expression;
|
|
2704
|
-
if (!compiled.usesHelper) candidate.insert = void 0;
|
|
2705
|
-
candidate.className = compiled.staticClasses;
|
|
2706
|
-
candidate.classNames = compiled.classNames;
|
|
2707
|
-
candidate.outputKind = compiled.outputKind === "slots" ? "slots" : void 0;
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
|
-
/**
|
|
2711
|
-
* Ranges the rewrite actually replaced. Declared before the early return below, because
|
|
2712
|
-
* that return is now also a reporting point: a module with nothing to fold is exactly the
|
|
2713
|
-
* shape `reportSurvivors` exists to catch.
|
|
2714
|
-
*/
|
|
2715
|
-
const applied = [];
|
|
2716
|
-
if (candidates.length === 0 && recipeDefinitions.length === 0) {
|
|
2717
|
-
if (reportSurvivors) reportRuntimeBindings();
|
|
2718
|
-
return {
|
|
2719
|
-
code,
|
|
2720
|
-
map: null,
|
|
2721
|
-
folded,
|
|
2722
|
-
skipped,
|
|
2723
|
-
dependencies: []
|
|
2724
|
-
};
|
|
2725
|
-
}
|
|
2726
|
-
const rewriteSourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
|
|
2727
|
-
if (!rewriteSourceFile) return {
|
|
2728
|
-
code,
|
|
2729
|
-
map: null,
|
|
2730
|
-
folded,
|
|
2731
|
-
skipped,
|
|
2732
|
-
dependencies: []
|
|
2733
|
-
};
|
|
2734
|
-
const dependencyScan = createDependencyScan(rewriteSourceFile);
|
|
2735
|
-
candidates.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
2736
|
-
const magic = new magic_string.default(code);
|
|
2737
|
-
const insertedNames = /* @__PURE__ */ new Set();
|
|
2738
|
-
const applyInsert = (insert) => {
|
|
2739
|
-
if (!insert) return;
|
|
2740
|
-
const missing = insert.names.filter((name) => !insertedNames.has(name));
|
|
2741
|
-
if (!missing.length) return;
|
|
2742
|
-
magic.appendLeft(insert.pos, insert.module ? `\nimport { ${missing.join(", ")} } from '${insert.module}'` : missing.map((name) => `, ${name}`).join(""));
|
|
2743
|
-
for (const name of missing) insertedNames.add(name);
|
|
2744
|
-
};
|
|
2745
|
-
const collides = (edits) => edits.some(([start, end]) => applied.some(([from, to]) => start < to && from < end));
|
|
2746
|
-
for (const candidate of candidates) {
|
|
2747
|
-
const { item, start, end } = candidate;
|
|
2748
|
-
const name = candidate.displayName ?? item.name ?? item.type ?? "";
|
|
2749
|
-
const ranges = [[start, end]];
|
|
2750
|
-
if (collides(ranges)) {
|
|
2751
|
-
skipped.push({
|
|
2752
|
-
name,
|
|
2753
|
-
reason: "overlapping",
|
|
2754
|
-
start,
|
|
2755
|
-
end
|
|
2756
|
-
});
|
|
2757
|
-
continue;
|
|
2758
|
-
}
|
|
2759
|
-
if (candidate.value !== void 0) {
|
|
2760
|
-
magic.overwrite(start, end, JSON.stringify(candidate.value));
|
|
2761
|
-
applied.push(...ranges);
|
|
2762
|
-
folded.push({
|
|
2763
|
-
name,
|
|
2764
|
-
kind: "value",
|
|
2765
|
-
className: "",
|
|
2766
|
-
classNames: [],
|
|
2767
|
-
value: candidate.value,
|
|
2768
|
-
start,
|
|
2769
|
-
end
|
|
2770
|
-
});
|
|
2771
|
-
collectSourceFiles(item.box, dependencyScan);
|
|
2772
|
-
continue;
|
|
2773
|
-
}
|
|
2774
|
-
if (candidate.replacement) {
|
|
2775
|
-
magic.overwrite(start, end, candidate.replacement);
|
|
2776
|
-
applyInsert(candidate.insert);
|
|
2777
|
-
applied.push(...ranges);
|
|
2778
|
-
folded.push({
|
|
2779
|
-
name,
|
|
2780
|
-
kind: candidate.outputKind ?? "class",
|
|
2781
|
-
className: candidate.className,
|
|
2782
|
-
classNames: (candidate.classNames ?? [candidate.className]).filter(Boolean),
|
|
2783
|
-
start,
|
|
2784
|
-
end
|
|
2785
|
-
});
|
|
2786
|
-
collectSourceFiles(item.box, dependencyScan);
|
|
2787
|
-
if (candidate.configBox) collectSourceFiles(candidate.configBox, dependencyScan);
|
|
2788
|
-
for (const box of candidate.sourceBoxes ?? []) collectSourceFiles(box, dependencyScan);
|
|
2789
|
-
continue;
|
|
2790
|
-
}
|
|
2791
|
-
let className;
|
|
2792
|
-
try {
|
|
2793
|
-
if (item.type === "pattern") className = runtimeCss(...item.data.map((entry) => ctx.patterns.transform(name, entry)));
|
|
2794
|
-
else className = runtimeCss(...item.data);
|
|
2795
|
-
} catch {
|
|
2796
|
-
skipped.push({
|
|
2797
|
-
name,
|
|
2798
|
-
reason: "dynamic",
|
|
2799
|
-
start,
|
|
2800
|
-
end
|
|
2801
|
-
});
|
|
2802
|
-
continue;
|
|
2803
|
-
}
|
|
2804
|
-
magic.overwrite(start, end, JSON.stringify(className));
|
|
2805
|
-
applied.push(...ranges);
|
|
2806
|
-
folded.push({
|
|
2807
|
-
name,
|
|
2808
|
-
kind: "class",
|
|
2809
|
-
className,
|
|
2810
|
-
classNames: className ? [className] : [],
|
|
2811
|
-
start,
|
|
2812
|
-
end
|
|
2813
|
-
});
|
|
2814
|
-
collectSourceFiles(item.box, dependencyScan);
|
|
2815
|
-
}
|
|
2816
|
-
const recipeSourceFile = candidates[0]?.node.getSourceFile();
|
|
2817
|
-
if (recipeSourceFile && mayNameSplitVariantProps(recipeSourceFile.getFullText())) for (const access of recipeSourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.PropertyAccessExpression)) {
|
|
2818
|
-
if (access.getName() !== "splitVariantProps") continue;
|
|
2819
|
-
const target = access.getExpression();
|
|
2820
|
-
if (!ts_morph.Node.isIdentifier(target)) continue;
|
|
2821
|
-
if (isShadowed(access, target.getText())) continue;
|
|
2822
|
-
const local = recipeConfigs?.get(target.getText());
|
|
2823
|
-
const importedConfig = !local && importsFor(recipeSourceFile).has(target.getText()) ? ctx.recipes.getConfig(target.getText()) : void 0;
|
|
2824
|
-
const entry = local && local !== AMBIGUOUS ? local : importedConfig ? {
|
|
2825
|
-
config: importedConfig,
|
|
2826
|
-
box: void 0
|
|
2827
|
-
} : void 0;
|
|
2828
|
-
if (!entry) continue;
|
|
2829
|
-
const call = access.getParent();
|
|
2830
|
-
if (!ts_morph.Node.isCallExpression(call) || call.getExpression() !== access) continue;
|
|
2831
|
-
const args = call.getArguments();
|
|
2832
|
-
if (args.length !== 1) continue;
|
|
2833
|
-
const start = call.getStart();
|
|
2834
|
-
const end = call.getEnd();
|
|
2835
|
-
if (code.slice(start, end) !== call.getText()) continue;
|
|
2836
|
-
if (collides([[start, end]])) continue;
|
|
2837
|
-
const helper = ensureRecipeHelperImport(SPLIT_PROPS_HELPER, call, isBambooCssModule, isGeneratedCssModule, isShadowed, helperModules.get(target.getText()), helperModuleFromSubpath);
|
|
2838
|
-
if (!helper) continue;
|
|
2839
|
-
const keys = Object.keys(entry.config.variants ?? {});
|
|
2840
|
-
magic.overwrite(start, end, `${helper.name}(${args[0].getText()}, ${JSON.stringify(keys)})`);
|
|
2841
|
-
applyInsert(helper.insert);
|
|
2842
|
-
applied.push([start, end]);
|
|
2843
|
-
}
|
|
2844
|
-
for (const { name, call } of recipeDefinitions) {
|
|
2845
|
-
const start = call.getStart();
|
|
2846
|
-
const end = call.getEnd();
|
|
2847
|
-
if (collides([[start, end]])) continue;
|
|
2848
|
-
magic.overwrite(start, end, "undefined");
|
|
2849
|
-
applied.push([start, end]);
|
|
2850
|
-
folded.push({
|
|
2851
|
-
name,
|
|
2852
|
-
kind: "definition",
|
|
2853
|
-
className: "",
|
|
2854
|
-
classNames: [],
|
|
2855
|
-
start,
|
|
2856
|
-
end
|
|
2857
|
-
});
|
|
2858
|
-
}
|
|
2859
|
-
/**
|
|
2860
|
-
* Bindings from a bamboo module still referenced once every rewrite is applied.
|
|
2861
|
-
*
|
|
2862
|
-
* Deliberately not driven by `parserResult`: that is the recogniser, and the point here is
|
|
2863
|
-
* to catch what it did not see. A namespace import called as `s.cva(...)`, a default
|
|
2864
|
-
* import, a specifier that resolved to nothing — each leaves a live reference and no ledger
|
|
2865
|
-
* entry at all, which used to let a build silently ship the engine.
|
|
2866
|
-
*
|
|
2867
|
-
* The helpers the compiler writes are excluded because they pull no style engine. `cx` is
|
|
2868
|
-
* also allowed to remain when it joins an arbitrary external class; only fully analyzable
|
|
2869
|
-
* arguments receive Bamboo's semantic composition guarantee.
|
|
2870
|
-
*/
|
|
2871
|
-
/** The specifier this module imported `binding` through, if it did. */
|
|
2872
|
-
function importSpecifierFor(sourceFile, binding) {
|
|
2873
|
-
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
2874
|
-
if (declaration.isTypeOnly()) continue;
|
|
2875
|
-
for (const named of declaration.getNamedImports()) {
|
|
2876
|
-
if (named.isTypeOnly()) continue;
|
|
2877
|
-
if ((named.getAliasNode() ?? named.getNameNode()).getText() === binding) return named.getAliasNode() ?? named.getNameNode();
|
|
2878
|
-
}
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
function reportRuntimeBindings() {
|
|
2882
|
-
const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
|
|
2883
|
-
if (!sourceFile) return;
|
|
2884
|
-
const importedRecipeBindings = new Set(parserResult.importedRecipes?.keys() ?? []);
|
|
2885
|
-
/**
|
|
2886
|
-
* Every identifier in the module, grouped by the name it spells — built at most once per
|
|
2887
|
-
* pass, and only when something below has a name to look up.
|
|
2888
|
-
*
|
|
2889
|
-
* One index answers for every binding, rather than a walk per binding. Built here rather
|
|
2890
|
-
* than cached across passes: it holds nodes, and a node does not outlive its source file
|
|
2891
|
-
* being replaced.
|
|
2892
|
-
*
|
|
2893
|
-
* Deferred because most modules in an app neither declare nor import a recipe, and the
|
|
2894
|
-
* walk is not cheap — `getDescendantsOfKind` wraps every identifier in the file in a
|
|
2895
|
-
* ts-morph node to answer a question those modules never ask. It was 11% of a 6,307-file
|
|
2896
|
-
* build, most of it spent producing an index nothing read.
|
|
2897
|
-
*/
|
|
2898
|
-
let identifiers;
|
|
2899
|
-
const identifiersByName = () => identifiers ??= identifierIndex(sourceFile);
|
|
2900
|
-
for (const binding of new Set([...recipeConfigs.keys(), ...importedRecipeBindings])) {
|
|
2901
|
-
const entry = recipeConfigs.get(binding);
|
|
2902
|
-
if (entry === AMBIGUOUS) continue;
|
|
2903
|
-
if (!entry && !importedRecipeBindings.has(binding)) continue;
|
|
2904
|
-
const definition = entry?.box?.getNode?.();
|
|
2905
|
-
const nameNode = definition?.getSourceFile() === sourceFile ? definition?.getFirstAncestorByKind(ts_morph.SyntaxKind.VariableDeclaration)?.getNameNode() : importSpecifierFor(sourceFile, binding);
|
|
2906
|
-
if (!nameNode || !ts_morph.Node.isIdentifier(nameNode)) continue;
|
|
2907
|
-
const references = localReferencesTo(identifiersByName(), binding, nameNode);
|
|
2908
|
-
if (skipped.filter((item) => SURVIVES_TO_RUNTIME.has(item.reason) && item.end > item.start).some((item) => references.some((ref) => ref.getStart() >= item.start && ref.getStart() < item.end))) continue;
|
|
2909
|
-
const survivor = references.find((ref) => !applied.some(([from, to]) => ref.getStart() >= from && ref.getStart() < to));
|
|
2910
|
-
if (!survivor) continue;
|
|
2911
|
-
skipped.push({
|
|
2912
|
-
name: binding,
|
|
2913
|
-
reason: "runtime-binding",
|
|
2914
|
-
start: survivor.getStart(),
|
|
2915
|
-
end: survivor.getEnd()
|
|
2916
|
-
});
|
|
2917
|
-
}
|
|
2918
|
-
const bambooModules = [
|
|
2919
|
-
...cssModules,
|
|
2920
|
-
...ctx.imports.matchers.recipe?.mods ?? [],
|
|
2921
|
-
...ctx.imports.matchers.pattern?.mods ?? [],
|
|
2922
|
-
...ctx.imports.matchers.tokens?.mods ?? []
|
|
2923
|
-
];
|
|
2924
|
-
const runtimeCalls = [];
|
|
2925
|
-
const importEquals = [];
|
|
2926
|
-
sourceFile.forEachDescendant((node) => {
|
|
2927
|
-
if (ts_morph.Node.isCallExpression(node)) runtimeCalls.push(node);
|
|
2928
|
-
else if (ts_morph.Node.isImportEqualsDeclaration(node)) importEquals.push(node);
|
|
2929
|
-
});
|
|
2930
|
-
for (const call of runtimeCalls) {
|
|
2931
|
-
const callee = call.getExpression();
|
|
2932
|
-
const argument = call.getArguments()[0];
|
|
2933
|
-
if (!argument || !ts_morph.Node.isStringLiteral(argument) && !ts_morph.Node.isNoSubstitutionTemplateLiteral(argument)) continue;
|
|
2934
|
-
if (!matchesModule(argument.getLiteralValue(), bambooModules)) continue;
|
|
2935
|
-
const isDynamicImport = callee.getKind() === ts_morph.SyntaxKind.ImportKeyword;
|
|
2936
|
-
const isRequire = ts_morph.Node.isIdentifier(callee) && callee.getText() === "require" && !isShadowed(call, "require");
|
|
2937
|
-
if (!isDynamicImport && !isRequire) continue;
|
|
2938
|
-
skipped.push({
|
|
2939
|
-
name: isDynamicImport ? "import" : "require",
|
|
2940
|
-
reason: "runtime-binding",
|
|
2941
|
-
start: call.getStart(),
|
|
2942
|
-
end: call.getEnd()
|
|
2943
|
-
});
|
|
2944
|
-
}
|
|
2945
|
-
for (const declaration of importEquals) {
|
|
2946
|
-
if (declaration.isTypeOnly()) continue;
|
|
2947
|
-
const reference = declaration.getModuleReference();
|
|
2948
|
-
if (!ts_morph.Node.isExternalModuleReference(reference)) continue;
|
|
2949
|
-
const expression = reference.getExpression();
|
|
2950
|
-
if (!expression || !ts_morph.Node.isStringLiteral(expression) || !matchesModule(expression.getLiteralValue(), bambooModules)) continue;
|
|
2951
|
-
skipped.push({
|
|
2952
|
-
name: declaration.getName(),
|
|
2953
|
-
reason: "runtime-binding",
|
|
2954
|
-
start: declaration.getStart(),
|
|
2955
|
-
end: declaration.getEnd()
|
|
2956
|
-
});
|
|
2957
|
-
}
|
|
2958
|
-
/** Local name -> what to call it in the report. */
|
|
2959
|
-
const watched = /* @__PURE__ */ new Map();
|
|
2960
|
-
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
2961
|
-
if (declaration.isTypeOnly()) continue;
|
|
2962
|
-
if (!matchesModule(declaration.getModuleSpecifierValue(), bambooModules)) continue;
|
|
2963
|
-
for (const named of declaration.getNamedImports()) {
|
|
2964
|
-
if (named.isTypeOnly()) continue;
|
|
2965
|
-
const imported = named.getNameNode().getText();
|
|
2966
|
-
if (PERMITTED_BINDINGS.has(imported)) continue;
|
|
2967
|
-
watched.set((named.getAliasNode() ?? named.getNameNode()).getText(), imported);
|
|
2968
|
-
}
|
|
2969
|
-
const namespace = declaration.getNamespaceImport();
|
|
2970
|
-
if (namespace) watched.set(namespace.getText(), `${namespace.getText()}.*`);
|
|
2971
|
-
const defaultImport = declaration.getDefaultImport();
|
|
2972
|
-
if (defaultImport) watched.set(defaultImport.getText(), defaultImport.getText());
|
|
2973
|
-
}
|
|
2974
|
-
for (const declaration of sourceFile.getExportDeclarations()) {
|
|
2975
|
-
if (declaration.isTypeOnly()) continue;
|
|
2976
|
-
if (!matchesModule(declaration.getModuleSpecifierValue() ?? "", bambooModules)) continue;
|
|
2977
|
-
if (declaration.isNamespaceExport()) {
|
|
2978
|
-
skipped.push({
|
|
2979
|
-
name: declaration.getNamespaceExport()?.getName() ?? "*",
|
|
2980
|
-
reason: "runtime-binding",
|
|
2981
|
-
start: declaration.getStart(),
|
|
2982
|
-
end: declaration.getEnd()
|
|
2983
|
-
});
|
|
2984
|
-
continue;
|
|
404
|
+
},
|
|
405
|
+
resolveId(id) {
|
|
406
|
+
const query = queryOf(id);
|
|
407
|
+
const base = id.slice(0, id.length - query.length);
|
|
408
|
+
if (base !== "virtual:bamboo.css" && base !== RESOLVED_ID) return null;
|
|
409
|
+
return `${RESOLVED_ID}${query}`;
|
|
410
|
+
},
|
|
411
|
+
async load(id) {
|
|
412
|
+
const query = queryOf(id);
|
|
413
|
+
if (id.slice(0, id.length - query.length) !== RESOLVED_ID) return null;
|
|
414
|
+
session.cssLoaded = true;
|
|
415
|
+
const generationAtStart = changeGeneration;
|
|
416
|
+
if (command === "serve" && servedCss?.generation === generationAtStart) {
|
|
417
|
+
if (this.addWatchFile) for (const file of extractedSourceFiles()) this.addWatchFile(file);
|
|
418
|
+
return servedCss.css;
|
|
2985
419
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
const
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
});
|
|
420
|
+
let css;
|
|
421
|
+
try {
|
|
422
|
+
const validateDevCss = command === "serve" ? (await loadCssOutput()).pruneStaticCss : void 0;
|
|
423
|
+
const first = prebuilt;
|
|
424
|
+
prebuilt = void 0;
|
|
425
|
+
css = await (first ?? generate());
|
|
426
|
+
if (validateDevCss) css = validateDevCss(css, session, { prune: false });
|
|
427
|
+
} catch (error) {
|
|
428
|
+
throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
|
|
2996
429
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
430
|
+
if (command === "serve" && generationAtStart === changeGeneration) servedCss = {
|
|
431
|
+
generation: generationAtStart,
|
|
432
|
+
css
|
|
433
|
+
};
|
|
434
|
+
if (this.addWatchFile) for (const file of extractedSourceFiles()) this.addWatchFile(file);
|
|
435
|
+
return css;
|
|
436
|
+
},
|
|
437
|
+
configureServer(devServer) {
|
|
438
|
+
server = devServer;
|
|
439
|
+
/**
|
|
440
|
+
* The graph the stylesheet's own module lives in, which is the one that has to reach it.
|
|
441
|
+
*
|
|
442
|
+
* `load` registers every extracted file with `addWatchFile`, and `vite:css-analysis`
|
|
443
|
+
* turns those into real importer edges — the virtual module ends up a direct importer of
|
|
444
|
+
* each file the extractor read. So an edit to any of them propagates to the stylesheet on
|
|
445
|
+
* Vite's own pass, in whichever environment holds that edge.
|
|
446
|
+
*
|
|
447
|
+
* The client one, because CSS is a client concern: an ssr environment never applies a
|
|
448
|
+
* stylesheet update, and asking whether *any* environment matched would skip the forced
|
|
449
|
+
* reload below for a server-only module whose styles the client still has to be told
|
|
450
|
+
* about. Vite 5 has one graph and no `environments`, where the question is exact.
|
|
451
|
+
*/
|
|
452
|
+
const clientGraph = devServer.environments?.client?.moduleGraph ?? devServer.moduleGraph;
|
|
453
|
+
const invalidate = (file) => {
|
|
454
|
+
const ctx = builder?.context;
|
|
455
|
+
if (!ctx) return;
|
|
456
|
+
const absoluteFile = ctx.runtime.path.abs(ctx.config.cwd, file);
|
|
457
|
+
if (!session.extractedFiles.has(absoluteFile)) return;
|
|
458
|
+
changeGeneration++;
|
|
459
|
+
prebuilt = void 0;
|
|
460
|
+
const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
|
|
461
|
+
if (!mod) return;
|
|
462
|
+
if (clientGraph.getModulesByFile(absoluteFile)?.size) return;
|
|
463
|
+
server?.moduleGraph.invalidateModule(mod);
|
|
464
|
+
server?.reloadModule(mod);
|
|
465
|
+
_bamboocss_logger.logger.debug("vite", `styles invalidated by ${absoluteFile}`);
|
|
466
|
+
};
|
|
467
|
+
devServer.watcher.on("change", invalidate);
|
|
468
|
+
devServer.watcher.on("add", invalidate);
|
|
469
|
+
devServer.watcher.on("unlink", invalidate);
|
|
470
|
+
},
|
|
471
|
+
generateBundle: {
|
|
472
|
+
order: "post",
|
|
473
|
+
async handler(outputOptions, bundle) {
|
|
474
|
+
const { containsGeneratedCssAsset, optimizeStaticCssAssets } = await loadCssOutputModule();
|
|
475
|
+
const environment = this.environment;
|
|
476
|
+
const environmentName = environment?.name ?? "default";
|
|
477
|
+
const replacesGeneratedStylesheet = containsGeneratedCssAsset(bundle);
|
|
478
|
+
const outputProjection = session.beginOutputProjection(environmentName, outputOptions, bundle, replacesGeneratedStylesheet);
|
|
479
|
+
try {
|
|
480
|
+
/**
|
|
481
|
+
* Pruned against what this environment compiled, without waiting for the rest.
|
|
482
|
+
*
|
|
483
|
+
* The stylesheet is emitted and finalized by the environment that *imports* it, which
|
|
484
|
+
* in an SSR app is the client — and the client builds first, before the server
|
|
485
|
+
* environment has transformed a single module. Waiting for a complete answer therefore
|
|
486
|
+
* meant never pruning at all in any SSR framework: react-router, Remix, Nuxt, SvelteKit
|
|
487
|
+
* and Qwik all build the client first, and the client's output is on disk before the
|
|
488
|
+
* server environment starts. That is most production apps, and the feature was inert in
|
|
489
|
+
* every one of them — silently, since a build with nothing to prune looks identical.
|
|
490
|
+
*
|
|
491
|
+
* The reason for waiting was real: a class only the server graph reaches is not in this
|
|
492
|
+
* environment's reachability set, so pruning here removes rules the server-rendered
|
|
493
|
+
* markup still names. What makes it safe to prune anyway is that the mistake is
|
|
494
|
+
* *detectable* rather than silent — `buildEnd` in `plugin.ts` intersects every later
|
|
495
|
+
* environment's compiled classes against `prunedClasses` and fails the build naming
|
|
496
|
+
* them. A styled component that only ever renders on the server is the shape that
|
|
497
|
+
* trips it, and `pruneCss: false` is the answer when it does.
|
|
498
|
+
*
|
|
499
|
+
* So the trade is deliberate: a loud build failure in the rare case, in exchange for
|
|
500
|
+
* the feature working at all in the common one. It is the same reasoning as the
|
|
501
|
+
* unimported-`virtual:bamboo.css` check — a class with no rule behind it must never
|
|
502
|
+
* leave the build quietly.
|
|
503
|
+
*/
|
|
504
|
+
const pending = remainingEnvironments(session);
|
|
505
|
+
if (pending.length) _bamboocss_logger.logger.debug("vite", `Pruning against the ${JSON.stringify(environment?.name ?? "default")} environment with ${(0, _bamboocss_shared.truncateList)(pending, {
|
|
506
|
+
unit: "environment",
|
|
507
|
+
separator: ", "
|
|
508
|
+
})} still to compile. A class only those reach fails the build rather than shipping without its rule.`);
|
|
509
|
+
const { sheets } = optimizeStaticCssAssets(bundle, session, {
|
|
510
|
+
environment: environmentName,
|
|
511
|
+
prune: pruneCss,
|
|
512
|
+
requiredClasses: outputProjection.requiredClasses,
|
|
513
|
+
sourcemap: environment?.config?.build?.sourcemap
|
|
514
|
+
});
|
|
515
|
+
if (sheets && !pruneCss) _bamboocss_logger.logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
|
|
516
|
+
if (!outputProjection.cssLoaded) return;
|
|
517
|
+
if (!session.transformedFiles.size) return;
|
|
518
|
+
/**
|
|
519
|
+
* An SSR bundle emits no CSS assets, and is not supposed to.
|
|
520
|
+
*
|
|
521
|
+
* `build.ssrEmitAssets` is off by default, so Vite discards them: the client build is
|
|
522
|
+
* what carries the stylesheet, and a server bundle that imports `virtual:bamboo.css`
|
|
523
|
+
* from shared code — a root component, a layout — still asks this plugin to load it.
|
|
524
|
+
* Which means the environment *served* the sheet and then emitted nothing, and the
|
|
525
|
+
* check below read that as the failure it exists to catch.
|
|
526
|
+
*
|
|
527
|
+
* It fails a build that is entirely correct. Qwik's `vite build --ssr` is the shape
|
|
528
|
+
* that showed it: 7/7 calls compiled, the client bundle carrying the stylesheet, and
|
|
529
|
+
* the server bundle refusing to finish. React Router does not hit it only because its
|
|
530
|
+
* plugin turns `ssrEmitAssets` on.
|
|
531
|
+
*
|
|
532
|
+
* Read per environment where that exists, falling back to the run's own config, so
|
|
533
|
+
* Vite 5's single-config builds are answered by the same question.
|
|
534
|
+
*/
|
|
535
|
+
const buildOptions = environment?.config?.build ?? ssrBuildOptions;
|
|
536
|
+
if (buildOptions?.ssr && !buildOptions.ssrEmitAssets) return;
|
|
537
|
+
if (!replacesGeneratedStylesheet) throw new Error(`bamboocss: ${session.transformedFiles.size} module(s) were compiled to Bamboo class values, but no emitted asset carries the generated stylesheet. The build would ship unstyled.\n\nThis happens when another plugin, or the bundler itself, drops or replaces the CSS asset after it is emitted. If you are on Rolldown, report this — the rename that used to cause it is already disabled there. Otherwise look for a plugin running in \`generateBundle\` that rewrites CSS assets.`);
|
|
538
|
+
} finally {
|
|
539
|
+
outputProjection.restore();
|
|
540
|
+
}
|
|
3010
541
|
}
|
|
3011
542
|
}
|
|
3012
|
-
if (watched.size === 0) return;
|
|
3013
|
-
const declined = skipped.filter((entry) => SURVIVES_TO_RUNTIME.has(entry.reason) && entry.end > entry.start).map((entry) => [entry.start, entry.end]);
|
|
3014
|
-
const survivors = [];
|
|
3015
|
-
for (const [local, imported] of watched) for (const identifier of identifiersByName().get(local)) {
|
|
3016
|
-
const start = identifier.getStart();
|
|
3017
|
-
if (identifier.getFirstAncestorByKind(ts_morph.SyntaxKind.ImportDeclaration)) continue;
|
|
3018
|
-
if (applied.some(([from, to]) => start >= from && start < to)) continue;
|
|
3019
|
-
if (declined.some(([from, to]) => start >= from && start < to)) continue;
|
|
3020
|
-
if (!isValueReference(identifier)) continue;
|
|
3021
|
-
if (isShadowed(identifier, local)) continue;
|
|
3022
|
-
survivors.push({
|
|
3023
|
-
name: imported,
|
|
3024
|
-
reason: "runtime-binding",
|
|
3025
|
-
start,
|
|
3026
|
-
end: identifier.getEnd()
|
|
3027
|
-
});
|
|
3028
|
-
break;
|
|
3029
|
-
}
|
|
3030
|
-
survivors.sort((a, b) => a.start - b.start);
|
|
3031
|
-
skipped.push(...survivors);
|
|
3032
|
-
}
|
|
3033
|
-
if (reportSurvivors) reportRuntimeBindings();
|
|
3034
|
-
if (folded.length === 0) return {
|
|
3035
|
-
code,
|
|
3036
|
-
map: null,
|
|
3037
|
-
folded,
|
|
3038
|
-
skipped,
|
|
3039
|
-
dependencies: []
|
|
3040
|
-
};
|
|
3041
|
-
return {
|
|
3042
|
-
code: magic.toString(),
|
|
3043
|
-
map: magic.generateMap({
|
|
3044
|
-
source: options.filePath,
|
|
3045
|
-
hires: true,
|
|
3046
|
-
includeContent: true
|
|
3047
|
-
}),
|
|
3048
|
-
folded,
|
|
3049
|
-
skipped,
|
|
3050
|
-
dependencies: [...dependencyScan.results, ...foreignDependencies]
|
|
3051
|
-
};
|
|
3052
|
-
};
|
|
3053
|
-
//#endregion
|
|
3054
|
-
//#region src/style-set.ts
|
|
3055
|
-
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3056
|
-
/** A compound selector matches only through variant classes the recipe actually emits. */
|
|
3057
|
-
const matchesCompound = (compound, selection, variants) => {
|
|
3058
|
-
for (const [key, expected] of Object.entries(compound)) {
|
|
3059
|
-
if (key === "css") continue;
|
|
3060
|
-
const declared = variants?.[key];
|
|
3061
|
-
const selected = selection[key];
|
|
3062
|
-
if (selected == null || !declared || !Object.hasOwn(declared, String(selected))) return false;
|
|
3063
|
-
if (!(Array.isArray(expected) ? expected : [expected]).some((value) => value != null && String(selected) === String(value))) return false;
|
|
3064
|
-
}
|
|
3065
|
-
return true;
|
|
3066
|
-
};
|
|
3067
|
-
/**
|
|
3068
|
-
* Resolve the style fragments one recipe call contributes, in emitted-rule precedence.
|
|
3069
|
-
*
|
|
3070
|
-
* This intentionally rejects conditional variant *selections*. A scalar selects a style
|
|
3071
|
-
* object; an object such as `{ base: 'sm', md: 'lg' }` selects several objects under
|
|
3072
|
-
* conditions and needs a separate lowering. Returning `undefined` rejects that call instead
|
|
3073
|
-
* of silently compiling only one branch.
|
|
3074
|
-
*/
|
|
3075
|
-
const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (className) => className) => {
|
|
3076
|
-
const { mergeCssUncached } = (0, _bamboocss_shared.createMergeCss)(createCssContext(ctx));
|
|
3077
|
-
const compose = (...styles) => mergeCssUncached(...styles);
|
|
3078
|
-
const resolveRecipe = (config, input = {}, slot) => {
|
|
3079
|
-
const slots = Array.isArray(config.slots) ? config.slots : void 0;
|
|
3080
|
-
if (Boolean(slots) !== Boolean(slot)) return void 0;
|
|
3081
|
-
if (slot && !slots?.includes(slot)) return void 0;
|
|
3082
|
-
const selection = {
|
|
3083
|
-
...config.defaultVariants ?? {},
|
|
3084
|
-
...(0, _bamboocss_shared.compact)(input)
|
|
3085
|
-
};
|
|
3086
|
-
if (Object.values(selection).some((value) => isRecord(value))) return void 0;
|
|
3087
|
-
const fragments = [];
|
|
3088
|
-
const take = (candidate) => {
|
|
3089
|
-
if (!isRecord(candidate)) return;
|
|
3090
|
-
const styles = slot ? candidate[slot] : candidate;
|
|
3091
|
-
if (isRecord(styles)) fragments.push(styles);
|
|
3092
|
-
};
|
|
3093
|
-
take(config.base);
|
|
3094
|
-
for (const variant of Object.keys(config.variants ?? {})) {
|
|
3095
|
-
const value = selection[variant];
|
|
3096
|
-
if (value == null) continue;
|
|
3097
|
-
take(config.variants?.[variant]?.[String(value)]);
|
|
3098
|
-
}
|
|
3099
|
-
for (const compound of config.compoundVariants ?? []) {
|
|
3100
|
-
if (!isRecord(compound) || !matchesCompound(compound, selection, config.variants)) continue;
|
|
3101
|
-
take(compound.css);
|
|
3102
|
-
}
|
|
3103
|
-
return compose(...fragments);
|
|
3104
|
-
};
|
|
3105
|
-
return {
|
|
3106
|
-
compose,
|
|
3107
|
-
resolveRecipe,
|
|
3108
|
-
className: (...styles) => runtimeCss(...styles),
|
|
3109
|
-
allocateClassString
|
|
3110
543
|
};
|
|
3111
544
|
};
|
|
3112
545
|
//#endregion
|
|
3113
546
|
//#region src/plugin.ts
|
|
3114
547
|
const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
|
|
3115
548
|
const NODE_MODULES = /node_modules/;
|
|
549
|
+
const TRANSFORM_META_KEY = "bamboocss:transform";
|
|
550
|
+
const TRANSFORM_ARTIFACT_VERSION = 3;
|
|
3116
551
|
/**
|
|
3117
552
|
* Queries that make Vite serve something other than the module's own source.
|
|
3118
553
|
*
|
|
@@ -3213,46 +648,181 @@ const formatSkipped = (id, skipped) => {
|
|
|
3213
648
|
*/
|
|
3214
649
|
const bamboocss = (options = {}) => {
|
|
3215
650
|
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
|
|
3216
|
-
(0,
|
|
651
|
+
(0, _bamboocss_node_static_compiler.markStaticCompilerActive)();
|
|
3217
652
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
3218
653
|
if ("renameCssAsset" in options) throw new Error("bamboocss: `renameCssAsset` has been replaced by `pruneCss`. Use `pruneCss: false` for what `renameCssAsset: false` did — it always disabled the pruning as well, since pruned bytes under the unpruned sheet's name is what lets a CDN serve a stale stylesheet. The new name says which of the two it is really about.");
|
|
654
|
+
const staticSession = createStaticCompilationSession();
|
|
655
|
+
const transformArtifactIntegrityKey = (0, node_crypto.randomBytes)(32);
|
|
656
|
+
const serializeTransformArtifact = (environment, artifact) => JSON.stringify([
|
|
657
|
+
TRANSFORM_META_KEY,
|
|
658
|
+
environment,
|
|
659
|
+
artifact.version,
|
|
660
|
+
artifact.moduleId,
|
|
661
|
+
artifact.file,
|
|
662
|
+
artifact.folded,
|
|
663
|
+
artifact.skipped.map(([reason, count]) => [reason, count]),
|
|
664
|
+
artifact.survivors.map(({ line, name, reason }) => [
|
|
665
|
+
line,
|
|
666
|
+
name,
|
|
667
|
+
reason
|
|
668
|
+
]),
|
|
669
|
+
artifact.transformedFile,
|
|
670
|
+
[...artifact.classNames],
|
|
671
|
+
[...artifact.dependencies],
|
|
672
|
+
artifact.signature ? [
|
|
673
|
+
artifact.signature.input,
|
|
674
|
+
artifact.signature.output,
|
|
675
|
+
artifact.signature.path
|
|
676
|
+
] : null
|
|
677
|
+
]);
|
|
678
|
+
const transformArtifactIntegrity = (environment, artifact) => (0, node_crypto.createHmac)("sha256", transformArtifactIntegrityKey).update(serializeTransformArtifact(environment, artifact)).digest("base64url");
|
|
679
|
+
const sealTransformArtifact = (environment, artifact) => ({
|
|
680
|
+
...artifact,
|
|
681
|
+
integrity: transformArtifactIntegrity(environment, artifact)
|
|
682
|
+
});
|
|
683
|
+
const skipReasons = new Set([
|
|
684
|
+
"dynamic",
|
|
685
|
+
"raw-call",
|
|
686
|
+
"recipe-call",
|
|
687
|
+
"unsupported-kind",
|
|
688
|
+
"not-imported",
|
|
689
|
+
"no-call-expression",
|
|
690
|
+
"overlapping",
|
|
691
|
+
"unresolved-token",
|
|
692
|
+
"runtime-binding",
|
|
693
|
+
"compile-failed"
|
|
694
|
+
]);
|
|
695
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
696
|
+
const isNonNegativeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
697
|
+
const isPositiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
|
|
698
|
+
const isTransformArtifact = (value) => {
|
|
699
|
+
if (!isRecord(value) || value.version !== TRANSFORM_ARTIFACT_VERSION) return false;
|
|
700
|
+
if (typeof value.moduleId !== "string" || typeof value.file !== "string") return false;
|
|
701
|
+
if (!isNonNegativeInteger(value.folded) || typeof value.transformedFile !== "boolean") return false;
|
|
702
|
+
if (typeof value.integrity !== "string" || !/^[\w-]{43}$/.test(value.integrity)) return false;
|
|
703
|
+
if (!Array.isArray(value.classNames) || !value.classNames.every((entry) => typeof entry === "string")) return false;
|
|
704
|
+
if (!Array.isArray(value.dependencies) || !value.dependencies.every((entry) => typeof entry === "string")) return false;
|
|
705
|
+
if (!Array.isArray(value.skipped) || !value.skipped.every((entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && skipReasons.has(entry[0]) && isPositiveInteger(entry[1]))) return false;
|
|
706
|
+
if (!Array.isArray(value.survivors) || !value.survivors.every((entry) => isRecord(entry) && Number.isSafeInteger(entry.line) && entry.line >= 1 && typeof entry.name === "string" && typeof entry.reason === "string" && skipReasons.has(entry.reason))) return false;
|
|
707
|
+
if (value.signature === void 0) return true;
|
|
708
|
+
return isRecord(value.signature) && typeof value.signature.input === "string" && typeof value.signature.output === "string" && value.signature.path === value.file;
|
|
709
|
+
};
|
|
710
|
+
const hasValidTransformArtifactIntegrity = (environment, artifact) => {
|
|
711
|
+
const expected = Buffer.from(transformArtifactIntegrity(environment, artifact));
|
|
712
|
+
const actual = Buffer.from(artifact.integrity);
|
|
713
|
+
return actual.length === expected.length && (0, node_crypto.timingSafeEqual)(actual, expected);
|
|
714
|
+
};
|
|
715
|
+
const cachedArtifactError = (id, environment, value, snapshotProblem) => {
|
|
716
|
+
let problem;
|
|
717
|
+
if (snapshotProblem) problem = snapshotProblem;
|
|
718
|
+
else if (!isRecord(value) || value.version !== TRANSFORM_ARTIFACT_VERSION) problem = isRecord(value) && (typeof value.version === "number" || typeof value.version === "string") ? `uses version ${JSON.stringify(value.version)}; expected schema version ${TRANSFORM_ARTIFACT_VERSION}` : `is malformed and has no valid version; expected schema version ${TRANSFORM_ARTIFACT_VERSION}`;
|
|
719
|
+
else if (!isTransformArtifact(value)) problem = `is malformed for schema version ${TRANSFORM_ARTIFACT_VERSION}`;
|
|
720
|
+
else if (value.moduleId !== id || value.file !== id.split("?")[0]) problem = `does not belong to this module id and physical file`;
|
|
721
|
+
else if (!hasValidTransformArtifactIntegrity(environment, value)) problem = `failed its schema version ${TRANSFORM_ARTIFACT_VERSION} integrity check`;
|
|
722
|
+
else problem = `could not be validated for schema version ${TRANSFORM_ARTIFACT_VERSION}`;
|
|
723
|
+
return /* @__PURE__ */ new Error(`bamboocss: cached transform metadata for ${JSON.stringify(id)} in the ${JSON.stringify(environment)} environment ${problem}.\n\nBamboo cannot safely rebuild from this entry because cached JavaScript may still name CSS classes whose rules would be dropped. Restart Vite to invalidate its in-memory transform cache. If this persists, clear Vite's cache directory and rebuild.`);
|
|
724
|
+
};
|
|
725
|
+
const transformStateByEnvironment = /* @__PURE__ */ new Map();
|
|
3219
726
|
/**
|
|
3220
|
-
*
|
|
3221
|
-
* goes, because a build has more than one environment and they share most of their modules.
|
|
727
|
+
* One fold per file content per change event, shared across environments and hooks.
|
|
3222
728
|
*
|
|
3223
|
-
*
|
|
3224
|
-
*
|
|
3225
|
-
*
|
|
3226
|
-
*
|
|
3227
|
-
*
|
|
729
|
+
* A single edit folds the same bytes repeatedly: `hotUpdate` provisionally re-folds every
|
|
730
|
+
* dependent once per environment to decide what to invalidate, then `transform` folds the
|
|
731
|
+
* edited module for the client graph, again for SSR, and once more for each update a
|
|
732
|
+
* framework re-drives — react-router's server-change trigger calls `reloadModule` per pass.
|
|
733
|
+
* All of them read the same shared ts-morph project under the same config, so the result is
|
|
734
|
+
* a function of the bytes alone and the repeats were pure cost: on the app this was measured
|
|
735
|
+
* on, four transforms of a 46 kB route module per edit, ~10 ms each.
|
|
3228
736
|
*
|
|
3229
|
-
*
|
|
3230
|
-
*
|
|
3231
|
-
*
|
|
3232
|
-
*
|
|
3233
|
-
*
|
|
737
|
+
* `watchChange` clears it, which is the exact validity window: entries are correct until a
|
|
738
|
+
* file event changes what a fold could resolve, and `watchChange` is the one hook Vite calls
|
|
739
|
+
* for every such event before any update work begins. The per-environment resolution closure
|
|
740
|
+
* is deliberately not memoized — `withResolutionClosure` is recomputed per consumer against
|
|
741
|
+
* that environment's own recorded dependencies.
|
|
742
|
+
*
|
|
743
|
+
* Keyed by path *and* content digest, not path alone, because one physical file is served
|
|
744
|
+
* as more than one module shape in the same event — react-router clips a route module down
|
|
745
|
+
* to its route exports for the client graph while SSR gets the full file — and a last-write
|
|
746
|
+
* key would make the two shapes evict each other on every pass.
|
|
747
|
+
*
|
|
748
|
+
* Dev only, like the verdict memo it sits beside: a build transforms each module once per
|
|
749
|
+
* environment with no bracketing events, and holding every module's fold for the length of a
|
|
750
|
+
* build is memory a one-shot pass has no reason to spend.
|
|
3234
751
|
*/
|
|
3235
|
-
const
|
|
3236
|
-
const
|
|
752
|
+
const foldMemoByContent = /* @__PURE__ */ new Map();
|
|
753
|
+
const foldMemoKey = (filePath, inputDigest) => `${filePath}\0${inputDigest}`;
|
|
3237
754
|
/**
|
|
3238
|
-
*
|
|
755
|
+
* The Project resolution walk `withResolutionClosure` runs, memoized per change event.
|
|
3239
756
|
*
|
|
3240
|
-
*
|
|
3241
|
-
*
|
|
3242
|
-
*
|
|
3243
|
-
*
|
|
757
|
+
* The walk is the dominant per-dependent cost once the fold itself is memoized — it runs
|
|
758
|
+
* once per dependent per environment with identical inputs, since both environments record
|
|
759
|
+
* the same fold dependencies for byte-identical source. Same bracketing as the fold memo:
|
|
760
|
+
* `watchChange` clears it, so no entry outlives the project state it was computed against.
|
|
3244
761
|
*/
|
|
3245
|
-
const
|
|
3246
|
-
|
|
3247
|
-
const
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
const
|
|
3254
|
-
|
|
762
|
+
const resolutionClosureMemo = /* @__PURE__ */ new Map();
|
|
763
|
+
/** The immutable generation currently occupying each configured output on disk. */
|
|
764
|
+
const liveOutputSlotsByEnvironment = /* @__PURE__ */ new Map();
|
|
765
|
+
/** Graphs which passed `buildEnd`, but whose output has not succeeded yet. */
|
|
766
|
+
const preparedGenerations = /* @__PURE__ */ new Map();
|
|
767
|
+
/** Success accumulated across hosts which run one buildStart/buildEnd pair per output. */
|
|
768
|
+
const pendingOutputCycles = /* @__PURE__ */ new Map();
|
|
769
|
+
/** Last configured output whose render phase actually began, for missing-marker recovery. */
|
|
770
|
+
const lastStartedOutputSlotByEnvironment = /* @__PURE__ */ new Map();
|
|
771
|
+
const nextBuildSerialByEnvironment = /* @__PURE__ */ new Map();
|
|
772
|
+
const observedBuildSerialByEnvironment = /* @__PURE__ */ new Map();
|
|
773
|
+
const buildSerialByState = /* @__PURE__ */ new WeakMap();
|
|
774
|
+
/** Output finalizers installed by the `options` hook for the next generation. */
|
|
775
|
+
const outputTokensByEnvironment = /* @__PURE__ */ new Map();
|
|
776
|
+
const OUTPUT_FINALIZER = Symbol("bamboocss-output-finalizer");
|
|
777
|
+
const OUTPUT_START_MARKER = Symbol("bamboocss-output-start-marker");
|
|
778
|
+
const DIRECT_OUTPUT_TOKEN = 0;
|
|
779
|
+
let nextOutputToken = 0;
|
|
780
|
+
let nextEpochId = 0;
|
|
781
|
+
const outputIdentityByBundle = /* @__PURE__ */ new WeakMap();
|
|
782
|
+
const outputIdentityByOptions = /* @__PURE__ */ new WeakMap();
|
|
783
|
+
const outputStageByBundle = /* @__PURE__ */ new WeakMap();
|
|
784
|
+
const outputStageByOptions = /* @__PURE__ */ new WeakMap();
|
|
785
|
+
const environmentOf = (context) => context.environment;
|
|
786
|
+
const environmentName = (context) => environmentOf(context)?.name ?? "default";
|
|
787
|
+
const newEnvironmentState = () => ({
|
|
788
|
+
transformArtifactsByModule: /* @__PURE__ */ new Map(),
|
|
789
|
+
dependentsByDependency: /* @__PURE__ */ new Map(),
|
|
790
|
+
dependenciesByModule: /* @__PURE__ */ new Map(),
|
|
791
|
+
filesByModule: /* @__PURE__ */ new Map(),
|
|
792
|
+
foldSignatures: /* @__PURE__ */ new Map(),
|
|
793
|
+
recipeConfigCache: /* @__PURE__ */ new Map(),
|
|
794
|
+
transformedModulesThisRun: /* @__PURE__ */ new Set(),
|
|
795
|
+
unchangedFolds: /* @__PURE__ */ new Map(),
|
|
796
|
+
changedRun: 0,
|
|
797
|
+
cssLoaded: false
|
|
798
|
+
});
|
|
799
|
+
const cloneEnvironmentState = (state) => ({
|
|
800
|
+
transformArtifactsByModule: new Map(state.transformArtifactsByModule),
|
|
801
|
+
dependentsByDependency: new Map([...state.dependentsByDependency].map(([dependency, dependents]) => [dependency, new Set(dependents)])),
|
|
802
|
+
dependenciesByModule: new Map([...state.dependenciesByModule].map(([moduleId, dependencies]) => [moduleId, new Set(dependencies)])),
|
|
803
|
+
filesByModule: new Map(state.filesByModule),
|
|
804
|
+
foldSignatures: new Map(state.foldSignatures),
|
|
805
|
+
recipeConfigCache: new Map(state.recipeConfigCache),
|
|
806
|
+
transformedModulesThisRun: new Set(state.transformedModulesThisRun),
|
|
807
|
+
unchangedFolds: new Map(state.unchangedFolds),
|
|
808
|
+
changedRun: state.changedRun,
|
|
809
|
+
cssLoaded: state.cssLoaded
|
|
810
|
+
});
|
|
811
|
+
const environmentState = (context) => {
|
|
812
|
+
const identity = environmentName(context);
|
|
813
|
+
let state = transformStateByEnvironment.get(identity);
|
|
814
|
+
if (!state) {
|
|
815
|
+
state = newEnvironmentState();
|
|
816
|
+
transformStateByEnvironment.set(identity, state);
|
|
817
|
+
}
|
|
818
|
+
return state;
|
|
3255
819
|
};
|
|
820
|
+
const allSurvivors = (states) => [...states].flatMap((state) => [...state.transformArtifactsByModule.values()].flatMap((artifact) => artifact.survivors.map(({ line, name, reason }) => ({
|
|
821
|
+
file: artifact.file,
|
|
822
|
+
line,
|
|
823
|
+
name,
|
|
824
|
+
reason
|
|
825
|
+
}))));
|
|
3256
826
|
const createSurvivorError = (entries) => {
|
|
3257
827
|
const byFile = /* @__PURE__ */ new Map();
|
|
3258
828
|
for (const entry of entries) {
|
|
@@ -3269,13 +839,6 @@ const bamboocss = (options = {}) => {
|
|
|
3269
839
|
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" : "") + (entries.some((entry) => entry.reason === "runtime-binding") ? "`runtime-binding` is a Bamboo value read rather than called. An inline `cva`/`sva` declaration is erased, so its binding is `undefined` at runtime: calling it compiles, including from another module, but reading the value itself — `const alias = badge`, `badge.raw(...)`, re-exporting it — has nothing behind it. The location given is the read to change, not the declaration.\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`.\n\nSet `BAMBOO_DIAGNOSTIC_LIMIT=all` to list every finding rather than the first few.");
|
|
3270
840
|
};
|
|
3271
841
|
/**
|
|
3272
|
-
* Recipe configs read out of modules other than the one being transformed.
|
|
3273
|
-
*
|
|
3274
|
-
* Per build rather than per module: a recipe declared once and imported by fifty components
|
|
3275
|
-
* would otherwise re-parse its module fifty times, which is the transform path.
|
|
3276
|
-
*/
|
|
3277
|
-
const recipeConfigCache = /* @__PURE__ */ new Map();
|
|
3278
|
-
/**
|
|
3279
842
|
* Which modules folded a value read out of which other module, for dev invalidation.
|
|
3280
843
|
*
|
|
3281
844
|
* `addWatchFile` reports the same edges, and in a build that is enough — Rollup discards a
|
|
@@ -3300,26 +863,27 @@ const bamboocss = (options = {}) => {
|
|
|
3300
863
|
* Keyed by dependency, since "what changed" is the question asked, and tracked in the other
|
|
3301
864
|
* direction as well so a re-transform can retract edges the module no longer has.
|
|
3302
865
|
*/
|
|
3303
|
-
const
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
const next = new Set(dependencies.map(normalizeFsPath).filter((dependency) => dependency !==
|
|
3307
|
-
const previous =
|
|
866
|
+
const recordFoldDependencies = (state, moduleId, file, dependencies) => {
|
|
867
|
+
const { dependenciesByModule, dependentsByDependency, filesByModule } = state;
|
|
868
|
+
const normalizedFile = normalizeFsPath(file);
|
|
869
|
+
const next = new Set(dependencies.map(normalizeFsPath).filter((dependency) => dependency !== normalizedFile));
|
|
870
|
+
const previous = dependenciesByModule.get(moduleId);
|
|
871
|
+
filesByModule.set(moduleId, file);
|
|
3308
872
|
for (const dependency of previous ?? []) {
|
|
3309
873
|
if (next.has(dependency)) continue;
|
|
3310
874
|
const dependents = dependentsByDependency.get(dependency);
|
|
3311
|
-
if (!dependents?.delete(
|
|
875
|
+
if (!dependents?.delete(moduleId)) continue;
|
|
3312
876
|
if (!dependents.size) dependentsByDependency.delete(dependency);
|
|
3313
877
|
}
|
|
3314
878
|
if (!next.size) {
|
|
3315
|
-
|
|
879
|
+
dependenciesByModule.delete(moduleId);
|
|
3316
880
|
return;
|
|
3317
881
|
}
|
|
3318
|
-
|
|
882
|
+
dependenciesByModule.set(moduleId, next);
|
|
3319
883
|
for (const dependency of next) {
|
|
3320
884
|
const dependents = dependentsByDependency.get(dependency);
|
|
3321
|
-
if (dependents) dependents.add(
|
|
3322
|
-
else dependentsByDependency.set(dependency, new Set([
|
|
885
|
+
if (dependents) dependents.add(moduleId);
|
|
886
|
+
else dependentsByDependency.set(dependency, new Set([moduleId]));
|
|
3323
887
|
}
|
|
3324
888
|
};
|
|
3325
889
|
/**
|
|
@@ -3333,7 +897,7 @@ const bamboocss = (options = {}) => {
|
|
|
3333
897
|
* Digests, not text. Retaining every consumer's source and compiled output for the life of the
|
|
3334
898
|
* process is the same order of memory as the ts-morph project already holding it; two 44-byte
|
|
3335
899
|
* strings per entry is not, and it does not grow with module size. The set is bounded the way
|
|
3336
|
-
* `
|
|
900
|
+
* `dependenciesByModule` is — an entry exists only while a module's fold actually reads another
|
|
3337
901
|
* file, which most modules never do — so a project that folds nothing across a boundary pays
|
|
3338
902
|
* neither the bytes nor the hashing.
|
|
3339
903
|
*
|
|
@@ -3343,16 +907,343 @@ const bamboocss = (options = {}) => {
|
|
|
3343
907
|
* that comparison and is treated as changed. `path` is the spelling `transform` used, because
|
|
3344
908
|
* ts-morph and the fold both key on it and Windows spells it more than one way.
|
|
3345
909
|
*/
|
|
3346
|
-
const foldSignatures = /* @__PURE__ */ new Map();
|
|
3347
910
|
const digest = (text) => (0, node_crypto.createHash)("sha256").update(text).digest("base64");
|
|
911
|
+
/** Fingerprint the generated Bamboo assets after every plugin which may rewrite the bundle. */
|
|
912
|
+
const bambooCssDigest = (bundle) => {
|
|
913
|
+
const assets = [];
|
|
914
|
+
for (const output of Object.values(bundle)) {
|
|
915
|
+
if (!isRecord(output) || output.type !== "asset") continue;
|
|
916
|
+
const { source } = output;
|
|
917
|
+
if (typeof source !== "string" && !(source instanceof Uint8Array)) continue;
|
|
918
|
+
const text = typeof source === "string" ? source : Buffer.from(source).toString();
|
|
919
|
+
if (!text.includes("--made-with-bamboo")) continue;
|
|
920
|
+
assets.push(text);
|
|
921
|
+
}
|
|
922
|
+
if (!assets.length) return void 0;
|
|
923
|
+
return digest(JSON.stringify(assets.sort()));
|
|
924
|
+
};
|
|
925
|
+
/** Add one detached transform contribution to the global reachability projection. */
|
|
926
|
+
const applyStaticTransformContribution = (artifact) => {
|
|
927
|
+
if (artifact.transformedFile) staticSession.transformedFiles.add((0, node_path.resolve)(artifact.file));
|
|
928
|
+
for (const className of artifact.classNames) staticSession.markClassUsed(className);
|
|
929
|
+
};
|
|
930
|
+
/**
|
|
931
|
+
* Re-derive the two global sets CSS pruning consumes from environment-owned artifacts.
|
|
932
|
+
*
|
|
933
|
+
* A watch run may rebuild one environment or several at once. The candidate being judged
|
|
934
|
+
* replaces its own previous contribution; every sibling contributes its committed generation,
|
|
935
|
+
* even when a replacement for that sibling is already in flight. Its old JavaScript remains
|
|
936
|
+
* the live output until the replacement succeeds, so pruning against a half-filled candidate
|
|
937
|
+
* would remove rules that output still names.
|
|
938
|
+
*/
|
|
939
|
+
const contributionStates = (candidateEnvironment, candidateState) => {
|
|
940
|
+
const states = [];
|
|
941
|
+
const seenEpochs = /* @__PURE__ */ new Set();
|
|
942
|
+
let candidateIncluded = false;
|
|
943
|
+
for (const [environment, slots] of liveOutputSlotsByEnvironment) if (environment === candidateEnvironment && candidateState) {
|
|
944
|
+
states.push(candidateState);
|
|
945
|
+
candidateIncluded = true;
|
|
946
|
+
} else for (const { epoch } of slots.values()) {
|
|
947
|
+
if (seenEpochs.has(epoch.id)) continue;
|
|
948
|
+
seenEpochs.add(epoch.id);
|
|
949
|
+
states.push(epoch.state);
|
|
950
|
+
}
|
|
951
|
+
if (candidateEnvironment && candidateState && !candidateIncluded) states.push(candidateState);
|
|
952
|
+
return states;
|
|
953
|
+
};
|
|
954
|
+
const rebuildStaticTransformContributions = (candidateEnvironment, candidateState) => {
|
|
955
|
+
staticSession.transformedFiles.clear();
|
|
956
|
+
staticSession.usedClasses.clear();
|
|
957
|
+
staticSession.cssLoaded = false;
|
|
958
|
+
for (const state of contributionStates(candidateEnvironment, candidateState)) {
|
|
959
|
+
if (state.cssLoaded) staticSession.cssLoaded = true;
|
|
960
|
+
for (const artifact of state.transformArtifactsByModule.values()) applyStaticTransformContribution(artifact);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
/**
|
|
964
|
+
* Snapshot which reported classes Bamboo actually extracted for this JavaScript generation.
|
|
965
|
+
*
|
|
966
|
+
* Fold artifacts also report literal classes passed through helpers such as `cx('external',
|
|
967
|
+
* css(...))`. Intersecting with the extraction inventory keeps those useful reachability facts
|
|
968
|
+
* without later demanding that Bamboo provide a rule it never owned.
|
|
969
|
+
*/
|
|
970
|
+
const ownedClassesForState = (state) => {
|
|
971
|
+
const extracted = new Map([...staticSession.prunableClasses].map((className) => [require_class_name.bare(className), className]));
|
|
972
|
+
const owned = /* @__PURE__ */ new Set();
|
|
973
|
+
for (const artifact of state.transformArtifactsByModule.values()) for (const reported of artifact.classNames) for (const token of reported.split(" ")) {
|
|
974
|
+
if (!token) continue;
|
|
975
|
+
const extractedClass = extracted.get(require_class_name.bare(token));
|
|
976
|
+
if (extractedClass !== void 0) owned.add(extractedClass);
|
|
977
|
+
}
|
|
978
|
+
return owned;
|
|
979
|
+
};
|
|
980
|
+
const createTransformEpoch = (state) => {
|
|
981
|
+
const detachedState = cloneEnvironmentState(state);
|
|
982
|
+
return {
|
|
983
|
+
id: ++nextEpochId,
|
|
984
|
+
state: detachedState,
|
|
985
|
+
ownedClasses: ownedClassesForState(detachedState)
|
|
986
|
+
};
|
|
987
|
+
};
|
|
988
|
+
/** Apply the same candidate-replaces-its-environment rule as the reachability projection. */
|
|
989
|
+
const requiredClassesForProjection = (candidateEnvironment, candidateOwnedClasses) => {
|
|
990
|
+
const required = /* @__PURE__ */ new Set();
|
|
991
|
+
let candidateIncluded = false;
|
|
992
|
+
const seenEpochs = /* @__PURE__ */ new Set();
|
|
993
|
+
for (const [environment, slots] of liveOutputSlotsByEnvironment) {
|
|
994
|
+
if (environment === candidateEnvironment) {
|
|
995
|
+
for (const className of candidateOwnedClasses) required.add(className);
|
|
996
|
+
candidateIncluded = true;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
for (const { epoch } of slots.values()) {
|
|
1000
|
+
if (seenEpochs.has(epoch.id)) continue;
|
|
1001
|
+
seenEpochs.add(epoch.id);
|
|
1002
|
+
for (const className of epoch.ownedClasses) required.add(className);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (!candidateIncluded) for (const className of candidateOwnedClasses) required.add(className);
|
|
1006
|
+
return required;
|
|
1007
|
+
};
|
|
1008
|
+
const currentRequiredClasses = () => {
|
|
1009
|
+
const prunable = new Set([...staticSession.prunableClasses].map(require_class_name.bare));
|
|
1010
|
+
return new Set([...staticSession.usedClasses].filter((className) => prunable.has(require_class_name.bare(className))));
|
|
1011
|
+
};
|
|
1012
|
+
/** Derive loss history from the stylesheet outputs which are still observable. */
|
|
1013
|
+
const rebuildLivePrunedClasses = () => {
|
|
1014
|
+
staticSession.prunedClasses.clear();
|
|
1015
|
+
for (const slots of liveOutputSlotsByEnvironment.values()) for (const slot of slots.values()) for (const className of slot.prunedClasses ?? []) staticSession.prunedClasses.add(className);
|
|
1016
|
+
};
|
|
1017
|
+
const observeEnvironmentBuildStart = (environment) => {
|
|
1018
|
+
const serial = (nextBuildSerialByEnvironment.get(environment) ?? 0) + 1;
|
|
1019
|
+
nextBuildSerialByEnvironment.set(environment, serial);
|
|
1020
|
+
observedBuildSerialByEnvironment.set(environment, serial);
|
|
1021
|
+
};
|
|
1022
|
+
/** Open a replacement without discarding the generation a failed rebuild can fall back to. */
|
|
1023
|
+
const beginEnvironmentGeneration = (environment) => {
|
|
1024
|
+
preparedGenerations.delete(environment);
|
|
1025
|
+
const state = newEnvironmentState();
|
|
1026
|
+
const observedSerial = observedBuildSerialByEnvironment.get(environment);
|
|
1027
|
+
const buildSerial = observedSerial ?? (nextBuildSerialByEnvironment.get(environment) ?? 0) + 1;
|
|
1028
|
+
if (observedSerial === void 0) nextBuildSerialByEnvironment.set(environment, buildSerial);
|
|
1029
|
+
else observedBuildSerialByEnvironment.delete(environment);
|
|
1030
|
+
buildSerialByState.set(state, buildSerial);
|
|
1031
|
+
transformStateByEnvironment.set(environment, state);
|
|
1032
|
+
staticSession.participatingEnvironments.add(environment);
|
|
1033
|
+
if (liveOutputSlotsByEnvironment.get(environment)?.size) staticSession.completedEnvironments.add(environment);
|
|
1034
|
+
else staticSession.completedEnvironments.delete(environment);
|
|
1035
|
+
rebuildStaticTransformContributions();
|
|
1036
|
+
return state;
|
|
1037
|
+
};
|
|
1038
|
+
/** Collapse an environment to a generation which replaced all of its observable outputs. */
|
|
1039
|
+
const completeEnvironmentGeneration = (environment, state) => {
|
|
1040
|
+
if (transformStateByEnvironment.get(environment) !== state) return;
|
|
1041
|
+
const epoch = createTransformEpoch(state);
|
|
1042
|
+
liveOutputSlotsByEnvironment.set(environment, new Map([[DIRECT_OUTPUT_TOKEN, { epoch }]]));
|
|
1043
|
+
staticSession.completedEnvironments.add(environment);
|
|
1044
|
+
rebuildStaticTransformContributions();
|
|
1045
|
+
rebuildLivePrunedClasses();
|
|
1046
|
+
};
|
|
1047
|
+
const prepareEnvironmentGeneration = (environment, state) => {
|
|
1048
|
+
if (transformStateByEnvironment.get(environment) !== state) return;
|
|
1049
|
+
preparedGenerations.set(environment, {
|
|
1050
|
+
state,
|
|
1051
|
+
epoch: createTransformEpoch(state),
|
|
1052
|
+
buildSerial: buildSerialByState.get(state) ?? 0,
|
|
1053
|
+
outputTokens: new Set(outputTokensByEnvironment.get(environment) ?? []),
|
|
1054
|
+
stagedOutputs: /* @__PURE__ */ new Map()
|
|
1055
|
+
});
|
|
1056
|
+
rebuildStaticTransformContributions();
|
|
1057
|
+
};
|
|
1058
|
+
const sameTokens = (left, right) => left.size === right.size && [...left].every((token) => right.has(token));
|
|
1059
|
+
/** The first configured output is an observable boundary even when its later hooks throw. */
|
|
1060
|
+
const beginOutputCycle = (environment, outputSlot) => {
|
|
1061
|
+
const previousSlot = lastStartedOutputSlotByEnvironment.get(environment);
|
|
1062
|
+
if (outputSlot === 0 || previousSlot !== void 0 && outputSlot <= previousSlot) pendingOutputCycles.delete(environment);
|
|
1063
|
+
lastStartedOutputSlotByEnvironment.set(environment, outputSlot);
|
|
1064
|
+
};
|
|
1065
|
+
const completeOutputCycle = (environment, cycle) => {
|
|
1066
|
+
liveOutputSlotsByEnvironment.set(environment, new Map(cycle.candidatesBySlot));
|
|
1067
|
+
pendingOutputCycles.delete(environment);
|
|
1068
|
+
preparedGenerations.delete(environment);
|
|
1069
|
+
staticSession.completedEnvironments.add(environment);
|
|
1070
|
+
rebuildStaticTransformContributions();
|
|
1071
|
+
rebuildLivePrunedClasses();
|
|
1072
|
+
};
|
|
1073
|
+
const publishPreparedOutput = (environment, outputToken, outputSlot, wasWritten) => {
|
|
1074
|
+
const generation = preparedGenerations.get(environment);
|
|
1075
|
+
if (!generation || transformStateByEnvironment.get(environment) !== generation.state) return;
|
|
1076
|
+
if (!generation.outputTokens.has(outputToken)) return;
|
|
1077
|
+
const mode = wasWritten ? "write" : "memory";
|
|
1078
|
+
let cycle = pendingOutputCycles.get(environment);
|
|
1079
|
+
const serialContinues = cycle !== void 0 && outputSlot > cycle.lastOutputSlot && (generation.buildSerial === cycle.lastBuildSerial || generation.buildSerial === cycle.lastBuildSerial + (outputSlot - cycle.lastOutputSlot));
|
|
1080
|
+
if (!cycle || cycle.mode !== mode || !sameTokens(cycle.expectedTokens, generation.outputTokens) || cycle.successfulTokens.has(outputToken) || !serialContinues) {
|
|
1081
|
+
cycle = {
|
|
1082
|
+
expectedTokens: new Set(generation.outputTokens),
|
|
1083
|
+
successfulTokens: /* @__PURE__ */ new Set(),
|
|
1084
|
+
candidatesBySlot: /* @__PURE__ */ new Map(),
|
|
1085
|
+
lastBuildSerial: generation.buildSerial,
|
|
1086
|
+
lastOutputSlot: outputSlot,
|
|
1087
|
+
mode
|
|
1088
|
+
};
|
|
1089
|
+
pendingOutputCycles.set(environment, cycle);
|
|
1090
|
+
}
|
|
1091
|
+
const stage = generation.stagedOutputs.get(outputToken);
|
|
1092
|
+
const candidateSlot = {
|
|
1093
|
+
epoch: generation.epoch,
|
|
1094
|
+
...stage?.prunedClasses ? { prunedClasses: new Set(stage.prunedClasses) } : {}
|
|
1095
|
+
};
|
|
1096
|
+
cycle.successfulTokens.add(outputToken);
|
|
1097
|
+
cycle.candidatesBySlot.set(outputSlot, candidateSlot);
|
|
1098
|
+
cycle.lastBuildSerial = generation.buildSerial;
|
|
1099
|
+
cycle.lastOutputSlot = outputSlot;
|
|
1100
|
+
const allOutputsSucceeded = cycle.successfulTokens.size === cycle.expectedTokens.size;
|
|
1101
|
+
if (wasWritten) {
|
|
1102
|
+
const slots = liveOutputSlotsByEnvironment.get(environment) ?? /* @__PURE__ */ new Map();
|
|
1103
|
+
slots.set(outputSlot, candidateSlot);
|
|
1104
|
+
liveOutputSlotsByEnvironment.set(environment, slots);
|
|
1105
|
+
staticSession.completedEnvironments.add(environment);
|
|
1106
|
+
rebuildStaticTransformContributions();
|
|
1107
|
+
rebuildLivePrunedClasses();
|
|
1108
|
+
}
|
|
1109
|
+
if (!allOutputsSucceeded || !wasWritten) return;
|
|
1110
|
+
completeOutputCycle(environment, cycle);
|
|
1111
|
+
};
|
|
1112
|
+
const closePreparedMemoryOutputs = (environment) => {
|
|
1113
|
+
const cycle = pendingOutputCycles.get(environment);
|
|
1114
|
+
if (!cycle || cycle.mode !== "memory" || cycle.successfulTokens.size !== cycle.expectedTokens.size) return;
|
|
1115
|
+
completeOutputCycle(environment, cycle);
|
|
1116
|
+
};
|
|
1117
|
+
/** Restore the still-live output contribution when the replacement generation fails. */
|
|
1118
|
+
const rollbackEnvironmentGeneration = (environment, state) => {
|
|
1119
|
+
if (transformStateByEnvironment.get(environment) !== state) return;
|
|
1120
|
+
preparedGenerations.delete(environment);
|
|
1121
|
+
pendingOutputCycles.delete(environment);
|
|
1122
|
+
const slots = liveOutputSlotsByEnvironment.get(environment);
|
|
1123
|
+
const liveEpochs = /* @__PURE__ */ new Map();
|
|
1124
|
+
for (const { epoch } of slots?.values() ?? []) liveEpochs.set(epoch.id, epoch);
|
|
1125
|
+
const latestLive = [...liveEpochs.values()].sort((a, b) => a.id - b.id).at(-1);
|
|
1126
|
+
if (latestLive) {
|
|
1127
|
+
transformStateByEnvironment.set(environment, cloneEnvironmentState(latestLive.state));
|
|
1128
|
+
staticSession.completedEnvironments.add(environment);
|
|
1129
|
+
} else {
|
|
1130
|
+
transformStateByEnvironment.delete(environment);
|
|
1131
|
+
staticSession.completedEnvironments.delete(environment);
|
|
1132
|
+
}
|
|
1133
|
+
rebuildStaticTransformContributions();
|
|
1134
|
+
rebuildLivePrunedClasses();
|
|
1135
|
+
};
|
|
1136
|
+
staticSession.beginOutputProjection = (environment, outputOptions, bundle, replacesGeneratedStylesheet) => {
|
|
1137
|
+
const generation = preparedGenerations.get(environment);
|
|
1138
|
+
if (!generation || transformStateByEnvironment.get(environment) !== generation.state) {
|
|
1139
|
+
const currentState = transformStateByEnvironment.get(environment);
|
|
1140
|
+
return {
|
|
1141
|
+
cssLoaded: currentState?.cssLoaded ?? staticSession.cssLoaded,
|
|
1142
|
+
requiredClasses: requiredClassesForProjection(environment, currentState ? ownedClassesForState(currentState) : currentRequiredClasses()),
|
|
1143
|
+
restore() {}
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
const committedPrunedClasses = staticSession.prunedClasses;
|
|
1147
|
+
staticSession.prunedClasses = new Set(committedPrunedClasses);
|
|
1148
|
+
rebuildStaticTransformContributions(environment, generation.epoch.state);
|
|
1149
|
+
const requiredClasses = requiredClassesForProjection(environment, generation.epoch.ownedClasses);
|
|
1150
|
+
if (replacesGeneratedStylesheet) staticSession.prunedClasses.clear();
|
|
1151
|
+
let restored = false;
|
|
1152
|
+
return {
|
|
1153
|
+
cssLoaded: generation.epoch.state.cssLoaded,
|
|
1154
|
+
requiredClasses,
|
|
1155
|
+
restore() {
|
|
1156
|
+
if (restored) return;
|
|
1157
|
+
restored = true;
|
|
1158
|
+
const stage = replacesGeneratedStylesheet ? {
|
|
1159
|
+
cssDigest: bambooCssDigest(bundle),
|
|
1160
|
+
prunedClasses: new Set(staticSession.prunedClasses)
|
|
1161
|
+
} : {};
|
|
1162
|
+
outputStageByBundle.set(bundle, stage);
|
|
1163
|
+
outputStageByOptions.set(outputOptions, stage);
|
|
1164
|
+
staticSession.prunedClasses = committedPrunedClasses;
|
|
1165
|
+
rebuildStaticTransformContributions();
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
};
|
|
1169
|
+
/** Aggregate environment-owned coverage once the participating generation is complete. */
|
|
1170
|
+
const reportTransformCoverage = (states) => {
|
|
1171
|
+
const perFile = /* @__PURE__ */ new Map();
|
|
1172
|
+
const reportedCoverageByModule = /* @__PURE__ */ new Map();
|
|
1173
|
+
for (const state of states) for (const artifact of state.transformArtifactsByModule.values()) {
|
|
1174
|
+
const coverageKey = JSON.stringify([
|
|
1175
|
+
artifact.file,
|
|
1176
|
+
artifact.folded,
|
|
1177
|
+
artifact.skipped
|
|
1178
|
+
]);
|
|
1179
|
+
const reported = reportedCoverageByModule.get(artifact.moduleId);
|
|
1180
|
+
if (reported?.has(coverageKey)) continue;
|
|
1181
|
+
if (reported) reported.add(coverageKey);
|
|
1182
|
+
else reportedCoverageByModule.set(artifact.moduleId, new Set([coverageKey]));
|
|
1183
|
+
const entry = perFile.get(artifact.file) ?? {
|
|
1184
|
+
folded: 0,
|
|
1185
|
+
skipped: /* @__PURE__ */ new Map()
|
|
1186
|
+
};
|
|
1187
|
+
entry.folded += artifact.folded;
|
|
1188
|
+
for (const [reason, count] of artifact.skipped) entry.skipped.set(reason, (entry.skipped.get(reason) ?? 0) + count);
|
|
1189
|
+
perFile.set(artifact.file, entry);
|
|
1190
|
+
}
|
|
1191
|
+
let folded = 0;
|
|
1192
|
+
let filesWithFolds = 0;
|
|
1193
|
+
const skipped = /* @__PURE__ */ new Map();
|
|
1194
|
+
for (const entry of perFile.values()) {
|
|
1195
|
+
folded += entry.folded;
|
|
1196
|
+
if (entry.folded) filesWithFolds++;
|
|
1197
|
+
for (const [reason, count] of entry.skipped) skipped.set(reason, (skipped.get(reason) ?? 0) + count);
|
|
1198
|
+
}
|
|
1199
|
+
const declined = Array.from(skipped.values()).reduce((sum, count) => sum + count, 0);
|
|
1200
|
+
const total = folded + declined;
|
|
1201
|
+
if (!total) return;
|
|
1202
|
+
const share = Math.round(folded / total * 100);
|
|
1203
|
+
const reasons = Array.from(skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
|
|
1204
|
+
_bamboocss_logger.logger.info("vite:transform", `Compiled ${folded}/${total} (${share}%) across ${filesWithFolds}/${perFile.size} files` + (reasons ? ` — declined: ${reasons}` : ""));
|
|
1205
|
+
};
|
|
1206
|
+
/** Restore every per-build fact established by one successful transform. */
|
|
1207
|
+
const applyTransformArtifact = (state, value, expectedModuleId, environment) => {
|
|
1208
|
+
let snapshot;
|
|
1209
|
+
try {
|
|
1210
|
+
snapshot = structuredClone(value);
|
|
1211
|
+
} catch {
|
|
1212
|
+
throw cachedArtifactError(expectedModuleId, environment, void 0, `could not be snapshotted as serializable schema version ${TRANSFORM_ARTIFACT_VERSION} data`);
|
|
1213
|
+
}
|
|
1214
|
+
if (!isTransformArtifact(snapshot) || snapshot.moduleId !== expectedModuleId || snapshot.file !== expectedModuleId.split("?")[0] || !hasValidTransformArtifactIntegrity(environment, snapshot)) throw cachedArtifactError(expectedModuleId, environment, snapshot);
|
|
1215
|
+
const artifact = snapshot;
|
|
1216
|
+
const { file, moduleId } = artifact;
|
|
1217
|
+
state.transformArtifactsByModule.set(moduleId, artifact);
|
|
1218
|
+
recordFoldDependencies(state, moduleId, file, artifact.dependencies);
|
|
1219
|
+
if (artifact.signature) state.foldSignatures.set(moduleId, artifact.signature);
|
|
1220
|
+
else state.foldSignatures.delete(moduleId);
|
|
1221
|
+
};
|
|
3348
1222
|
/**
|
|
3349
|
-
*
|
|
1223
|
+
* Replay transform metadata for modules Rollup reused from its cache.
|
|
3350
1224
|
*
|
|
3351
|
-
* `
|
|
3352
|
-
*
|
|
3353
|
-
*
|
|
1225
|
+
* `buildStart` has to clear reachability because a watch rebuild may have a different graph,
|
|
1226
|
+
* but Rollup does not call `transform` again for an unchanged module. The transform result is
|
|
1227
|
+
* still present on that module as serializable metadata, and `buildEnd` is the common Rollup
|
|
1228
|
+
* and Rolldown point where the complete graph can be enumerated while CSS generation is still
|
|
1229
|
+
* ahead of us. Replaying here restores the exact state pruning and the finished-build guards
|
|
1230
|
+
* would have observed after a clean build.
|
|
1231
|
+
*
|
|
1232
|
+
* Freshly transformed module IDs are skipped inside this environment only. Besides avoiding
|
|
1233
|
+
* duplicate work, that makes a failed transform authoritative: an older cached artifact must
|
|
1234
|
+
* not overwrite the diagnostic and signature retraction established by the failing pass.
|
|
3354
1235
|
*/
|
|
3355
|
-
const
|
|
1236
|
+
const replayCachedTransformArtifacts = (pluginContext) => {
|
|
1237
|
+
if (!pluginContext.getModuleIds || !pluginContext.getModuleInfo) return;
|
|
1238
|
+
const state = environmentState(pluginContext);
|
|
1239
|
+
for (const id of pluginContext.getModuleIds()) {
|
|
1240
|
+
if (state.transformedModulesThisRun.has(id)) continue;
|
|
1241
|
+
const meta = pluginContext.getModuleInfo(id)?.meta;
|
|
1242
|
+
if (!meta || !Object.prototype.hasOwnProperty.call(meta, TRANSFORM_META_KEY)) continue;
|
|
1243
|
+
const artifact = meta[TRANSFORM_META_KEY];
|
|
1244
|
+
applyTransformArtifact(state, artifact, id, environmentName(pluginContext));
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
3356
1247
|
/**
|
|
3357
1248
|
* How many dependents in a row may come back changed before the check gives up for this event.
|
|
3358
1249
|
*
|
|
@@ -3379,7 +1270,6 @@ const bamboocss = (options = {}) => {
|
|
|
3379
1270
|
* run to reach eight, the consumers seen so far have to be uniformly changed.
|
|
3380
1271
|
*/
|
|
3381
1272
|
const CHANGED_RUN_LIMIT = 8;
|
|
3382
|
-
let changedRun = 0;
|
|
3383
1273
|
/**
|
|
3384
1274
|
* Whether re-folding `dependent` now produces exactly the bytes it produced last time.
|
|
3385
1275
|
*
|
|
@@ -3394,64 +1284,72 @@ const bamboocss = (options = {}) => {
|
|
|
3394
1284
|
* that returns nothing, a fold that throws — because "changed" is what this path did before,
|
|
3395
1285
|
* and a wrong "unchanged" is a stale class string in the browser.
|
|
3396
1286
|
*/
|
|
3397
|
-
const foldOutputUnchanged = (dependent) => {
|
|
3398
|
-
const memoized = unchangedFolds.get(dependent);
|
|
1287
|
+
const foldOutputUnchanged = (state, dependent) => {
|
|
1288
|
+
const memoized = state.unchangedFolds.get(dependent);
|
|
3399
1289
|
if (memoized !== void 0) return memoized;
|
|
3400
|
-
const unchanged = changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(dependent);
|
|
3401
|
-
changedRun = unchanged ? 0 : changedRun + 1;
|
|
3402
|
-
unchangedFolds.set(dependent, unchanged);
|
|
1290
|
+
const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent);
|
|
1291
|
+
state.changedRun = unchanged ? 0 : state.changedRun + 1;
|
|
1292
|
+
state.unchangedFolds.set(dependent, unchanged);
|
|
3403
1293
|
return unchanged;
|
|
3404
1294
|
};
|
|
3405
|
-
const refoldMatchesSignature = (dependent) => {
|
|
3406
|
-
const signature = foldSignatures.get(dependent);
|
|
3407
|
-
if (!signature || !ctx || !runtimeCss || !styleCompiler) return false;
|
|
1295
|
+
const refoldMatchesSignature = (state, dependent) => {
|
|
1296
|
+
const signature = state.foldSignatures.get(dependent);
|
|
1297
|
+
if (!signature || !ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return false;
|
|
3408
1298
|
try {
|
|
3409
1299
|
const code = (0, node_fs.readFileSync)(signature.path, "utf8");
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
const
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
1300
|
+
const inputDigest = digest(code);
|
|
1301
|
+
if (inputDigest !== signature.input) return false;
|
|
1302
|
+
let raw;
|
|
1303
|
+
let parserDependencies;
|
|
1304
|
+
const memoKey = foldMemoKey(signature.path, inputDigest);
|
|
1305
|
+
const memoized = foldMemoByContent.get(memoKey);
|
|
1306
|
+
if (memoized) {
|
|
1307
|
+
raw = memoized.result;
|
|
1308
|
+
parserDependencies = memoized.parserDependencies;
|
|
1309
|
+
} else {
|
|
1310
|
+
const sourceFile = ctx.project.addSourceFile(signature.path, code);
|
|
1311
|
+
const parserResult = ctx.project.parseSourceFile(signature.path);
|
|
1312
|
+
if (!parserResult) return false;
|
|
1313
|
+
raw = foldSourceImpl({
|
|
1314
|
+
ctx,
|
|
1315
|
+
code,
|
|
1316
|
+
parserResult,
|
|
1317
|
+
filePath: signature.path,
|
|
1318
|
+
runtimeCss,
|
|
1319
|
+
styleCompiler,
|
|
1320
|
+
maxRecipeStates,
|
|
1321
|
+
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
1322
|
+
recipeConfigCache: state.recipeConfigCache,
|
|
1323
|
+
reportSurvivors: false,
|
|
1324
|
+
sourceFile
|
|
1325
|
+
});
|
|
1326
|
+
parserDependencies = parserResult.getDependencies();
|
|
1327
|
+
foldMemoByContent.set(memoKey, {
|
|
1328
|
+
result: raw,
|
|
1329
|
+
parserDependencies,
|
|
1330
|
+
reportedSurvivors: false
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
const result = withResolutionClosure(signature.path, raw, parserDependencies, state.dependenciesByModule.get(dependent));
|
|
3427
1334
|
const unchanged = digest(result.code) === signature.output;
|
|
3428
1335
|
/**
|
|
3429
|
-
* Edges re-recorded on the way to suppressing a module
|
|
3430
|
-
*
|
|
1336
|
+
* Edges re-recorded exactly on the way to suppressing a module. A changed provisional
|
|
1337
|
+
* fold may add newly observed edges, but never retracts the last recoverable ones.
|
|
3431
1338
|
*
|
|
3432
1339
|
* The first is necessary: *which* files a module folds from can move while the bytes it
|
|
3433
1340
|
* emits do not — a value now re-exported through a different module, say — and suppressing
|
|
3434
1341
|
* the announcement means no transform will run to notice. Leaving the old edges would make
|
|
3435
1342
|
* the next edit to the new dependency reach nobody.
|
|
3436
1343
|
*
|
|
3437
|
-
*
|
|
3438
|
-
*
|
|
3439
|
-
*
|
|
3440
|
-
*
|
|
3441
|
-
*
|
|
3442
|
-
*
|
|
3443
|
-
* with the client half correctly updated. That is the shape of the bug the self-accepting
|
|
3444
|
-
* fix was about, one environment over. `transform` performs the same retraction, but only
|
|
3445
|
-
* after every environment has already invalidated, which is why it is safe there.
|
|
3446
|
-
*
|
|
3447
|
-
* Confining the write to the unchanged branch removes the hazard rather than sequencing
|
|
3448
|
-
* around it, because a retraction cannot reach that branch: `dependencies` is empty only
|
|
3449
|
-
* when `folded.length === 0`, and a fold that folds nothing returns the module's own source,
|
|
3450
|
-
* which cannot equal an output digest recorded from a pass that replaced a call with a
|
|
3451
|
-
* literal. And where an unchanged verdict *does* narrow the edges, both passes agree anyway
|
|
3452
|
-
* — neither invalidates a module it has just called unchanged.
|
|
1344
|
+
* A changed result is only a provisional check on the way to a real transform. Its reads
|
|
1345
|
+
* can warm the extractor cache, so the authoritative parse may not cross every nested
|
|
1346
|
+
* module again; retaining the union gives that transform semantic targets to validate
|
|
1347
|
+
* against the current Project ledger. It then records the exact answer. If that pass
|
|
1348
|
+
* throws, the additive update has kept every old edge while also making a fix in a newly
|
|
1349
|
+
* observed dependency able to retry it.
|
|
3453
1350
|
*/
|
|
3454
|
-
if (unchanged) recordFoldDependencies(dependent, result.dependencies);
|
|
1351
|
+
if (unchanged) recordFoldDependencies(state, dependent, signature.path, result.dependencies);
|
|
1352
|
+
else recordFoldDependencies(state, dependent, signature.path, [...state.dependenciesByModule.get(dependent) ?? [], ...result.dependencies]);
|
|
3455
1353
|
return unchanged;
|
|
3456
1354
|
} catch {
|
|
3457
1355
|
return false;
|
|
@@ -3488,8 +1386,8 @@ const bamboocss = (options = {}) => {
|
|
|
3488
1386
|
* one can end in a page reload, which is the honest outcome: its compiled classes really did
|
|
3489
1387
|
* change, and a reload is what Vite does with any update nothing accepts.
|
|
3490
1388
|
*/
|
|
3491
|
-
const foldDependentModules = (file, modules, graph) => {
|
|
3492
|
-
const dependents = dependentsByDependency.get(normalizeFsPath(file));
|
|
1389
|
+
const foldDependentModules = (state, file, modules, graph) => {
|
|
1390
|
+
const dependents = state.dependentsByDependency.get(normalizeFsPath(file));
|
|
3493
1391
|
if (!dependents?.size) return;
|
|
3494
1392
|
const added = [];
|
|
3495
1393
|
for (const dependent of [...dependents]) {
|
|
@@ -3526,8 +1424,11 @@ const bamboocss = (options = {}) => {
|
|
|
3526
1424
|
* the edited module for a runtime value is still reached by `propagateUpdate` exactly as
|
|
3527
1425
|
* it would be with no plugin here at all — that direction was never this list's to decide.
|
|
3528
1426
|
*/
|
|
3529
|
-
if (foldOutputUnchanged(dependent)) continue;
|
|
3530
|
-
|
|
1427
|
+
if (foldOutputUnchanged(state, dependent)) continue;
|
|
1428
|
+
const exact = graph.getModuleById?.(dependent);
|
|
1429
|
+
const dependentFile = state.filesByModule.get(dependent) ?? dependent;
|
|
1430
|
+
const candidates = exact ? [exact] : graph.getModulesByFile(normalizeFsPath(dependentFile)) ?? [];
|
|
1431
|
+
for (const module of candidates) {
|
|
3531
1432
|
if (modules.includes(module) || added.includes(module)) continue;
|
|
3532
1433
|
graph.invalidateModule(module);
|
|
3533
1434
|
added.push(module);
|
|
@@ -3556,52 +1457,196 @@ const bamboocss = (options = {}) => {
|
|
|
3556
1457
|
return [...modules, ...added];
|
|
3557
1458
|
};
|
|
3558
1459
|
let ctx;
|
|
1460
|
+
let foldSourceImpl;
|
|
3559
1461
|
let runtimeCss;
|
|
3560
1462
|
let styleCompiler;
|
|
3561
1463
|
let command = "build";
|
|
3562
|
-
let
|
|
3563
|
-
|
|
3564
|
-
|
|
1464
|
+
let defaultEmitAssets = true;
|
|
1465
|
+
/**
|
|
1466
|
+
* Expand semantic leaf reads through the Project's exact resolution paths.
|
|
1467
|
+
*
|
|
1468
|
+
* Most boxed values point at their final declaration, which the fold reports directly. An
|
|
1469
|
+
* evaluated imported helper is different: its returned box belongs to the local call node,
|
|
1470
|
+
* while ParserResult records the modules crossed during that evaluation. Seed from both so
|
|
1471
|
+
* neither form can leave compiled JavaScript stale. A re-export or barrel on either path can
|
|
1472
|
+
* change which declaration the same import selects and must be watched too. Supplying only
|
|
1473
|
+
* these semantic leaves back to Project keeps unrelated runtime-import branches out and walks
|
|
1474
|
+
* only the indexed closure rather than scanning the global ledger.
|
|
1475
|
+
*/
|
|
1476
|
+
const withResolutionClosure = (filePath, result, parserDependencies = [], previousDependencies = void 0) => {
|
|
1477
|
+
if (!ctx || !result.folded.length) return result;
|
|
1478
|
+
const dependencies = new Set(result.dependencies);
|
|
1479
|
+
for (const dependency of parserDependencies) if (!isGeneratedOutput(dependency, ctx)) dependencies.add(dependency);
|
|
1480
|
+
const targets = new Set(dependencies);
|
|
1481
|
+
for (const dependency of previousDependencies ?? []) targets.add(dependency);
|
|
1482
|
+
if (!targets.size) return result;
|
|
1483
|
+
const targetList = [...targets];
|
|
1484
|
+
const closureKey = command === "serve" ? `${filePath}\0${targetList.slice().sort().join("|")}` : void 0;
|
|
1485
|
+
let reachable = closureKey ? resolutionClosureMemo.get(closureKey) : void 0;
|
|
1486
|
+
if (!reachable) {
|
|
1487
|
+
reachable = ctx.project.getDependencies(filePath, targetList);
|
|
1488
|
+
if (closureKey) resolutionClosureMemo.set(closureKey, reachable);
|
|
1489
|
+
}
|
|
1490
|
+
for (const dependency of reachable) if (!isGeneratedOutput(dependency, ctx)) dependencies.add(dependency);
|
|
1491
|
+
const expanded = [...dependencies];
|
|
1492
|
+
if (expanded.length === result.dependencies.length && expanded.every((dependency, index) => dependency === result.dependencies[index])) return result;
|
|
1493
|
+
return {
|
|
1494
|
+
...result,
|
|
1495
|
+
dependencies: expanded
|
|
1496
|
+
};
|
|
1497
|
+
};
|
|
1498
|
+
const loadContext = createRetryableLazy(async () => {
|
|
1499
|
+
const { loadConfigAndCreateContext } = await loadNodeModule();
|
|
1500
|
+
return loadConfigAndCreateContext({
|
|
3565
1501
|
configPath,
|
|
3566
1502
|
cwd,
|
|
3567
1503
|
dev: command === "serve"
|
|
3568
|
-
}).then((loaded) => {
|
|
3569
|
-
ctx = loaded;
|
|
3570
|
-
runtimeCss = createRuntimeCss(loaded);
|
|
3571
|
-
styleCompiler = createStaticStyleSetCompiler(loaded, runtimeCss);
|
|
3572
1504
|
});
|
|
3573
|
-
|
|
1505
|
+
});
|
|
1506
|
+
const loadCompilerState = createLazyCompilerState(loadContext, loadFoldModule);
|
|
1507
|
+
const ensureContext = async () => {
|
|
1508
|
+
ctx = await loadContext();
|
|
3574
1509
|
};
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
1510
|
+
const ensureCompilerState = async () => {
|
|
1511
|
+
const loaded = await loadCompilerState();
|
|
1512
|
+
ctx = loaded.context;
|
|
1513
|
+
foldSourceImpl = loaded.foldSource;
|
|
1514
|
+
runtimeCss = loaded.runtimeCss;
|
|
1515
|
+
styleCompiler = loaded.styleCompiler;
|
|
1516
|
+
};
|
|
1517
|
+
const outputFinalizerTag = (value) => {
|
|
1518
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1519
|
+
return value[OUTPUT_FINALIZER];
|
|
1520
|
+
};
|
|
1521
|
+
const outputStartMarkerTag = (value) => {
|
|
1522
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1523
|
+
return value[OUTPUT_START_MARKER];
|
|
1524
|
+
};
|
|
1525
|
+
const findOutputFinalizer = (value, environment, outputSlot) => {
|
|
1526
|
+
if (Array.isArray(value)) {
|
|
1527
|
+
for (const entry of value) {
|
|
1528
|
+
const found = findOutputFinalizer(entry, environment, outputSlot);
|
|
1529
|
+
if (found) return found;
|
|
1530
|
+
}
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const identity = outputFinalizerTag(value);
|
|
1534
|
+
return identity?.environment === environment && identity.outputSlot === outputSlot ? value : void 0;
|
|
1535
|
+
};
|
|
1536
|
+
const findOutputStartMarker = (value, environment, outputSlot) => {
|
|
1537
|
+
if (Array.isArray(value)) {
|
|
1538
|
+
for (const entry of value) {
|
|
1539
|
+
const found = findOutputStartMarker(entry, environment, outputSlot);
|
|
1540
|
+
if (found) return found;
|
|
1541
|
+
}
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
const identity = outputStartMarkerTag(value);
|
|
1545
|
+
return identity?.environment === environment && identity.outputSlot === outputSlot ? value : void 0;
|
|
1546
|
+
};
|
|
1547
|
+
const stripOutputLifecyclePlugins = (value) => {
|
|
1548
|
+
if (Array.isArray(value)) return value.flatMap(stripOutputLifecyclePlugins);
|
|
1549
|
+
return value == null || outputFinalizerTag(value) || outputStartMarkerTag(value) ? [] : [value];
|
|
1550
|
+
};
|
|
1551
|
+
const createOutputStartMarker = (environment, outputSlot) => {
|
|
1552
|
+
return Object.assign({
|
|
1553
|
+
name: `bamboocss:output-start:${environment}:${outputSlot}`,
|
|
1554
|
+
renderStart: {
|
|
1555
|
+
order: "pre",
|
|
1556
|
+
sequential: true,
|
|
1557
|
+
handler() {
|
|
1558
|
+
beginOutputCycle(environment, outputSlot);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}, { [OUTPUT_START_MARKER]: {
|
|
1562
|
+
environment,
|
|
1563
|
+
outputSlot
|
|
1564
|
+
} });
|
|
1565
|
+
};
|
|
1566
|
+
const createOutputFinalizer = (environment, outputSlot) => {
|
|
1567
|
+
const outputToken = ++nextOutputToken;
|
|
1568
|
+
return Object.assign({
|
|
1569
|
+
name: `bamboocss:output-finalizer:${environment}:${outputSlot}`,
|
|
1570
|
+
generateBundle: {
|
|
1571
|
+
order: "post",
|
|
1572
|
+
handler(outputOptions, bundle, isWrite) {
|
|
1573
|
+
if (!bundle || typeof bundle !== "object") return;
|
|
1574
|
+
const identity = {
|
|
1575
|
+
environment,
|
|
1576
|
+
outputSlot,
|
|
1577
|
+
outputToken
|
|
1578
|
+
};
|
|
1579
|
+
outputIdentityByBundle.set(bundle, identity);
|
|
1580
|
+
if (outputOptions && typeof outputOptions === "object") outputIdentityByOptions.set(outputOptions, identity);
|
|
1581
|
+
const generation = preparedGenerations.get(environment);
|
|
1582
|
+
const stage = outputStageByBundle.get(bundle) ?? (outputOptions && typeof outputOptions === "object" ? outputStageByOptions.get(outputOptions) : void 0);
|
|
1583
|
+
if (stage?.cssDigest && bambooCssDigest(bundle) !== stage.cssDigest) throw new Error("bamboocss: an output plugin changed or removed the generated stylesheet after Bamboo finalized its reachability. The cached prune history would no longer describe the emitted CSS. Preserve the Bamboo asset in later `generateBundle` hooks, or run that transformation before Bamboo.");
|
|
1584
|
+
if (generation && stage) generation.stagedOutputs.set(outputToken, stage);
|
|
1585
|
+
if (!isWrite) publishPreparedOutput(environment, outputToken, outputSlot, false);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}, { [OUTPUT_FINALIZER]: {
|
|
1589
|
+
environment,
|
|
1590
|
+
outputSlot,
|
|
1591
|
+
outputToken
|
|
1592
|
+
} });
|
|
1593
|
+
};
|
|
1594
|
+
const installOutputFinalizers = (inputOptions, environment) => {
|
|
1595
|
+
if (!inputOptions.output) {
|
|
1596
|
+
outputTokensByEnvironment.delete(environment);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const outputs = Array.isArray(inputOptions.output) ? inputOptions.output : [inputOptions.output];
|
|
1600
|
+
const outputTokens = /* @__PURE__ */ new Set();
|
|
1601
|
+
const installed = outputs.map((output, outputSlot) => {
|
|
1602
|
+
const existing = findOutputFinalizer(output.plugins, environment, outputSlot);
|
|
1603
|
+
const existingStart = findOutputStartMarker(output.plugins, environment, outputSlot);
|
|
1604
|
+
if (existing && existingStart) {
|
|
1605
|
+
outputTokens.add(existing[OUTPUT_FINALIZER].outputToken);
|
|
1606
|
+
return output;
|
|
1607
|
+
}
|
|
1608
|
+
const finalizer = existing ?? createOutputFinalizer(environment, outputSlot);
|
|
1609
|
+
const startMarker = existingStart ?? createOutputStartMarker(environment, outputSlot);
|
|
1610
|
+
outputTokens.add(finalizer[OUTPUT_FINALIZER].outputToken);
|
|
1611
|
+
output.plugins = [
|
|
1612
|
+
startMarker,
|
|
1613
|
+
...stripOutputLifecyclePlugins(output.plugins),
|
|
1614
|
+
finalizer
|
|
1615
|
+
];
|
|
1616
|
+
return output;
|
|
1617
|
+
});
|
|
1618
|
+
inputOptions.output = Array.isArray(inputOptions.output) ? installed : installed[0];
|
|
1619
|
+
outputTokensByEnvironment.set(environment, outputTokens);
|
|
1620
|
+
};
|
|
1621
|
+
const compiler = {
|
|
3581
1622
|
name: "bamboocss:compiler",
|
|
3582
1623
|
enforce: "pre",
|
|
3583
1624
|
/** See the same declaration on the css plugin: one instance per build, not per environment. */
|
|
3584
1625
|
sharedDuringBuild: true,
|
|
1626
|
+
options(inputOptions) {
|
|
1627
|
+
installOutputFinalizers(inputOptions, environmentName(this));
|
|
1628
|
+
return inputOptions;
|
|
1629
|
+
},
|
|
3585
1630
|
configResolved(config) {
|
|
3586
1631
|
command = config.command;
|
|
1632
|
+
defaultEmitAssets = config.build?.emitAssets ?? (!config.build?.ssr || config.build?.ssrEmitAssets === true);
|
|
1633
|
+
const plugins = config.plugins;
|
|
1634
|
+
if (plugins) {
|
|
1635
|
+
for (const finalizer of [outputWriteObserver, memoryOutputCommitter]) {
|
|
1636
|
+
const index = plugins.indexOf(finalizer);
|
|
1637
|
+
if (index !== -1) plugins.splice(index, 1);
|
|
1638
|
+
}
|
|
1639
|
+
plugins.unshift(outputWriteObserver);
|
|
1640
|
+
plugins.push(memoryOutputCommitter);
|
|
1641
|
+
}
|
|
3587
1642
|
},
|
|
3588
1643
|
async buildStart() {
|
|
3589
|
-
const environment = this
|
|
3590
|
-
|
|
3591
|
-
perFile.clear();
|
|
3592
|
-
survivorsByFile.clear();
|
|
3593
|
-
recipeConfigCache.clear();
|
|
3594
|
-
dependentsByDependency.clear();
|
|
3595
|
-
dependenciesByFile.clear();
|
|
3596
|
-
foldSignatures.clear();
|
|
3597
|
-
unchangedFolds.clear();
|
|
3598
|
-
changedRun = 0;
|
|
3599
|
-
resetStaticCompilationSession(staticSession);
|
|
3600
|
-
}
|
|
3601
|
-
staticSession.startedEnvironments.add(environment);
|
|
1644
|
+
const environment = environmentName(this);
|
|
1645
|
+
const state = beginEnvironmentGeneration(environment);
|
|
3602
1646
|
try {
|
|
3603
1647
|
await ensureContext();
|
|
3604
1648
|
} catch (error) {
|
|
1649
|
+
rollbackEnvironmentGeneration(environment, state);
|
|
3605
1650
|
throw asError(error, "failed to load the bamboo config");
|
|
3606
1651
|
}
|
|
3607
1652
|
},
|
|
@@ -3626,17 +1671,27 @@ const bamboocss = (options = {}) => {
|
|
|
3626
1671
|
* the parser still holds the file.
|
|
3627
1672
|
*/
|
|
3628
1673
|
watchChange(id, change) {
|
|
3629
|
-
|
|
3630
|
-
|
|
1674
|
+
foldMemoByContent.clear();
|
|
1675
|
+
resolutionClosureMemo.clear();
|
|
1676
|
+
for (const state of transformStateByEnvironment.values()) {
|
|
1677
|
+
state.unchangedFolds.clear();
|
|
1678
|
+
state.changedRun = 0;
|
|
1679
|
+
}
|
|
3631
1680
|
if (!ctx) return;
|
|
3632
1681
|
if (!shouldTransform(id)) return;
|
|
3633
1682
|
const [filePath] = id.split("?");
|
|
3634
1683
|
if (!filePath) return;
|
|
3635
|
-
recipeConfigCache.clear();
|
|
1684
|
+
for (const state of transformStateByEnvironment.values()) state.recipeConfigCache.clear();
|
|
3636
1685
|
if (change.event === "delete") {
|
|
3637
1686
|
ctx.project.removeSourceFile(filePath);
|
|
3638
|
-
|
|
3639
|
-
|
|
1687
|
+
const deleted = normalizeFsPath(filePath);
|
|
1688
|
+
for (const state of transformStateByEnvironment.values()) for (const [moduleId, moduleFile] of [...state.filesByModule]) {
|
|
1689
|
+
if (normalizeFsPath(moduleFile) !== deleted) continue;
|
|
1690
|
+
recordFoldDependencies(state, moduleId, moduleFile, []);
|
|
1691
|
+
state.foldSignatures.delete(moduleId);
|
|
1692
|
+
state.transformArtifactsByModule.delete(moduleId);
|
|
1693
|
+
state.filesByModule.delete(moduleId);
|
|
1694
|
+
}
|
|
3640
1695
|
return;
|
|
3641
1696
|
}
|
|
3642
1697
|
ctx.project.reloadSourceFile(filePath);
|
|
@@ -3663,138 +1718,220 @@ const bamboocss = (options = {}) => {
|
|
|
3663
1718
|
hotUpdate({ file, modules }) {
|
|
3664
1719
|
const graph = this.environment?.moduleGraph;
|
|
3665
1720
|
if (!graph) return;
|
|
3666
|
-
return foldDependentModules(file, modules, graph);
|
|
1721
|
+
return foldDependentModules(environmentState(this), file, modules, graph);
|
|
3667
1722
|
},
|
|
3668
1723
|
handleHotUpdate({ file, modules, server }) {
|
|
3669
1724
|
const legacy = server;
|
|
3670
1725
|
if (legacy.environments) return;
|
|
3671
|
-
return foldDependentModules(file, modules, legacy.moduleGraph);
|
|
1726
|
+
return foldDependentModules(environmentState(this), file, modules, legacy.moduleGraph);
|
|
3672
1727
|
},
|
|
3673
1728
|
async transform(code, id) {
|
|
3674
1729
|
if (!shouldTransform(id)) return null;
|
|
3675
1730
|
try {
|
|
3676
|
-
await
|
|
1731
|
+
await ensureCompilerState();
|
|
3677
1732
|
} catch (error) {
|
|
3678
|
-
throw asError(error, "failed to
|
|
1733
|
+
throw asError(error, "failed to initialize the bamboo compiler");
|
|
3679
1734
|
}
|
|
3680
|
-
if (!ctx || !runtimeCss || !styleCompiler) return null;
|
|
1735
|
+
if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return null;
|
|
3681
1736
|
const [filePath] = id.split("?");
|
|
3682
|
-
clearSurvivorsFor(filePath);
|
|
3683
1737
|
if (isGeneratedOutput(filePath, ctx)) return null;
|
|
1738
|
+
const state = environmentState(this);
|
|
1739
|
+
state.transformedModulesThisRun.add(id);
|
|
1740
|
+
let inputDigest;
|
|
1741
|
+
const previousSignature = state.foldSignatures.get(id);
|
|
1742
|
+
const previousDependencies = previousSignature && previousSignature.input === (inputDigest ??= digest(code)) ? state.dependenciesByModule.get(id) : void 0;
|
|
3684
1743
|
let result;
|
|
3685
1744
|
try {
|
|
3686
|
-
const
|
|
3687
|
-
const
|
|
3688
|
-
if (
|
|
3689
|
-
|
|
3690
|
-
ctx,
|
|
3691
|
-
|
|
3692
|
-
parserResult
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
1745
|
+
const memoKey = command === "serve" ? foldMemoKey(filePath, inputDigest ??= digest(code)) : void 0;
|
|
1746
|
+
const memoized = memoKey ? foldMemoByContent.get(memoKey) : void 0;
|
|
1747
|
+
if (memoized?.reportedSurvivors) result = withResolutionClosure(filePath, memoized.result, memoized.parserDependencies, previousDependencies);
|
|
1748
|
+
else {
|
|
1749
|
+
const sourceFile = ctx.project.addSourceFile(filePath, code);
|
|
1750
|
+
const parserResult = ctx.project.parseSourceFile(filePath);
|
|
1751
|
+
if (!parserResult) {
|
|
1752
|
+
state.transformArtifactsByModule.delete(id);
|
|
1753
|
+
recordFoldDependencies(state, id, filePath, []);
|
|
1754
|
+
state.foldSignatures.delete(id);
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
const folded = foldSourceImpl({
|
|
1758
|
+
ctx,
|
|
1759
|
+
code,
|
|
1760
|
+
parserResult,
|
|
1761
|
+
filePath,
|
|
1762
|
+
runtimeCss,
|
|
1763
|
+
styleCompiler,
|
|
1764
|
+
maxRecipeStates,
|
|
1765
|
+
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
1766
|
+
recipeConfigCache: state.recipeConfigCache,
|
|
1767
|
+
reportSurvivors: true,
|
|
1768
|
+
sourceFile
|
|
1769
|
+
});
|
|
1770
|
+
const parserDependencies = parserResult.getDependencies();
|
|
1771
|
+
if (memoKey) foldMemoByContent.set(memoKey, {
|
|
1772
|
+
result: folded,
|
|
1773
|
+
parserDependencies,
|
|
1774
|
+
reportedSurvivors: true
|
|
1775
|
+
});
|
|
1776
|
+
result = withResolutionClosure(filePath, folded, parserDependencies, previousDependencies);
|
|
1777
|
+
}
|
|
3702
1778
|
} catch (error) {
|
|
3703
1779
|
_bamboocss_logger.logger.caughtError("vite:transform", `Failed to compile ${filePath}`, error);
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
foldSignatures.delete(normalizeFsPath(filePath));
|
|
3709
|
-
addSurvivor({
|
|
1780
|
+
const previousDependencies = [...state.dependenciesByModule.get(id) ?? []];
|
|
1781
|
+
applyTransformArtifact(state, sealTransformArtifact(environmentName(this), {
|
|
1782
|
+
version: TRANSFORM_ARTIFACT_VERSION,
|
|
1783
|
+
moduleId: id,
|
|
3710
1784
|
file: filePath,
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
1785
|
+
folded: 0,
|
|
1786
|
+
skipped: [["compile-failed", 1]],
|
|
1787
|
+
survivors: [{
|
|
1788
|
+
line: 1,
|
|
1789
|
+
name: "compiler",
|
|
1790
|
+
reason: "compile-failed"
|
|
1791
|
+
}],
|
|
1792
|
+
transformedFile: false,
|
|
1793
|
+
classNames: [],
|
|
1794
|
+
dependencies: previousDependencies
|
|
1795
|
+
}), id, environmentName(this));
|
|
1796
|
+
state.foldSignatures.delete(id);
|
|
3715
1797
|
if (command === "serve") throw asError(error, `failed to compile ${filePath}`);
|
|
3716
1798
|
return null;
|
|
3717
1799
|
}
|
|
3718
|
-
|
|
3719
|
-
for (const entry of result.skipped)
|
|
3720
|
-
|
|
3721
|
-
skippedHere.set(entry.reason, (skippedHere.get(entry.reason) ?? 0) + 1);
|
|
3722
|
-
}
|
|
3723
|
-
perFile.set(filePath, {
|
|
3724
|
-
folded: result.folded.length,
|
|
3725
|
-
skipped: skippedHere
|
|
3726
|
-
});
|
|
3727
|
-
if (result.folded.some((entry) => entry.kind === "class" || entry.kind === "slots")) staticSession.transformedFiles.add((0, node_path.resolve)(filePath));
|
|
3728
|
-
for (const entry of result.folded) for (const className of entry.classNames) staticSession.markClassUsed(className);
|
|
1800
|
+
const skippedHere = /* @__PURE__ */ new Map();
|
|
1801
|
+
for (const entry of result.skipped) skippedHere.set(entry.reason, (skippedHere.get(entry.reason) ?? 0) + 1);
|
|
1802
|
+
const survivorsHere = [];
|
|
3729
1803
|
for (const entry of result.skipped) {
|
|
3730
1804
|
if (entry.reason === "not-imported" || entry.reason === "overlapping") continue;
|
|
3731
1805
|
if (entry.name === "cx" && entry.reason === "dynamic") continue;
|
|
3732
|
-
|
|
3733
|
-
file: filePath,
|
|
1806
|
+
survivorsHere.push({
|
|
3734
1807
|
line: lineAt(code, entry.start),
|
|
3735
1808
|
name: entry.name,
|
|
3736
1809
|
reason: entry.reason
|
|
3737
1810
|
});
|
|
3738
1811
|
}
|
|
1812
|
+
const artifact = sealTransformArtifact(environmentName(this), {
|
|
1813
|
+
version: TRANSFORM_ARTIFACT_VERSION,
|
|
1814
|
+
moduleId: id,
|
|
1815
|
+
file: filePath,
|
|
1816
|
+
folded: result.folded.length,
|
|
1817
|
+
skipped: [...skippedHere],
|
|
1818
|
+
survivors: survivorsHere,
|
|
1819
|
+
transformedFile: result.folded.some((entry) => entry.kind === "class" || entry.kind === "slots"),
|
|
1820
|
+
classNames: [...new Set(result.folded.flatMap((entry) => entry.classNames))],
|
|
1821
|
+
dependencies: [...result.dependencies],
|
|
1822
|
+
...result.dependencies.length ? { signature: {
|
|
1823
|
+
input: inputDigest ??= digest(code),
|
|
1824
|
+
output: digest(result.code),
|
|
1825
|
+
path: filePath
|
|
1826
|
+
} } : {}
|
|
1827
|
+
});
|
|
1828
|
+
applyTransformArtifact(state, artifact, id, environmentName(this));
|
|
3739
1829
|
if (reportSkipped && result.skipped.length) _bamboocss_logger.logger.info("vite:transform", formatSkipped(filePath, result.skipped));
|
|
3740
1830
|
for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
});
|
|
3748
|
-
else foldSignatures.delete(dependentKey);
|
|
3749
|
-
const forFile = survivorsByFile.get(filePath);
|
|
3750
|
-
if (command === "serve" && forFile?.length) {
|
|
3751
|
-
foldSignatures.delete(dependentKey);
|
|
3752
|
-
throw createSurvivorError(forFile);
|
|
1831
|
+
if (command === "serve" && artifact.survivors.length) {
|
|
1832
|
+
state.foldSignatures.delete(id);
|
|
1833
|
+
throw createSurvivorError(artifact.survivors.map((survivor) => ({
|
|
1834
|
+
file: filePath,
|
|
1835
|
+
...survivor
|
|
1836
|
+
})));
|
|
3753
1837
|
}
|
|
3754
|
-
|
|
1838
|
+
const meta = { [TRANSFORM_META_KEY]: artifact };
|
|
1839
|
+
if (!result.folded.length) return typeof this.getModuleInfo === "function" ? {
|
|
1840
|
+
code,
|
|
1841
|
+
map: null,
|
|
1842
|
+
meta
|
|
1843
|
+
} : null;
|
|
3755
1844
|
_bamboocss_logger.logger.debug("vite:transform", `Compiled ${result.folded.length} call(s) in ${filePath}`);
|
|
3756
1845
|
return {
|
|
3757
1846
|
code: result.code,
|
|
3758
|
-
map: result.map
|
|
1847
|
+
map: result.map,
|
|
1848
|
+
meta
|
|
3759
1849
|
};
|
|
3760
1850
|
},
|
|
3761
|
-
buildEnd() {
|
|
3762
|
-
const
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
1851
|
+
buildEnd(buildError) {
|
|
1852
|
+
const environment = environmentName(this);
|
|
1853
|
+
const state = environmentState(this);
|
|
1854
|
+
if (buildError) {
|
|
1855
|
+
rollbackEnvironmentGeneration(environment, state);
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
try {
|
|
1859
|
+
replayCachedTransformArtifacts(this);
|
|
1860
|
+
let currentWillEmitCss = false;
|
|
1861
|
+
if (typeof this.getModuleIds === "function") {
|
|
1862
|
+
state.cssLoaded = [...this.getModuleIds()].some((id) => id.split("?")[0] === `\0${VIRTUAL_CSS_ID}`);
|
|
1863
|
+
currentWillEmitCss = state.cssLoaded && (this.environment?.config?.build?.emitAssets ?? defaultEmitAssets);
|
|
1864
|
+
}
|
|
1865
|
+
const states = contributionStates(environment, state);
|
|
1866
|
+
rebuildStaticTransformContributions(environment, state);
|
|
1867
|
+
const survivors = allSurvivors(states);
|
|
1868
|
+
if (survivors.length) throw createSurvivorError(survivors);
|
|
1869
|
+
const lost = currentWillEmitCss ? [] : [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(require_class_name.bare(className)));
|
|
1870
|
+
if (lost.length) throw new Error(`bamboocss: ${lost.length} class(es) compiled in the ${JSON.stringify(environment)} environment were already pruned out of a stylesheet emitted by an earlier one. Elements carrying them would render unstyled.\n\n${(0, _bamboocss_shared.truncateList)(lost.map((className) => ` ${className}`), {
|
|
3768
1871
|
unit: "class",
|
|
3769
1872
|
separator: "\n"
|
|
3770
|
-
})}\n\nThe stylesheet is finalized by the environment that imports it, so
|
|
1873
|
+
})}\n\nThe stylesheet is finalized by the environment that imports it — the client, which builds first — so it is pruned against what that environment compiled. These classes are reached only from here, so no rule for them survived.\n\nThat usually means a styled component which renders only on the server. Either give the client a path to it, or set \`bamboocss({ pruneCss: false })\` to ship the whole extracted stylesheet.`);
|
|
1874
|
+
const remaining = remainingEnvironments(staticSession, environment);
|
|
1875
|
+
if (typeof this.getModuleInfo === "function" && !remaining.length) {
|
|
1876
|
+
if (!staticSession.cssLoaded) throw new Error(`bamboocss: compiled class values were produced, but ${JSON.stringify(VIRTUAL_CSS_ID)} was not imported. Add \`import ${JSON.stringify(VIRTUAL_CSS_ID)}\` once, from a JavaScript or TypeScript module in the application entry graph.\n\nIt has to be a JS import. \`@import\` from a stylesheet does not reach it: the id names a virtual module resolved by this plugin, and Vite resolves CSS \`@import\` before plugin resolution, so it fails as an unresolvable path. A project that ships one preloaded stylesheet imports this from its entry module instead, and lets Vite emit the CSS asset.`);
|
|
1877
|
+
const outsideExtraction = [...staticSession.transformedFiles].filter((file) => !staticSession.extractedFiles.has(file));
|
|
1878
|
+
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}`), {
|
|
1879
|
+
unit: "file",
|
|
1880
|
+
separator: "\n"
|
|
1881
|
+
})}\n\nAdd them to \`include\` in bamboo.config, or no CSS rule can back their emitted classes.`);
|
|
1882
|
+
}
|
|
1883
|
+
if (reportSummary && (command !== "build" || !remaining.length)) reportTransformCoverage(states);
|
|
1884
|
+
if (command === "serve" || !outputTokensByEnvironment.get(environment)?.size) completeEnvironmentGeneration(environment, state);
|
|
1885
|
+
else prepareEnvironmentGeneration(environment, state);
|
|
1886
|
+
} catch (error) {
|
|
1887
|
+
rollbackEnvironmentGeneration(environment, state);
|
|
1888
|
+
throw error;
|
|
3771
1889
|
}
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
1890
|
+
},
|
|
1891
|
+
renderError() {
|
|
1892
|
+
const environment = environmentName(this);
|
|
1893
|
+
const state = transformStateByEnvironment.get(environment);
|
|
1894
|
+
if (state) rollbackEnvironmentGeneration(environment, state);
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
const outputWriteObserver = {
|
|
1898
|
+
name: "bamboocss:output-write-observer",
|
|
1899
|
+
enforce: "pre",
|
|
1900
|
+
sharedDuringBuild: true,
|
|
1901
|
+
buildStart: {
|
|
1902
|
+
order: "pre",
|
|
1903
|
+
sequential: true,
|
|
1904
|
+
handler() {
|
|
1905
|
+
observeEnvironmentBuildStart(environmentName(this));
|
|
1906
|
+
}
|
|
1907
|
+
},
|
|
1908
|
+
writeBundle: {
|
|
1909
|
+
order: "pre",
|
|
1910
|
+
sequential: true,
|
|
1911
|
+
handler(outputOptions, bundle) {
|
|
1912
|
+
const identity = outputIdentityByBundle.get(bundle) ?? outputIdentityByOptions.get(outputOptions);
|
|
1913
|
+
if (identity?.environment === environmentName(this)) publishPreparedOutput(identity.environment, identity.outputToken, identity.outputSlot, true);
|
|
3779
1914
|
}
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
const memoryOutputCommitter = {
|
|
1918
|
+
name: "bamboocss:output-memory-committer",
|
|
1919
|
+
enforce: "post",
|
|
1920
|
+
sharedDuringBuild: true,
|
|
1921
|
+
closeBundle: {
|
|
1922
|
+
order: "post",
|
|
1923
|
+
sequential: true,
|
|
1924
|
+
handler() {
|
|
1925
|
+
closePreparedMemoryOutputs(environmentName(this));
|
|
3789
1926
|
}
|
|
3790
|
-
const declined = Array.from(skipped.values()).reduce((sum, count) => sum + count, 0);
|
|
3791
|
-
const total = folded + declined;
|
|
3792
|
-
if (!total) return;
|
|
3793
|
-
const share = Math.round(folded / total * 100);
|
|
3794
|
-
const reasons = Array.from(skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
|
|
3795
|
-
_bamboocss_logger.logger.info("vite:transform", `Compiled ${folded}/${total} (${share}%) across ${filesWithFolds}/${perFile.size} files` + (reasons ? ` — declined: ${reasons}` : ""));
|
|
3796
1927
|
}
|
|
3797
|
-
}
|
|
1928
|
+
};
|
|
1929
|
+
return [bamboocssCss({
|
|
1930
|
+
configPath,
|
|
1931
|
+
cwd,
|
|
1932
|
+
session: staticSession,
|
|
1933
|
+
pruneCss
|
|
1934
|
+
}), compiler];
|
|
3798
1935
|
};
|
|
3799
1936
|
//#endregion
|
|
3800
1937
|
exports.VIRTUAL_CSS_ID = VIRTUAL_CSS_ID;
|