@bamboocss/vite 1.37.11 → 1.37.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +144 -48
- package/dist/index.mjs +144 -48
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -94,6 +94,7 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
|
|
|
94
94
|
if (!className || !prunable.has(className) || used.has(className)) return;
|
|
95
95
|
candidate.remove();
|
|
96
96
|
removedAny = true;
|
|
97
|
+
session.prunedClasses.add(className);
|
|
97
98
|
});
|
|
98
99
|
}).processSync(rule.selector);
|
|
99
100
|
} catch {
|
|
@@ -154,6 +155,56 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
|
|
|
154
155
|
return root.toString();
|
|
155
156
|
};
|
|
156
157
|
//#endregion
|
|
158
|
+
//#region src/static-session.ts
|
|
159
|
+
const createStaticCompilationSession = () => {
|
|
160
|
+
const session = {
|
|
161
|
+
utilityLayer: "utilities",
|
|
162
|
+
sourcemap: false,
|
|
163
|
+
cssLoaded: false,
|
|
164
|
+
transformedFiles: /* @__PURE__ */ new Set(),
|
|
165
|
+
extractedFiles: /* @__PURE__ */ new Set(),
|
|
166
|
+
prunableClasses: /* @__PURE__ */ new Set(),
|
|
167
|
+
viewTransitionClasses: /* @__PURE__ */ new Set(),
|
|
168
|
+
usedClasses: /* @__PURE__ */ new Set(),
|
|
169
|
+
expectedEnvironments: void 0,
|
|
170
|
+
startedEnvironments: /* @__PURE__ */ new Set(),
|
|
171
|
+
prunedClasses: /* @__PURE__ */ new Set(),
|
|
172
|
+
markClassUsed(className) {
|
|
173
|
+
for (const token of className.split(" ")) {
|
|
174
|
+
if (!token) continue;
|
|
175
|
+
session.usedClasses.add(token.includes("\\") ? token : (0, _bamboocss_shared.esc)(token));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
return session;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Environments this run intends to build that have not been compiled yet.
|
|
183
|
+
*
|
|
184
|
+
* Empty means everything the run will contribute has been contributed, which is the condition
|
|
185
|
+
* every whole-run judgement here waits for: pruning the stylesheet against reachability, and
|
|
186
|
+
* the two guards that ask whether the compiled modules and the extraction graph agree. Each of
|
|
187
|
+
* those is false about a build in progress and true only about a finished one.
|
|
188
|
+
*
|
|
189
|
+
* Empty is also the answer for a single-environment build, where nothing announced an
|
|
190
|
+
* environment list because there is only ever one — so that path is unchanged.
|
|
191
|
+
*
|
|
192
|
+
* An environment a run declares and then never builds leaves this permanently non-empty, and
|
|
193
|
+
* those judgements are skipped for the run. Every one of them errs towards shipping more CSS
|
|
194
|
+
* or asserting less, so that is the safe direction to be wrong in.
|
|
195
|
+
*/
|
|
196
|
+
const remainingEnvironments = (session) => [...session.expectedEnvironments ?? []].filter((name) => !session.startedEnvironments.has(name));
|
|
197
|
+
const resetStaticCompilationSession = (session) => {
|
|
198
|
+
session.cssLoaded = false;
|
|
199
|
+
session.transformedFiles.clear();
|
|
200
|
+
session.extractedFiles.clear();
|
|
201
|
+
session.prunableClasses.clear();
|
|
202
|
+
session.viewTransitionClasses.clear();
|
|
203
|
+
session.usedClasses.clear();
|
|
204
|
+
session.startedEnvironments.clear();
|
|
205
|
+
session.prunedClasses.clear();
|
|
206
|
+
};
|
|
207
|
+
//#endregion
|
|
157
208
|
//#region src/css.ts
|
|
158
209
|
/**
|
|
159
210
|
* What a project imports to get the stylesheet.
|
|
@@ -262,11 +313,15 @@ const carriesGeneratedCss = (output) => output.type === "asset" && output.fileNa
|
|
|
262
313
|
* hash is not cosmetic: it makes late graph reachability cache-safe.
|
|
263
314
|
*/
|
|
264
315
|
const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
265
|
-
const { rename = true } = options;
|
|
316
|
+
const { rename = true, prune = true, sourcemap = session.sourcemap } = options;
|
|
317
|
+
/** Assets in this bundle that carry the generated stylesheet, pruned or not. */
|
|
318
|
+
let sheets = 0;
|
|
266
319
|
for (const output of Object.values(bundle)) {
|
|
267
320
|
if (!carriesGeneratedCss(output)) continue;
|
|
268
321
|
const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString();
|
|
269
322
|
if (!source.includes("--made-with-bamboo")) continue;
|
|
323
|
+
sheets++;
|
|
324
|
+
if (!prune) continue;
|
|
270
325
|
const optimized = pruneStaticCss(source, session);
|
|
271
326
|
output.source = optimized;
|
|
272
327
|
if (optimized === source) continue;
|
|
@@ -279,8 +334,9 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
|
279
334
|
if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
|
|
280
335
|
const previous = output.fileName;
|
|
281
336
|
output.fileName = nextName;
|
|
282
|
-
replaceAssetReferences(bundle, previous, nextName,
|
|
337
|
+
replaceAssetReferences(bundle, previous, nextName, sourcemap);
|
|
283
338
|
}
|
|
339
|
+
return { sheets };
|
|
284
340
|
};
|
|
285
341
|
/**
|
|
286
342
|
* Serve bamboo's stylesheet as a virtual module, in dev and in build.
|
|
@@ -356,9 +412,32 @@ const bamboocssCss = (options) => {
|
|
|
356
412
|
};
|
|
357
413
|
return {
|
|
358
414
|
name: "bamboocss:css",
|
|
415
|
+
/**
|
|
416
|
+
* One instance for every environment of a build, rather than one per environment.
|
|
417
|
+
*
|
|
418
|
+
* Vite re-reads the config file once per environment, so a project that lists this plugin
|
|
419
|
+
* in `vite.config.ts` — every project — got a *fresh* instance per environment, each with
|
|
420
|
+
* its own compilation session, context and ts-morph project. Nothing an environment
|
|
421
|
+
* established could then be seen by the next one, which is the premise the reachability
|
|
422
|
+
* accounting below is built on, and it also meant the whole config load and extraction
|
|
423
|
+
* happened once per environment.
|
|
424
|
+
*/
|
|
425
|
+
sharedDuringBuild: true,
|
|
359
426
|
configResolved(config) {
|
|
360
427
|
command = config.command;
|
|
361
428
|
session.sourcemap = config.build.sourcemap;
|
|
429
|
+
if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
|
|
430
|
+
},
|
|
431
|
+
/**
|
|
432
|
+
* The definitive environment list, for a run that reaches `builder.buildApp()` without
|
|
433
|
+
* configuring `builder` — the shape `vite build` itself takes, where exactly one
|
|
434
|
+
* environment is set up and pruning is therefore safe.
|
|
435
|
+
*/
|
|
436
|
+
buildApp: {
|
|
437
|
+
order: "pre",
|
|
438
|
+
async handler(builder) {
|
|
439
|
+
session.expectedEnvironments = new Set(Object.keys(builder.environments));
|
|
440
|
+
}
|
|
362
441
|
},
|
|
363
442
|
resolveId(id) {
|
|
364
443
|
const query = queryOf(id);
|
|
@@ -398,8 +477,34 @@ const bamboocssCss = (options) => {
|
|
|
398
477
|
generateBundle: {
|
|
399
478
|
order: "post",
|
|
400
479
|
handler(_, bundle) {
|
|
401
|
-
|
|
402
|
-
|
|
480
|
+
const environment = this.environment;
|
|
481
|
+
/**
|
|
482
|
+
* Every environment of this run has already had its modules compiled.
|
|
483
|
+
*
|
|
484
|
+
* Reachability is what pruning removes rules against, and it is only complete once
|
|
485
|
+
* nothing is left to contribute to it. The stylesheet is emitted and finalized by the
|
|
486
|
+
* environment that *imports* it, which in an SSR app is the client — and the client
|
|
487
|
+
* builds first, before the server environment has transformed a single module. Pruning
|
|
488
|
+
* there deletes every rule for a class only the server graph reaches, and the pages
|
|
489
|
+
* link the pruned copy: one project lost 39% of its atoms that way, presenting as
|
|
490
|
+
* rarely-used classes such as `md:{display:inline-block}` silently not applying.
|
|
491
|
+
*
|
|
492
|
+
* So the full extracted stylesheet ships instead, exactly as `renameCssAsset: false`
|
|
493
|
+
* already does. Being the last environment is not the common case — frameworks build
|
|
494
|
+
* the client first — but it is the only one where the answer is knowable, and a
|
|
495
|
+
* framework that builds its server bundle first does get pruned output.
|
|
496
|
+
*/
|
|
497
|
+
const pending = remainingEnvironments(session);
|
|
498
|
+
const { sheets } = optimizeStaticCssAssets(bundle, session, {
|
|
499
|
+
rename: renameCssAsset,
|
|
500
|
+
prune: pending.length === 0,
|
|
501
|
+
sourcemap: environment?.config?.build?.sourcemap
|
|
502
|
+
});
|
|
503
|
+
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, {
|
|
504
|
+
unit: "environment",
|
|
505
|
+
separator: ", "
|
|
506
|
+
})} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
|
|
507
|
+
if (!servedEnvironments.has(environment?.name ?? "default")) return;
|
|
403
508
|
if (!session.transformedFiles.size) return;
|
|
404
509
|
if (!Object.values(bundle).some((output) => {
|
|
405
510
|
if (!carriesGeneratedCss(output)) return false;
|
|
@@ -2562,7 +2667,21 @@ const foldSource = (options) => {
|
|
|
2562
2667
|
const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
|
|
2563
2668
|
if (!sourceFile) return;
|
|
2564
2669
|
const importedRecipeBindings = new Set(parserResult.importedRecipes?.keys() ?? []);
|
|
2565
|
-
|
|
2670
|
+
/**
|
|
2671
|
+
* Every identifier in the module, grouped by the name it spells — built at most once per
|
|
2672
|
+
* pass, and only when something below has a name to look up.
|
|
2673
|
+
*
|
|
2674
|
+
* One index answers for every binding, rather than a walk per binding. Built here rather
|
|
2675
|
+
* than cached across passes: it holds nodes, and a node does not outlive its source file
|
|
2676
|
+
* being replaced.
|
|
2677
|
+
*
|
|
2678
|
+
* Deferred because most modules in an app neither declare nor import a recipe, and the
|
|
2679
|
+
* walk is not cheap — `getDescendantsOfKind` wraps every identifier in the file in a
|
|
2680
|
+
* ts-morph node to answer a question those modules never ask. It was 11% of a 6,307-file
|
|
2681
|
+
* build, most of it spent producing an index nothing read.
|
|
2682
|
+
*/
|
|
2683
|
+
let identifiers;
|
|
2684
|
+
const identifiersByName = () => identifiers ??= identifierIndex(sourceFile);
|
|
2566
2685
|
for (const binding of new Set([...recipeConfigs.keys(), ...importedRecipeBindings])) {
|
|
2567
2686
|
const entry = recipeConfigs.get(binding);
|
|
2568
2687
|
if (entry === AMBIGUOUS) continue;
|
|
@@ -2570,7 +2689,7 @@ const foldSource = (options) => {
|
|
|
2570
2689
|
const definition = entry?.box?.getNode?.();
|
|
2571
2690
|
const nameNode = definition?.getSourceFile() === sourceFile ? definition?.getFirstAncestorByKind(ts_morph.SyntaxKind.VariableDeclaration)?.getNameNode() : importSpecifierFor(sourceFile, binding);
|
|
2572
2691
|
if (!nameNode || !ts_morph.Node.isIdentifier(nameNode)) continue;
|
|
2573
|
-
const references = localReferencesTo(
|
|
2692
|
+
const references = localReferencesTo(identifiersByName(), binding, nameNode);
|
|
2574
2693
|
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;
|
|
2575
2694
|
const survivor = references.find((ref) => !applied.some(([from, to]) => ref.getStart() >= from && ref.getStart() < to));
|
|
2576
2695
|
if (!survivor) continue;
|
|
@@ -2671,25 +2790,24 @@ const foldSource = (options) => {
|
|
|
2671
2790
|
}
|
|
2672
2791
|
if (watched.size === 0) return;
|
|
2673
2792
|
const declined = skipped.filter((entry) => SURVIVES_TO_RUNTIME.has(entry.reason) && entry.end > entry.start).map((entry) => [entry.start, entry.end]);
|
|
2674
|
-
const
|
|
2675
|
-
for (const identifier of
|
|
2676
|
-
const local = identifier.getText();
|
|
2677
|
-
const imported = watched.get(local);
|
|
2678
|
-
if (imported === void 0 || reported.has(local)) continue;
|
|
2793
|
+
const survivors = [];
|
|
2794
|
+
for (const [local, imported] of watched) for (const identifier of identifiersByName().get(local) ?? []) {
|
|
2679
2795
|
const start = identifier.getStart();
|
|
2680
2796
|
if (identifier.getFirstAncestorByKind(ts_morph.SyntaxKind.ImportDeclaration)) continue;
|
|
2681
2797
|
if (applied.some(([from, to]) => start >= from && start < to)) continue;
|
|
2682
2798
|
if (declined.some(([from, to]) => start >= from && start < to)) continue;
|
|
2683
2799
|
if (!isValueReference(identifier)) continue;
|
|
2684
2800
|
if (isShadowed(identifier, local)) continue;
|
|
2685
|
-
|
|
2686
|
-
skipped.push({
|
|
2801
|
+
survivors.push({
|
|
2687
2802
|
name: imported,
|
|
2688
2803
|
reason: "runtime-binding",
|
|
2689
2804
|
start,
|
|
2690
2805
|
end: identifier.getEnd()
|
|
2691
2806
|
});
|
|
2807
|
+
break;
|
|
2692
2808
|
}
|
|
2809
|
+
survivors.sort((a, b) => a.start - b.start);
|
|
2810
|
+
skipped.push(...survivors);
|
|
2693
2811
|
}
|
|
2694
2812
|
if (reportSurvivors) reportRuntimeBindings();
|
|
2695
2813
|
if (folded.length === 0) return {
|
|
@@ -2771,35 +2889,6 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
|
|
|
2771
2889
|
};
|
|
2772
2890
|
};
|
|
2773
2891
|
//#endregion
|
|
2774
|
-
//#region src/static-session.ts
|
|
2775
|
-
const createStaticCompilationSession = () => {
|
|
2776
|
-
const session = {
|
|
2777
|
-
utilityLayer: "utilities",
|
|
2778
|
-
sourcemap: false,
|
|
2779
|
-
cssLoaded: false,
|
|
2780
|
-
transformedFiles: /* @__PURE__ */ new Set(),
|
|
2781
|
-
extractedFiles: /* @__PURE__ */ new Set(),
|
|
2782
|
-
prunableClasses: /* @__PURE__ */ new Set(),
|
|
2783
|
-
viewTransitionClasses: /* @__PURE__ */ new Set(),
|
|
2784
|
-
usedClasses: /* @__PURE__ */ new Set(),
|
|
2785
|
-
markClassUsed(className) {
|
|
2786
|
-
for (const token of className.split(" ")) {
|
|
2787
|
-
if (!token) continue;
|
|
2788
|
-
session.usedClasses.add(token.includes("\\") ? token : (0, _bamboocss_shared.esc)(token));
|
|
2789
|
-
}
|
|
2790
|
-
}
|
|
2791
|
-
};
|
|
2792
|
-
return session;
|
|
2793
|
-
};
|
|
2794
|
-
const resetStaticCompilationSession = (session) => {
|
|
2795
|
-
session.cssLoaded = false;
|
|
2796
|
-
session.transformedFiles.clear();
|
|
2797
|
-
session.extractedFiles.clear();
|
|
2798
|
-
session.prunableClasses.clear();
|
|
2799
|
-
session.viewTransitionClasses.clear();
|
|
2800
|
-
session.usedClasses.clear();
|
|
2801
|
-
};
|
|
2802
|
-
//#endregion
|
|
2803
2892
|
//#region src/plugin.ts
|
|
2804
2893
|
const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
|
|
2805
2894
|
const NODE_MODULES = /node_modules/;
|
|
@@ -2853,8 +2942,6 @@ const formatSkipped = (id, skipped) => {
|
|
|
2853
2942
|
const bamboocss = (options = {}) => {
|
|
2854
2943
|
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, renameCssAsset = true } = options;
|
|
2855
2944
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
2856
|
-
/** Environments whose `buildStart` has run in the build currently in progress. */
|
|
2857
|
-
const seenEnvironments = /* @__PURE__ */ new Set();
|
|
2858
2945
|
/** Totals across the build, for the summary. */
|
|
2859
2946
|
const totals = {
|
|
2860
2947
|
folded: 0,
|
|
@@ -2928,13 +3015,14 @@ const bamboocss = (options = {}) => {
|
|
|
2928
3015
|
}), {
|
|
2929
3016
|
name: "bamboocss:compiler",
|
|
2930
3017
|
enforce: "pre",
|
|
3018
|
+
/** See the same declaration on the css plugin: one instance per build, not per environment. */
|
|
3019
|
+
sharedDuringBuild: true,
|
|
2931
3020
|
configResolved(config) {
|
|
2932
3021
|
command = config.command;
|
|
2933
3022
|
},
|
|
2934
3023
|
async buildStart() {
|
|
2935
3024
|
const environment = this.environment?.name ?? "default";
|
|
2936
|
-
if (
|
|
2937
|
-
seenEnvironments.clear();
|
|
3025
|
+
if (staticSession.startedEnvironments.has(environment)) {
|
|
2938
3026
|
totals.folded = 0;
|
|
2939
3027
|
totals.files = 0;
|
|
2940
3028
|
totals.filesWithFolds = 0;
|
|
@@ -2943,7 +3031,7 @@ const bamboocss = (options = {}) => {
|
|
|
2943
3031
|
recipeConfigCache.clear();
|
|
2944
3032
|
resetStaticCompilationSession(staticSession);
|
|
2945
3033
|
}
|
|
2946
|
-
|
|
3034
|
+
staticSession.startedEnvironments.add(environment);
|
|
2947
3035
|
try {
|
|
2948
3036
|
await ensureContext();
|
|
2949
3037
|
} catch (error) {
|
|
@@ -3054,7 +3142,15 @@ const bamboocss = (options = {}) => {
|
|
|
3054
3142
|
buildEnd() {
|
|
3055
3143
|
const survivors = allSurvivors();
|
|
3056
3144
|
if (survivors.length) throw createSurvivorError(survivors);
|
|
3057
|
-
|
|
3145
|
+
const lost = [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(bare(className)));
|
|
3146
|
+
if (lost.length) {
|
|
3147
|
+
const environment = this.environment?.name ?? "default";
|
|
3148
|
+
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}`), {
|
|
3149
|
+
unit: "class",
|
|
3150
|
+
separator: "\n"
|
|
3151
|
+
})}\n\nThe stylesheet is finalized by the environment that imports it, so pruning it is only safe once every environment has been compiled. This build did not say how many there would be: it called \`builder.build(environment)\` directly. Run it through \`vite build\`, call \`builder.buildApp()\`, or set \`builder: {}\` in the Vite config so the environments are known before the first one builds. \`bamboocss({ renameCssAsset: false })\` also turns pruning off entirely.`);
|
|
3152
|
+
}
|
|
3153
|
+
if (typeof this.getModuleInfo === "function" && !remainingEnvironments(staticSession).length) {
|
|
3058
3154
|
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.`);
|
|
3059
3155
|
const outsideExtraction = [...staticSession.transformedFiles].filter((file) => !staticSession.extractedFiles.has(file));
|
|
3060
3156
|
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}`), {
|
package/dist/index.mjs
CHANGED
|
@@ -64,6 +64,7 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
|
|
|
64
64
|
if (!className || !prunable.has(className) || used.has(className)) return;
|
|
65
65
|
candidate.remove();
|
|
66
66
|
removedAny = true;
|
|
67
|
+
session.prunedClasses.add(className);
|
|
67
68
|
});
|
|
68
69
|
}).processSync(rule.selector);
|
|
69
70
|
} catch {
|
|
@@ -124,6 +125,56 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
|
|
|
124
125
|
return root.toString();
|
|
125
126
|
};
|
|
126
127
|
//#endregion
|
|
128
|
+
//#region src/static-session.ts
|
|
129
|
+
const createStaticCompilationSession = () => {
|
|
130
|
+
const session = {
|
|
131
|
+
utilityLayer: "utilities",
|
|
132
|
+
sourcemap: false,
|
|
133
|
+
cssLoaded: false,
|
|
134
|
+
transformedFiles: /* @__PURE__ */ new Set(),
|
|
135
|
+
extractedFiles: /* @__PURE__ */ new Set(),
|
|
136
|
+
prunableClasses: /* @__PURE__ */ new Set(),
|
|
137
|
+
viewTransitionClasses: /* @__PURE__ */ new Set(),
|
|
138
|
+
usedClasses: /* @__PURE__ */ new Set(),
|
|
139
|
+
expectedEnvironments: void 0,
|
|
140
|
+
startedEnvironments: /* @__PURE__ */ new Set(),
|
|
141
|
+
prunedClasses: /* @__PURE__ */ new Set(),
|
|
142
|
+
markClassUsed(className) {
|
|
143
|
+
for (const token of className.split(" ")) {
|
|
144
|
+
if (!token) continue;
|
|
145
|
+
session.usedClasses.add(token.includes("\\") ? token : esc(token));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return session;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Environments this run intends to build that have not been compiled yet.
|
|
153
|
+
*
|
|
154
|
+
* Empty means everything the run will contribute has been contributed, which is the condition
|
|
155
|
+
* every whole-run judgement here waits for: pruning the stylesheet against reachability, and
|
|
156
|
+
* the two guards that ask whether the compiled modules and the extraction graph agree. Each of
|
|
157
|
+
* those is false about a build in progress and true only about a finished one.
|
|
158
|
+
*
|
|
159
|
+
* Empty is also the answer for a single-environment build, where nothing announced an
|
|
160
|
+
* environment list because there is only ever one — so that path is unchanged.
|
|
161
|
+
*
|
|
162
|
+
* An environment a run declares and then never builds leaves this permanently non-empty, and
|
|
163
|
+
* those judgements are skipped for the run. Every one of them errs towards shipping more CSS
|
|
164
|
+
* or asserting less, so that is the safe direction to be wrong in.
|
|
165
|
+
*/
|
|
166
|
+
const remainingEnvironments = (session) => [...session.expectedEnvironments ?? []].filter((name) => !session.startedEnvironments.has(name));
|
|
167
|
+
const resetStaticCompilationSession = (session) => {
|
|
168
|
+
session.cssLoaded = false;
|
|
169
|
+
session.transformedFiles.clear();
|
|
170
|
+
session.extractedFiles.clear();
|
|
171
|
+
session.prunableClasses.clear();
|
|
172
|
+
session.viewTransitionClasses.clear();
|
|
173
|
+
session.usedClasses.clear();
|
|
174
|
+
session.startedEnvironments.clear();
|
|
175
|
+
session.prunedClasses.clear();
|
|
176
|
+
};
|
|
177
|
+
//#endregion
|
|
127
178
|
//#region src/css.ts
|
|
128
179
|
/**
|
|
129
180
|
* What a project imports to get the stylesheet.
|
|
@@ -232,11 +283,15 @@ const carriesGeneratedCss = (output) => output.type === "asset" && output.fileNa
|
|
|
232
283
|
* hash is not cosmetic: it makes late graph reachability cache-safe.
|
|
233
284
|
*/
|
|
234
285
|
const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
235
|
-
const { rename = true } = options;
|
|
286
|
+
const { rename = true, prune = true, sourcemap = session.sourcemap } = options;
|
|
287
|
+
/** Assets in this bundle that carry the generated stylesheet, pruned or not. */
|
|
288
|
+
let sheets = 0;
|
|
236
289
|
for (const output of Object.values(bundle)) {
|
|
237
290
|
if (!carriesGeneratedCss(output)) continue;
|
|
238
291
|
const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString();
|
|
239
292
|
if (!source.includes("--made-with-bamboo")) continue;
|
|
293
|
+
sheets++;
|
|
294
|
+
if (!prune) continue;
|
|
240
295
|
const optimized = pruneStaticCss(source, session);
|
|
241
296
|
output.source = optimized;
|
|
242
297
|
if (optimized === source) continue;
|
|
@@ -249,8 +304,9 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
|
249
304
|
if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
|
|
250
305
|
const previous = output.fileName;
|
|
251
306
|
output.fileName = nextName;
|
|
252
|
-
replaceAssetReferences(bundle, previous, nextName,
|
|
307
|
+
replaceAssetReferences(bundle, previous, nextName, sourcemap);
|
|
253
308
|
}
|
|
309
|
+
return { sheets };
|
|
254
310
|
};
|
|
255
311
|
/**
|
|
256
312
|
* Serve bamboo's stylesheet as a virtual module, in dev and in build.
|
|
@@ -326,9 +382,32 @@ const bamboocssCss = (options) => {
|
|
|
326
382
|
};
|
|
327
383
|
return {
|
|
328
384
|
name: "bamboocss:css",
|
|
385
|
+
/**
|
|
386
|
+
* One instance for every environment of a build, rather than one per environment.
|
|
387
|
+
*
|
|
388
|
+
* Vite re-reads the config file once per environment, so a project that lists this plugin
|
|
389
|
+
* in `vite.config.ts` — every project — got a *fresh* instance per environment, each with
|
|
390
|
+
* its own compilation session, context and ts-morph project. Nothing an environment
|
|
391
|
+
* established could then be seen by the next one, which is the premise the reachability
|
|
392
|
+
* accounting below is built on, and it also meant the whole config load and extraction
|
|
393
|
+
* happened once per environment.
|
|
394
|
+
*/
|
|
395
|
+
sharedDuringBuild: true,
|
|
329
396
|
configResolved(config) {
|
|
330
397
|
command = config.command;
|
|
331
398
|
session.sourcemap = config.build.sourcemap;
|
|
399
|
+
if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
|
|
400
|
+
},
|
|
401
|
+
/**
|
|
402
|
+
* The definitive environment list, for a run that reaches `builder.buildApp()` without
|
|
403
|
+
* configuring `builder` — the shape `vite build` itself takes, where exactly one
|
|
404
|
+
* environment is set up and pruning is therefore safe.
|
|
405
|
+
*/
|
|
406
|
+
buildApp: {
|
|
407
|
+
order: "pre",
|
|
408
|
+
async handler(builder) {
|
|
409
|
+
session.expectedEnvironments = new Set(Object.keys(builder.environments));
|
|
410
|
+
}
|
|
332
411
|
},
|
|
333
412
|
resolveId(id) {
|
|
334
413
|
const query = queryOf(id);
|
|
@@ -368,8 +447,34 @@ const bamboocssCss = (options) => {
|
|
|
368
447
|
generateBundle: {
|
|
369
448
|
order: "post",
|
|
370
449
|
handler(_, bundle) {
|
|
371
|
-
|
|
372
|
-
|
|
450
|
+
const environment = this.environment;
|
|
451
|
+
/**
|
|
452
|
+
* Every environment of this run has already had its modules compiled.
|
|
453
|
+
*
|
|
454
|
+
* Reachability is what pruning removes rules against, and it is only complete once
|
|
455
|
+
* nothing is left to contribute to it. The stylesheet is emitted and finalized by the
|
|
456
|
+
* environment that *imports* it, which in an SSR app is the client — and the client
|
|
457
|
+
* builds first, before the server environment has transformed a single module. Pruning
|
|
458
|
+
* there deletes every rule for a class only the server graph reaches, and the pages
|
|
459
|
+
* link the pruned copy: one project lost 39% of its atoms that way, presenting as
|
|
460
|
+
* rarely-used classes such as `md:{display:inline-block}` silently not applying.
|
|
461
|
+
*
|
|
462
|
+
* So the full extracted stylesheet ships instead, exactly as `renameCssAsset: false`
|
|
463
|
+
* already does. Being the last environment is not the common case — frameworks build
|
|
464
|
+
* the client first — but it is the only one where the answer is knowable, and a
|
|
465
|
+
* framework that builds its server bundle first does get pruned output.
|
|
466
|
+
*/
|
|
467
|
+
const pending = remainingEnvironments(session);
|
|
468
|
+
const { sheets } = optimizeStaticCssAssets(bundle, session, {
|
|
469
|
+
rename: renameCssAsset,
|
|
470
|
+
prune: pending.length === 0,
|
|
471
|
+
sourcemap: environment?.config?.build?.sourcemap
|
|
472
|
+
});
|
|
473
|
+
if (sheets && pending.length) logger.info("vite", `Reachability pruning skipped: the stylesheet is emitted by the ${JSON.stringify(environment?.name ?? "default")} environment, and ${truncateList(pending, {
|
|
474
|
+
unit: "environment",
|
|
475
|
+
separator: ", "
|
|
476
|
+
})} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
|
|
477
|
+
if (!servedEnvironments.has(environment?.name ?? "default")) return;
|
|
373
478
|
if (!session.transformedFiles.size) return;
|
|
374
479
|
if (!Object.values(bundle).some((output) => {
|
|
375
480
|
if (!carriesGeneratedCss(output)) return false;
|
|
@@ -2532,7 +2637,21 @@ const foldSource = (options) => {
|
|
|
2532
2637
|
const sourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
|
|
2533
2638
|
if (!sourceFile) return;
|
|
2534
2639
|
const importedRecipeBindings = new Set(parserResult.importedRecipes?.keys() ?? []);
|
|
2535
|
-
|
|
2640
|
+
/**
|
|
2641
|
+
* Every identifier in the module, grouped by the name it spells — built at most once per
|
|
2642
|
+
* pass, and only when something below has a name to look up.
|
|
2643
|
+
*
|
|
2644
|
+
* One index answers for every binding, rather than a walk per binding. Built here rather
|
|
2645
|
+
* than cached across passes: it holds nodes, and a node does not outlive its source file
|
|
2646
|
+
* being replaced.
|
|
2647
|
+
*
|
|
2648
|
+
* Deferred because most modules in an app neither declare nor import a recipe, and the
|
|
2649
|
+
* walk is not cheap — `getDescendantsOfKind` wraps every identifier in the file in a
|
|
2650
|
+
* ts-morph node to answer a question those modules never ask. It was 11% of a 6,307-file
|
|
2651
|
+
* build, most of it spent producing an index nothing read.
|
|
2652
|
+
*/
|
|
2653
|
+
let identifiers;
|
|
2654
|
+
const identifiersByName = () => identifiers ??= identifierIndex(sourceFile);
|
|
2536
2655
|
for (const binding of new Set([...recipeConfigs.keys(), ...importedRecipeBindings])) {
|
|
2537
2656
|
const entry = recipeConfigs.get(binding);
|
|
2538
2657
|
if (entry === AMBIGUOUS) continue;
|
|
@@ -2540,7 +2659,7 @@ const foldSource = (options) => {
|
|
|
2540
2659
|
const definition = entry?.box?.getNode?.();
|
|
2541
2660
|
const nameNode = definition?.getSourceFile() === sourceFile ? definition?.getFirstAncestorByKind(SyntaxKind.VariableDeclaration)?.getNameNode() : importSpecifierFor(sourceFile, binding);
|
|
2542
2661
|
if (!nameNode || !Node.isIdentifier(nameNode)) continue;
|
|
2543
|
-
const references = localReferencesTo(
|
|
2662
|
+
const references = localReferencesTo(identifiersByName(), binding, nameNode);
|
|
2544
2663
|
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;
|
|
2545
2664
|
const survivor = references.find((ref) => !applied.some(([from, to]) => ref.getStart() >= from && ref.getStart() < to));
|
|
2546
2665
|
if (!survivor) continue;
|
|
@@ -2641,25 +2760,24 @@ const foldSource = (options) => {
|
|
|
2641
2760
|
}
|
|
2642
2761
|
if (watched.size === 0) return;
|
|
2643
2762
|
const declined = skipped.filter((entry) => SURVIVES_TO_RUNTIME.has(entry.reason) && entry.end > entry.start).map((entry) => [entry.start, entry.end]);
|
|
2644
|
-
const
|
|
2645
|
-
for (const identifier of
|
|
2646
|
-
const local = identifier.getText();
|
|
2647
|
-
const imported = watched.get(local);
|
|
2648
|
-
if (imported === void 0 || reported.has(local)) continue;
|
|
2763
|
+
const survivors = [];
|
|
2764
|
+
for (const [local, imported] of watched) for (const identifier of identifiersByName().get(local) ?? []) {
|
|
2649
2765
|
const start = identifier.getStart();
|
|
2650
2766
|
if (identifier.getFirstAncestorByKind(SyntaxKind.ImportDeclaration)) continue;
|
|
2651
2767
|
if (applied.some(([from, to]) => start >= from && start < to)) continue;
|
|
2652
2768
|
if (declined.some(([from, to]) => start >= from && start < to)) continue;
|
|
2653
2769
|
if (!isValueReference(identifier)) continue;
|
|
2654
2770
|
if (isShadowed(identifier, local)) continue;
|
|
2655
|
-
|
|
2656
|
-
skipped.push({
|
|
2771
|
+
survivors.push({
|
|
2657
2772
|
name: imported,
|
|
2658
2773
|
reason: "runtime-binding",
|
|
2659
2774
|
start,
|
|
2660
2775
|
end: identifier.getEnd()
|
|
2661
2776
|
});
|
|
2777
|
+
break;
|
|
2662
2778
|
}
|
|
2779
|
+
survivors.sort((a, b) => a.start - b.start);
|
|
2780
|
+
skipped.push(...survivors);
|
|
2663
2781
|
}
|
|
2664
2782
|
if (reportSurvivors) reportRuntimeBindings();
|
|
2665
2783
|
if (folded.length === 0) return {
|
|
@@ -2741,35 +2859,6 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
|
|
|
2741
2859
|
};
|
|
2742
2860
|
};
|
|
2743
2861
|
//#endregion
|
|
2744
|
-
//#region src/static-session.ts
|
|
2745
|
-
const createStaticCompilationSession = () => {
|
|
2746
|
-
const session = {
|
|
2747
|
-
utilityLayer: "utilities",
|
|
2748
|
-
sourcemap: false,
|
|
2749
|
-
cssLoaded: false,
|
|
2750
|
-
transformedFiles: /* @__PURE__ */ new Set(),
|
|
2751
|
-
extractedFiles: /* @__PURE__ */ new Set(),
|
|
2752
|
-
prunableClasses: /* @__PURE__ */ new Set(),
|
|
2753
|
-
viewTransitionClasses: /* @__PURE__ */ new Set(),
|
|
2754
|
-
usedClasses: /* @__PURE__ */ new Set(),
|
|
2755
|
-
markClassUsed(className) {
|
|
2756
|
-
for (const token of className.split(" ")) {
|
|
2757
|
-
if (!token) continue;
|
|
2758
|
-
session.usedClasses.add(token.includes("\\") ? token : esc(token));
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
};
|
|
2762
|
-
return session;
|
|
2763
|
-
};
|
|
2764
|
-
const resetStaticCompilationSession = (session) => {
|
|
2765
|
-
session.cssLoaded = false;
|
|
2766
|
-
session.transformedFiles.clear();
|
|
2767
|
-
session.extractedFiles.clear();
|
|
2768
|
-
session.prunableClasses.clear();
|
|
2769
|
-
session.viewTransitionClasses.clear();
|
|
2770
|
-
session.usedClasses.clear();
|
|
2771
|
-
};
|
|
2772
|
-
//#endregion
|
|
2773
2862
|
//#region src/plugin.ts
|
|
2774
2863
|
const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
|
|
2775
2864
|
const NODE_MODULES = /node_modules/;
|
|
@@ -2823,8 +2912,6 @@ const formatSkipped = (id, skipped) => {
|
|
|
2823
2912
|
const bamboocss = (options = {}) => {
|
|
2824
2913
|
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, renameCssAsset = true } = options;
|
|
2825
2914
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
2826
|
-
/** Environments whose `buildStart` has run in the build currently in progress. */
|
|
2827
|
-
const seenEnvironments = /* @__PURE__ */ new Set();
|
|
2828
2915
|
/** Totals across the build, for the summary. */
|
|
2829
2916
|
const totals = {
|
|
2830
2917
|
folded: 0,
|
|
@@ -2898,13 +2985,14 @@ const bamboocss = (options = {}) => {
|
|
|
2898
2985
|
}), {
|
|
2899
2986
|
name: "bamboocss:compiler",
|
|
2900
2987
|
enforce: "pre",
|
|
2988
|
+
/** See the same declaration on the css plugin: one instance per build, not per environment. */
|
|
2989
|
+
sharedDuringBuild: true,
|
|
2901
2990
|
configResolved(config) {
|
|
2902
2991
|
command = config.command;
|
|
2903
2992
|
},
|
|
2904
2993
|
async buildStart() {
|
|
2905
2994
|
const environment = this.environment?.name ?? "default";
|
|
2906
|
-
if (
|
|
2907
|
-
seenEnvironments.clear();
|
|
2995
|
+
if (staticSession.startedEnvironments.has(environment)) {
|
|
2908
2996
|
totals.folded = 0;
|
|
2909
2997
|
totals.files = 0;
|
|
2910
2998
|
totals.filesWithFolds = 0;
|
|
@@ -2913,7 +3001,7 @@ const bamboocss = (options = {}) => {
|
|
|
2913
3001
|
recipeConfigCache.clear();
|
|
2914
3002
|
resetStaticCompilationSession(staticSession);
|
|
2915
3003
|
}
|
|
2916
|
-
|
|
3004
|
+
staticSession.startedEnvironments.add(environment);
|
|
2917
3005
|
try {
|
|
2918
3006
|
await ensureContext();
|
|
2919
3007
|
} catch (error) {
|
|
@@ -3024,7 +3112,15 @@ const bamboocss = (options = {}) => {
|
|
|
3024
3112
|
buildEnd() {
|
|
3025
3113
|
const survivors = allSurvivors();
|
|
3026
3114
|
if (survivors.length) throw createSurvivorError(survivors);
|
|
3027
|
-
|
|
3115
|
+
const lost = [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(bare(className)));
|
|
3116
|
+
if (lost.length) {
|
|
3117
|
+
const environment = this.environment?.name ?? "default";
|
|
3118
|
+
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${truncateList(lost.map((className) => ` ${className}`), {
|
|
3119
|
+
unit: "class",
|
|
3120
|
+
separator: "\n"
|
|
3121
|
+
})}\n\nThe stylesheet is finalized by the environment that imports it, so pruning it is only safe once every environment has been compiled. This build did not say how many there would be: it called \`builder.build(environment)\` directly. Run it through \`vite build\`, call \`builder.buildApp()\`, or set \`builder: {}\` in the Vite config so the environments are known before the first one builds. \`bamboocss({ renameCssAsset: false })\` also turns pruning off entirely.`);
|
|
3122
|
+
}
|
|
3123
|
+
if (typeof this.getModuleInfo === "function" && !remainingEnvironments(staticSession).length) {
|
|
3028
3124
|
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.`);
|
|
3029
3125
|
const outsideExtraction = [...staticSession.transformedFiles].filter((file) => !staticSession.extractedFiles.has(file));
|
|
3030
3126
|
if (outsideExtraction.length) throw new Error(`bamboocss: ${outsideExtraction.length} statically compiled module(s) are outside the CSS extraction graph:\n\n${truncateList(outsideExtraction.map((file) => ` ${file}`), {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/vite",
|
|
3
|
-
"version": "1.37.
|
|
3
|
+
"version": "1.37.13",
|
|
4
4
|
"description": "Vite integration for Bamboo CSS",
|
|
5
5
|
"homepage": "https://bamboocss.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,18 +40,18 @@
|
|
|
40
40
|
"postcss": "8.5.26",
|
|
41
41
|
"postcss-selector-parser": "7.1.5",
|
|
42
42
|
"ts-morph": "28.0.0",
|
|
43
|
-
"@bamboocss/
|
|
44
|
-
"@bamboocss/
|
|
45
|
-
"@bamboocss/extractor": "1.37.
|
|
46
|
-
"@bamboocss/
|
|
47
|
-
"@bamboocss/
|
|
48
|
-
"@bamboocss/
|
|
49
|
-
"@bamboocss/
|
|
43
|
+
"@bamboocss/config": "1.37.13",
|
|
44
|
+
"@bamboocss/core": "1.37.13",
|
|
45
|
+
"@bamboocss/extractor": "1.37.13",
|
|
46
|
+
"@bamboocss/logger": "1.37.13",
|
|
47
|
+
"@bamboocss/node": "1.37.13",
|
|
48
|
+
"@bamboocss/types": "1.37.13",
|
|
49
|
+
"@bamboocss/shared": "1.37.13"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
53
53
|
"vite": "7.2.6",
|
|
54
|
-
"@bamboocss/fixture": "1.37.
|
|
54
|
+
"@bamboocss/fixture": "1.37.13"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vite": ">=5"
|