@bamboocss/vite 1.37.13 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +176 -40
- package/dist/index.d.cts +21 -10
- package/dist/index.d.mts +21 -10
- package/dist/index.mjs +176 -40
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -306,14 +306,19 @@ const replaceAssetReferences = (bundle, previous, next, sourcemap) => {
|
|
|
306
306
|
*/
|
|
307
307
|
const carriesGeneratedCss = (output) => output.type === "asset" && output.fileName.endsWith(".css");
|
|
308
308
|
/**
|
|
309
|
-
* Prune
|
|
309
|
+
* Prune compiler-owned CSS, then give any sheet whose bytes changed a hash of those bytes.
|
|
310
310
|
*
|
|
311
311
|
* Rollup has already expanded `[hash]` when `generateBundle` runs. Mutating only `source`
|
|
312
312
|
* would therefore leave two different reachable subsets under one CDN key. The extra final
|
|
313
313
|
* hash is not cosmetic: it makes late graph reachability cache-safe.
|
|
314
|
+
*
|
|
315
|
+
* Renaming is therefore not a choice this takes. Pruned bytes under the unpruned sheet's name
|
|
316
|
+
* is the one outcome that must never be reachable, and a sheet nothing was removed from keeps
|
|
317
|
+
* its name because its bytes are unchanged — so "rename" is a consequence of "the bytes moved",
|
|
318
|
+
* not a second option. `prune` is the only knob.
|
|
314
319
|
*/
|
|
315
320
|
const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
316
|
-
const {
|
|
321
|
+
const { prune = true, sourcemap = session.sourcemap } = options;
|
|
317
322
|
/** Assets in this bundle that carry the generated stylesheet, pruned or not. */
|
|
318
323
|
let sheets = 0;
|
|
319
324
|
for (const output of Object.values(bundle)) {
|
|
@@ -325,12 +330,7 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
|
325
330
|
const optimized = pruneStaticCss(source, session);
|
|
326
331
|
output.source = optimized;
|
|
327
332
|
if (optimized === source) continue;
|
|
328
|
-
if (!rename) {
|
|
329
|
-
output.source = source;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
333
|
const nextName = output.fileName.replace(/\.css$/, `.b-${(0, _bamboocss_shared.toHash)(optimized)}.css`);
|
|
333
|
-
if (nextName === output.fileName) continue;
|
|
334
334
|
if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
|
|
335
335
|
const previous = output.fileName;
|
|
336
336
|
output.fileName = nextName;
|
|
@@ -352,7 +352,7 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
|
352
352
|
* process just wrote, which is a race on any watch rebuild.
|
|
353
353
|
*/
|
|
354
354
|
const bamboocssCss = (options) => {
|
|
355
|
-
const { configPath, cwd, session,
|
|
355
|
+
const { configPath, cwd, session, pruneCss = true } = options;
|
|
356
356
|
/**
|
|
357
357
|
* Environments whose `load` served the virtual stylesheet.
|
|
358
358
|
*
|
|
@@ -489,18 +489,18 @@ const bamboocssCss = (options) => {
|
|
|
489
489
|
* link the pruned copy: one project lost 39% of its atoms that way, presenting as
|
|
490
490
|
* rarely-used classes such as `md:{display:inline-block}` silently not applying.
|
|
491
491
|
*
|
|
492
|
-
* So the full extracted stylesheet ships instead,
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
492
|
+
* So the full extracted stylesheet ships instead, which is what `pruneCss: false` asks
|
|
493
|
+
* for by hand. Being the last environment is not the common case — frameworks build the
|
|
494
|
+
* client first — but it is the only one where the answer is knowable, and a framework
|
|
495
|
+
* that builds its server bundle first does get pruned output.
|
|
496
496
|
*/
|
|
497
497
|
const pending = remainingEnvironments(session);
|
|
498
498
|
const { sheets } = optimizeStaticCssAssets(bundle, session, {
|
|
499
|
-
|
|
500
|
-
prune: pending.length === 0,
|
|
499
|
+
prune: pruneCss && pending.length === 0,
|
|
501
500
|
sourcemap: environment?.config?.build?.sourcemap
|
|
502
501
|
});
|
|
503
|
-
if (sheets &&
|
|
502
|
+
if (sheets && !pruneCss) _bamboocss_logger.logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
|
|
503
|
+
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, {
|
|
504
504
|
unit: "environment",
|
|
505
505
|
separator: ", "
|
|
506
506
|
})} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
|
|
@@ -517,6 +517,78 @@ const bamboocssCss = (options) => {
|
|
|
517
517
|
//#endregion
|
|
518
518
|
//#region src/fold-analysis.ts
|
|
519
519
|
/**
|
|
520
|
+
* Nodes that *compose* a value out of their children rather than computing one.
|
|
521
|
+
*
|
|
522
|
+
* The boundary of the walk below, and the whole of its precision. Climbing through these
|
|
523
|
+
* keeps a value's provenance: `'red.300'` inside `{ color: 'red.300' }` inside a default is
|
|
524
|
+
* still the default's. Anything else — a call, a function body, a JSX element — produces its
|
|
525
|
+
* value by being evaluated, so what is written inside it is an argument to that evaluation
|
|
526
|
+
* and not the enclosing default's value.
|
|
527
|
+
*
|
|
528
|
+
* Without the boundary, `({ cls = css({ color: 'red.300' }) }) => cls` is rejected: the call's
|
|
529
|
+
* own literal argument is syntactically inside a default, so an unbounded walk calls it one.
|
|
530
|
+
* That is correct code, and rejecting it fails the build.
|
|
531
|
+
*/
|
|
532
|
+
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);
|
|
533
|
+
/** Is `inner` written within `outer`? Positions rather than a walk, so it is O(1). */
|
|
534
|
+
const contains = (outer, inner) => outer.getSourceFile() === inner.getSourceFile() && outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
|
|
535
|
+
/**
|
|
536
|
+
* Did this value come from the `= …` of a destructuring binding?
|
|
537
|
+
*
|
|
538
|
+
* `const { tone = 'red.300' } = source` boxes as the literal `'red.300'`: the extractor's
|
|
539
|
+
* `maybeDefinitionValue` tests for an initializer first and returns the boxed default, never
|
|
540
|
+
* reaching the branch that would read `source`. So the default is reported as the value whether
|
|
541
|
+
* or not it is the one that applies.
|
|
542
|
+
*
|
|
543
|
+
* For extraction that is merely optimistic, and deliberately so: a CLI or PostCSS build ships a
|
|
544
|
+
* runtime `css()`, where the default genuinely does apply when the caller omits the key, and it
|
|
545
|
+
* needs a rule behind it. Folding is where the same resolution turns into a wrong answer,
|
|
546
|
+
* because the call is *replaced* by that value.
|
|
547
|
+
*
|
|
548
|
+
* Stops at the first non-composing parent, so a call written inside a default keeps its own
|
|
549
|
+
* provenance, and checks that the binding element was reached through its initializer, so
|
|
550
|
+
* `{ tone = X }`'s name node is not mistaken for its default.
|
|
551
|
+
*/
|
|
552
|
+
const isBindingElementDefault = (node) => {
|
|
553
|
+
if (node && ts_morph.Node.isCallExpression(node)) return false;
|
|
554
|
+
let current = node;
|
|
555
|
+
while (current) {
|
|
556
|
+
const parent = current.getParent();
|
|
557
|
+
if (!parent) return false;
|
|
558
|
+
if (ts_morph.Node.isBindingElement(parent)) return parent.getInitializer() === current;
|
|
559
|
+
if (!composesValue(parent)) return false;
|
|
560
|
+
current = parent;
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
};
|
|
564
|
+
/**
|
|
565
|
+
* The same question asked of a whole box, including how it was resolved.
|
|
566
|
+
*
|
|
567
|
+
* The node a box reports is not always the one its value came from — an empty `{}` default
|
|
568
|
+
* boxes against the call rather than against the `{}` — so the resolution stack is consulted
|
|
569
|
+
* too. A binding element reaches that stack by having been resolved *through*, which is the
|
|
570
|
+
* signal the extractor itself reads when one of these is a conditional's test.
|
|
571
|
+
*
|
|
572
|
+
* Only the entries that are binding elements are examined. Walking up from every other entry
|
|
573
|
+
* was tried and never once changed a verdict across the default spellings or the sandbox's own
|
|
574
|
+
* modules, while accounting for most of the parent hops this does — the stack carries nodes
|
|
575
|
+
* that were never resolved through, including a call's own arguments, so walking from them is
|
|
576
|
+
* both the expensive half and the one that reaches conclusions it has no basis for.
|
|
577
|
+
*
|
|
578
|
+
* A binding element without an initializer carries no default to mistrust: `const { tone } =
|
|
579
|
+
* source` either resolves from `source` or does not resolve at all.
|
|
580
|
+
*/
|
|
581
|
+
const isFromBindingDefault = (node) => {
|
|
582
|
+
const own = node.getNode?.();
|
|
583
|
+
if (isBindingElementDefault(own)) return true;
|
|
584
|
+
for (const entry of node.getStack?.() ?? []) {
|
|
585
|
+
if (!ts_morph.Node.isBindingElement(entry) || !entry.getInitializer()) continue;
|
|
586
|
+
if (own && contains(entry, own)) continue;
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
return false;
|
|
590
|
+
};
|
|
591
|
+
/**
|
|
520
592
|
* Statically resolvable means: every box in the tree carries a known value.
|
|
521
593
|
*
|
|
522
594
|
* `unresolvable` is the extractor saying it could not evaluate a node.
|
|
@@ -530,6 +602,7 @@ const isStaticBox = (node, seen = /* @__PURE__ */ new Set()) => {
|
|
|
530
602
|
seen.add(node);
|
|
531
603
|
if (_bamboocss_extractor.box.isUnresolvable(node) || _bamboocss_extractor.box.isConditional(node)) return false;
|
|
532
604
|
if (!("type" in node) || node.type == null) return false;
|
|
605
|
+
if (isFromBindingDefault(node)) return false;
|
|
533
606
|
if (_bamboocss_extractor.box.isLiteral(node) && node.value === void 0) {
|
|
534
607
|
const source = node.getNode?.();
|
|
535
608
|
return Boolean(source && ts_morph.Node.isIdentifier(source) && source.getText() === "undefined");
|
|
@@ -2892,8 +2965,47 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
|
|
|
2892
2965
|
//#region src/plugin.ts
|
|
2893
2966
|
const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
|
|
2894
2967
|
const NODE_MODULES = /node_modules/;
|
|
2968
|
+
/**
|
|
2969
|
+
* Queries that make Vite serve something other than the module's own source.
|
|
2970
|
+
*
|
|
2971
|
+
* `./theme.tsx?raw` is a module whose text is `export default "…"`, and `?url`, `?worker` and
|
|
2972
|
+
* `?sharedworker` are wrappers of the same kind. The query has to be stripped before the
|
|
2973
|
+
* extension is tested — otherwise nothing matches `.tsx` — and stripping it is what made these
|
|
2974
|
+
* look like the file itself. The transform then handed the wrapper's text to ts-morph *under
|
|
2975
|
+
* the real file's path*, overwriting the parsed module every fold reads for that path.
|
|
2976
|
+
*
|
|
2977
|
+
* That is not theoretical: a module folding `css(shared)` against a sibling the entry also
|
|
2978
|
+
* imported as `?raw` failed the build with "1 call(s) could not be compiled" — the compiler
|
|
2979
|
+
* had read `export default "…"` and found no `shared` to resolve. The advice it prints, to
|
|
2980
|
+
* make the value statically analyzable, is unfollowable, because the source already was.
|
|
2981
|
+
*
|
|
2982
|
+
* Whether it bites depends on which of the two ids Rollup transforms last, so the same project
|
|
2983
|
+
* can build and then stop building because an import moved.
|
|
2984
|
+
*
|
|
2985
|
+
* A deny list rather than an allow list of benign queries: dev ids carry `?t=` after an edit
|
|
2986
|
+
* and `?import` when a dynamic import is rewritten, and rejecting an unrecognised one of those
|
|
2987
|
+
* would silently stop folding a module rather than loudly refuse it.
|
|
2988
|
+
*
|
|
2989
|
+
* Exactly these four, matching Vite's own `SPECIAL_QUERY_RE`. The list was drafted wider —
|
|
2990
|
+
* `?inline`, `?no-inline`, `?worklet`, `?init` — and every one of those was wrong: Vite has no
|
|
2991
|
+
* `worklet` query at all, `?init` is `.wasm` only and that extension is already rejected below,
|
|
2992
|
+
* and `inline`/`no-inline` merely pick base64-versus-file for something that *already* matched
|
|
2993
|
+
* `raw`/`url`, so `./a.tsx?inline` is served as the module's own source. Rejecting an id that
|
|
2994
|
+
* carries real source is the expensive direction: the transform declines, its atoms never reach
|
|
2995
|
+
* the reachability set, pruning removes their rules, and the runtime still returns the class
|
|
2996
|
+
* names — unstyled elements, no error. Only names verified against Vite belong here.
|
|
2997
|
+
*
|
|
2998
|
+
* Note `?worker_file`, which is how dev serves a worker's *real* source, is deliberately absent
|
|
2999
|
+
* and must stay absent. It contains "worker" and is the obvious next entry; adding it would
|
|
3000
|
+
* stop folding every worker module in dev, silently, by the mechanism above.
|
|
3001
|
+
*
|
|
3002
|
+
* Tested against the whole id rather than a split-off query, so it cannot disagree with
|
|
3003
|
+
* `queryOf` in `css.ts` about where the query starts.
|
|
3004
|
+
*/
|
|
3005
|
+
const WRAPPED_MODULE_QUERY = /[?&](?:raw|url|worker|sharedworker)(?:&|=|$)/;
|
|
2895
3006
|
const shouldTransform = (id) => {
|
|
2896
3007
|
if (id.startsWith("\0")) return false;
|
|
3008
|
+
if (WRAPPED_MODULE_QUERY.test(id)) return false;
|
|
2897
3009
|
const [filePath] = id.split("?");
|
|
2898
3010
|
if (!filePath) return false;
|
|
2899
3011
|
if (NODE_MODULES.test(filePath)) return false;
|
|
@@ -2940,15 +3052,26 @@ const formatSkipped = (id, skipped) => {
|
|
|
2940
3052
|
* with no matching rule.
|
|
2941
3053
|
*/
|
|
2942
3054
|
const bamboocss = (options = {}) => {
|
|
2943
|
-
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates,
|
|
3055
|
+
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
|
|
2944
3056
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
3057
|
+
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.");
|
|
3058
|
+
/**
|
|
3059
|
+
* What each file's transform found, for the summary. Keyed by file rather than summed as it
|
|
3060
|
+
* goes, because a build has more than one environment and they share most of their modules.
|
|
3061
|
+
*
|
|
3062
|
+
* Running totals double-counted every shared module once per environment — a two-environment
|
|
3063
|
+
* build of one shared file and one entry each reported "2/2 across 2/4 files" for three
|
|
3064
|
+
* source modules. Coverage is a property of the source, not of how many times a bundler
|
|
3065
|
+
* handed the same file over. It also grew without bound in dev, where every HMR
|
|
3066
|
+
* re-transform of a file counted as another file.
|
|
3067
|
+
*
|
|
3068
|
+
* A second pass over a file replaces its entry rather than adding to it. Both environments
|
|
3069
|
+
* are assumed to compute the same answer for the same module — true of this compiler, though
|
|
3070
|
+
* not something the plugin can enforce, since another `pre` plugin may hand each environment
|
|
3071
|
+
* different code. Where they disagree the last one wins, which is a cosmetic number either
|
|
3072
|
+
* way.
|
|
3073
|
+
*/
|
|
3074
|
+
const perFile = /* @__PURE__ */ new Map();
|
|
2952
3075
|
const staticSession = createStaticCompilationSession();
|
|
2953
3076
|
/**
|
|
2954
3077
|
* Indexed by file, because the only bulk operation on it is "forget this one's".
|
|
@@ -3011,7 +3134,7 @@ const bamboocss = (options = {}) => {
|
|
|
3011
3134
|
configPath,
|
|
3012
3135
|
cwd,
|
|
3013
3136
|
session: staticSession,
|
|
3014
|
-
|
|
3137
|
+
pruneCss
|
|
3015
3138
|
}), {
|
|
3016
3139
|
name: "bamboocss:compiler",
|
|
3017
3140
|
enforce: "pre",
|
|
@@ -3023,10 +3146,7 @@ const bamboocss = (options = {}) => {
|
|
|
3023
3146
|
async buildStart() {
|
|
3024
3147
|
const environment = this.environment?.name ?? "default";
|
|
3025
3148
|
if (staticSession.startedEnvironments.has(environment)) {
|
|
3026
|
-
|
|
3027
|
-
totals.files = 0;
|
|
3028
|
-
totals.filesWithFolds = 0;
|
|
3029
|
-
totals.skipped.clear();
|
|
3149
|
+
perFile.clear();
|
|
3030
3150
|
survivorsByFile.clear();
|
|
3031
3151
|
recipeConfigCache.clear();
|
|
3032
3152
|
resetStaticCompilationSession(staticSession);
|
|
@@ -3101,8 +3221,10 @@ const bamboocss = (options = {}) => {
|
|
|
3101
3221
|
});
|
|
3102
3222
|
} catch (error) {
|
|
3103
3223
|
_bamboocss_logger.logger.caughtError("vite:transform", `Failed to compile ${filePath}`, error);
|
|
3104
|
-
|
|
3105
|
-
|
|
3224
|
+
perFile.set(filePath, {
|
|
3225
|
+
folded: 0,
|
|
3226
|
+
skipped: new Map([["compile-failed", 1]])
|
|
3227
|
+
});
|
|
3106
3228
|
addSurvivor({
|
|
3107
3229
|
file: filePath,
|
|
3108
3230
|
line: 1,
|
|
@@ -3112,10 +3234,15 @@ const bamboocss = (options = {}) => {
|
|
|
3112
3234
|
if (command === "serve") throw asError(error, `failed to compile ${filePath}`);
|
|
3113
3235
|
return null;
|
|
3114
3236
|
}
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3237
|
+
let skippedHere;
|
|
3238
|
+
for (const entry of result.skipped) {
|
|
3239
|
+
skippedHere ??= /* @__PURE__ */ new Map();
|
|
3240
|
+
skippedHere.set(entry.reason, (skippedHere.get(entry.reason) ?? 0) + 1);
|
|
3241
|
+
}
|
|
3242
|
+
perFile.set(filePath, {
|
|
3243
|
+
folded: result.folded.length,
|
|
3244
|
+
skipped: skippedHere
|
|
3245
|
+
});
|
|
3119
3246
|
if (result.folded.some((entry) => entry.kind === "class" || entry.kind === "slots")) staticSession.transformedFiles.add((0, node_path.resolve)(filePath));
|
|
3120
3247
|
for (const entry of result.folded) for (const className of entry.classNames) staticSession.markClassUsed(className);
|
|
3121
3248
|
for (const entry of result.skipped) {
|
|
@@ -3148,7 +3275,7 @@ const bamboocss = (options = {}) => {
|
|
|
3148
3275
|
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
3276
|
unit: "class",
|
|
3150
3277
|
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({
|
|
3278
|
+
})}\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({ pruneCss: false })\` also turns pruning off entirely.`);
|
|
3152
3279
|
}
|
|
3153
3280
|
if (typeof this.getModuleInfo === "function" && !remainingEnvironments(staticSession).length) {
|
|
3154
3281
|
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.`);
|
|
@@ -3159,12 +3286,21 @@ const bamboocss = (options = {}) => {
|
|
|
3159
3286
|
})}\n\nAdd them to \`include\` in bamboo.config, or no CSS rule can back their emitted classes.`);
|
|
3160
3287
|
}
|
|
3161
3288
|
if (!reportSummary) return;
|
|
3162
|
-
|
|
3163
|
-
|
|
3289
|
+
if (command === "build" && remainingEnvironments(staticSession).length) return;
|
|
3290
|
+
let folded = 0;
|
|
3291
|
+
let filesWithFolds = 0;
|
|
3292
|
+
const skipped = /* @__PURE__ */ new Map();
|
|
3293
|
+
for (const entry of perFile.values()) {
|
|
3294
|
+
folded += entry.folded;
|
|
3295
|
+
if (entry.folded) filesWithFolds++;
|
|
3296
|
+
for (const [reason, count] of entry.skipped ?? []) skipped.set(reason, (skipped.get(reason) ?? 0) + count);
|
|
3297
|
+
}
|
|
3298
|
+
const declined = Array.from(skipped.values()).reduce((sum, count) => sum + count, 0);
|
|
3299
|
+
const total = folded + declined;
|
|
3164
3300
|
if (!total) return;
|
|
3165
|
-
const share = Math.round(
|
|
3166
|
-
const reasons = Array.from(
|
|
3167
|
-
_bamboocss_logger.logger.info("vite:transform", `Compiled ${
|
|
3301
|
+
const share = Math.round(folded / total * 100);
|
|
3302
|
+
const reasons = Array.from(skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
|
|
3303
|
+
_bamboocss_logger.logger.info("vite:transform", `Compiled ${folded}/${total} (${share}%) across ${filesWithFolds}/${perFile.size} files` + (reasons ? ` — declined: ${reasons}` : ""));
|
|
3168
3304
|
}
|
|
3169
3305
|
}];
|
|
3170
3306
|
};
|
package/dist/index.d.cts
CHANGED
|
@@ -37,21 +37,32 @@ interface BambooVitePluginOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
maxRecipeStates?: number;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Remove rules for atoms no compiled module can emit. Builds only; dev never prunes.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* a
|
|
42
|
+
* Off ships the whole extracted stylesheet: every rule the source graph produced, including
|
|
43
|
+
* ones nothing reaches. Larger, and never wrong *by pruning* — it also stands down the
|
|
44
|
+
* assertion that every compiled class has a rule, since that check exists to catch this pass
|
|
45
|
+
* removing too much. So this is a true escape hatch: it cannot fail a build over reachability.
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* The pruned sheet is also renamed to a hash of its own bytes, and that is not a separate
|
|
48
|
+
* setting because it cannot safely be one. Rollup and Rolldown expand `[hash]` before
|
|
49
|
+
* `generateBundle`, where pruning has to run, so the name Vite assigned describes the sheet
|
|
50
|
+
* as it was *before* pruning. Leaving that name on pruned bytes is how a stale stylesheet
|
|
51
|
+
* outlives a deploy — a change to reachability alone, which is what upgrading Bamboo is,
|
|
52
|
+
* leaves identical source CSS under an identical name with different content, and a CDN
|
|
53
|
+
* holding that key keeps serving the old one. So the bytes and the name move together or
|
|
54
|
+
* neither does.
|
|
55
|
+
*
|
|
56
|
+
* Reach for this if something downstream derives an artifact from the stylesheet's *content*
|
|
57
|
+
* during `generateBundle` before Bamboo runs — subresource integrity is the clear case, since
|
|
58
|
+
* an `integrity` attribute is a digest of the bytes and no amount of reference rewriting can
|
|
59
|
+
* carry it across an edit — or to rule pruning out while diagnosing a missing rule. Where the
|
|
60
|
+
* consumer can be moved after Bamboo instead (`order: 'post'`, `writeBundle`, `closeBundle`),
|
|
61
|
+
* do that and keep the pruning.
|
|
51
62
|
*
|
|
52
63
|
* @default true
|
|
53
64
|
*/
|
|
54
|
-
|
|
65
|
+
pruneCss?: boolean;
|
|
55
66
|
}
|
|
56
67
|
/**
|
|
57
68
|
* Vite integration for Bamboo CSS.
|
package/dist/index.d.mts
CHANGED
|
@@ -37,21 +37,32 @@ interface BambooVitePluginOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
maxRecipeStates?: number;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Remove rules for atoms no compiled module can emit. Builds only; dev never prunes.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* a
|
|
42
|
+
* Off ships the whole extracted stylesheet: every rule the source graph produced, including
|
|
43
|
+
* ones nothing reaches. Larger, and never wrong *by pruning* — it also stands down the
|
|
44
|
+
* assertion that every compiled class has a rule, since that check exists to catch this pass
|
|
45
|
+
* removing too much. So this is a true escape hatch: it cannot fail a build over reachability.
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* The pruned sheet is also renamed to a hash of its own bytes, and that is not a separate
|
|
48
|
+
* setting because it cannot safely be one. Rollup and Rolldown expand `[hash]` before
|
|
49
|
+
* `generateBundle`, where pruning has to run, so the name Vite assigned describes the sheet
|
|
50
|
+
* as it was *before* pruning. Leaving that name on pruned bytes is how a stale stylesheet
|
|
51
|
+
* outlives a deploy — a change to reachability alone, which is what upgrading Bamboo is,
|
|
52
|
+
* leaves identical source CSS under an identical name with different content, and a CDN
|
|
53
|
+
* holding that key keeps serving the old one. So the bytes and the name move together or
|
|
54
|
+
* neither does.
|
|
55
|
+
*
|
|
56
|
+
* Reach for this if something downstream derives an artifact from the stylesheet's *content*
|
|
57
|
+
* during `generateBundle` before Bamboo runs — subresource integrity is the clear case, since
|
|
58
|
+
* an `integrity` attribute is a digest of the bytes and no amount of reference rewriting can
|
|
59
|
+
* carry it across an edit — or to rule pruning out while diagnosing a missing rule. Where the
|
|
60
|
+
* consumer can be moved after Bamboo instead (`order: 'post'`, `writeBundle`, `closeBundle`),
|
|
61
|
+
* do that and keep the pruning.
|
|
51
62
|
*
|
|
52
63
|
* @default true
|
|
53
64
|
*/
|
|
54
|
-
|
|
65
|
+
pruneCss?: boolean;
|
|
55
66
|
}
|
|
56
67
|
/**
|
|
57
68
|
* Vite integration for Bamboo CSS.
|
package/dist/index.mjs
CHANGED
|
@@ -276,14 +276,19 @@ const replaceAssetReferences = (bundle, previous, next, sourcemap) => {
|
|
|
276
276
|
*/
|
|
277
277
|
const carriesGeneratedCss = (output) => output.type === "asset" && output.fileName.endsWith(".css");
|
|
278
278
|
/**
|
|
279
|
-
* Prune
|
|
279
|
+
* Prune compiler-owned CSS, then give any sheet whose bytes changed a hash of those bytes.
|
|
280
280
|
*
|
|
281
281
|
* Rollup has already expanded `[hash]` when `generateBundle` runs. Mutating only `source`
|
|
282
282
|
* would therefore leave two different reachable subsets under one CDN key. The extra final
|
|
283
283
|
* hash is not cosmetic: it makes late graph reachability cache-safe.
|
|
284
|
+
*
|
|
285
|
+
* Renaming is therefore not a choice this takes. Pruned bytes under the unpruned sheet's name
|
|
286
|
+
* is the one outcome that must never be reachable, and a sheet nothing was removed from keeps
|
|
287
|
+
* its name because its bytes are unchanged — so "rename" is a consequence of "the bytes moved",
|
|
288
|
+
* not a second option. `prune` is the only knob.
|
|
284
289
|
*/
|
|
285
290
|
const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
286
|
-
const {
|
|
291
|
+
const { prune = true, sourcemap = session.sourcemap } = options;
|
|
287
292
|
/** Assets in this bundle that carry the generated stylesheet, pruned or not. */
|
|
288
293
|
let sheets = 0;
|
|
289
294
|
for (const output of Object.values(bundle)) {
|
|
@@ -295,12 +300,7 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
|
295
300
|
const optimized = pruneStaticCss(source, session);
|
|
296
301
|
output.source = optimized;
|
|
297
302
|
if (optimized === source) continue;
|
|
298
|
-
if (!rename) {
|
|
299
|
-
output.source = source;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
303
|
const nextName = output.fileName.replace(/\.css$/, `.b-${toHash(optimized)}.css`);
|
|
303
|
-
if (nextName === output.fileName) continue;
|
|
304
304
|
if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
|
|
305
305
|
const previous = output.fileName;
|
|
306
306
|
output.fileName = nextName;
|
|
@@ -322,7 +322,7 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
|
322
322
|
* process just wrote, which is a race on any watch rebuild.
|
|
323
323
|
*/
|
|
324
324
|
const bamboocssCss = (options) => {
|
|
325
|
-
const { configPath, cwd, session,
|
|
325
|
+
const { configPath, cwd, session, pruneCss = true } = options;
|
|
326
326
|
/**
|
|
327
327
|
* Environments whose `load` served the virtual stylesheet.
|
|
328
328
|
*
|
|
@@ -459,18 +459,18 @@ const bamboocssCss = (options) => {
|
|
|
459
459
|
* link the pruned copy: one project lost 39% of its atoms that way, presenting as
|
|
460
460
|
* rarely-used classes such as `md:{display:inline-block}` silently not applying.
|
|
461
461
|
*
|
|
462
|
-
* So the full extracted stylesheet ships instead,
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
462
|
+
* So the full extracted stylesheet ships instead, which is what `pruneCss: false` asks
|
|
463
|
+
* for by hand. Being the last environment is not the common case — frameworks build the
|
|
464
|
+
* client first — but it is the only one where the answer is knowable, and a framework
|
|
465
|
+
* that builds its server bundle first does get pruned output.
|
|
466
466
|
*/
|
|
467
467
|
const pending = remainingEnvironments(session);
|
|
468
468
|
const { sheets } = optimizeStaticCssAssets(bundle, session, {
|
|
469
|
-
|
|
470
|
-
prune: pending.length === 0,
|
|
469
|
+
prune: pruneCss && pending.length === 0,
|
|
471
470
|
sourcemap: environment?.config?.build?.sourcemap
|
|
472
471
|
});
|
|
473
|
-
if (sheets &&
|
|
472
|
+
if (sheets && !pruneCss) logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
|
|
473
|
+
else 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
474
|
unit: "environment",
|
|
475
475
|
separator: ", "
|
|
476
476
|
})} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
|
|
@@ -487,6 +487,78 @@ const bamboocssCss = (options) => {
|
|
|
487
487
|
//#endregion
|
|
488
488
|
//#region src/fold-analysis.ts
|
|
489
489
|
/**
|
|
490
|
+
* Nodes that *compose* a value out of their children rather than computing one.
|
|
491
|
+
*
|
|
492
|
+
* The boundary of the walk below, and the whole of its precision. Climbing through these
|
|
493
|
+
* keeps a value's provenance: `'red.300'` inside `{ color: 'red.300' }` inside a default is
|
|
494
|
+
* still the default's. Anything else — a call, a function body, a JSX element — produces its
|
|
495
|
+
* value by being evaluated, so what is written inside it is an argument to that evaluation
|
|
496
|
+
* and not the enclosing default's value.
|
|
497
|
+
*
|
|
498
|
+
* Without the boundary, `({ cls = css({ color: 'red.300' }) }) => cls` is rejected: the call's
|
|
499
|
+
* own literal argument is syntactically inside a default, so an unbounded walk calls it one.
|
|
500
|
+
* That is correct code, and rejecting it fails the build.
|
|
501
|
+
*/
|
|
502
|
+
const composesValue = (node) => Node.isObjectLiteralExpression(node) || Node.isArrayLiteralExpression(node) || Node.isPropertyAssignment(node) || Node.isShorthandPropertyAssignment(node) || Node.isSpreadAssignment(node) || Node.isSpreadElement(node) || Node.isAsExpression(node) || Node.isParenthesizedExpression(node) || Node.isNonNullExpression(node) || Node.isTypeAssertion(node) || Node.isSatisfiesExpression(node);
|
|
503
|
+
/** Is `inner` written within `outer`? Positions rather than a walk, so it is O(1). */
|
|
504
|
+
const contains = (outer, inner) => outer.getSourceFile() === inner.getSourceFile() && outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
|
|
505
|
+
/**
|
|
506
|
+
* Did this value come from the `= …` of a destructuring binding?
|
|
507
|
+
*
|
|
508
|
+
* `const { tone = 'red.300' } = source` boxes as the literal `'red.300'`: the extractor's
|
|
509
|
+
* `maybeDefinitionValue` tests for an initializer first and returns the boxed default, never
|
|
510
|
+
* reaching the branch that would read `source`. So the default is reported as the value whether
|
|
511
|
+
* or not it is the one that applies.
|
|
512
|
+
*
|
|
513
|
+
* For extraction that is merely optimistic, and deliberately so: a CLI or PostCSS build ships a
|
|
514
|
+
* runtime `css()`, where the default genuinely does apply when the caller omits the key, and it
|
|
515
|
+
* needs a rule behind it. Folding is where the same resolution turns into a wrong answer,
|
|
516
|
+
* because the call is *replaced* by that value.
|
|
517
|
+
*
|
|
518
|
+
* Stops at the first non-composing parent, so a call written inside a default keeps its own
|
|
519
|
+
* provenance, and checks that the binding element was reached through its initializer, so
|
|
520
|
+
* `{ tone = X }`'s name node is not mistaken for its default.
|
|
521
|
+
*/
|
|
522
|
+
const isBindingElementDefault = (node) => {
|
|
523
|
+
if (node && Node.isCallExpression(node)) return false;
|
|
524
|
+
let current = node;
|
|
525
|
+
while (current) {
|
|
526
|
+
const parent = current.getParent();
|
|
527
|
+
if (!parent) return false;
|
|
528
|
+
if (Node.isBindingElement(parent)) return parent.getInitializer() === current;
|
|
529
|
+
if (!composesValue(parent)) return false;
|
|
530
|
+
current = parent;
|
|
531
|
+
}
|
|
532
|
+
return false;
|
|
533
|
+
};
|
|
534
|
+
/**
|
|
535
|
+
* The same question asked of a whole box, including how it was resolved.
|
|
536
|
+
*
|
|
537
|
+
* The node a box reports is not always the one its value came from — an empty `{}` default
|
|
538
|
+
* boxes against the call rather than against the `{}` — so the resolution stack is consulted
|
|
539
|
+
* too. A binding element reaches that stack by having been resolved *through*, which is the
|
|
540
|
+
* signal the extractor itself reads when one of these is a conditional's test.
|
|
541
|
+
*
|
|
542
|
+
* Only the entries that are binding elements are examined. Walking up from every other entry
|
|
543
|
+
* was tried and never once changed a verdict across the default spellings or the sandbox's own
|
|
544
|
+
* modules, while accounting for most of the parent hops this does — the stack carries nodes
|
|
545
|
+
* that were never resolved through, including a call's own arguments, so walking from them is
|
|
546
|
+
* both the expensive half and the one that reaches conclusions it has no basis for.
|
|
547
|
+
*
|
|
548
|
+
* A binding element without an initializer carries no default to mistrust: `const { tone } =
|
|
549
|
+
* source` either resolves from `source` or does not resolve at all.
|
|
550
|
+
*/
|
|
551
|
+
const isFromBindingDefault = (node) => {
|
|
552
|
+
const own = node.getNode?.();
|
|
553
|
+
if (isBindingElementDefault(own)) return true;
|
|
554
|
+
for (const entry of node.getStack?.() ?? []) {
|
|
555
|
+
if (!Node.isBindingElement(entry) || !entry.getInitializer()) continue;
|
|
556
|
+
if (own && contains(entry, own)) continue;
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
return false;
|
|
560
|
+
};
|
|
561
|
+
/**
|
|
490
562
|
* Statically resolvable means: every box in the tree carries a known value.
|
|
491
563
|
*
|
|
492
564
|
* `unresolvable` is the extractor saying it could not evaluate a node.
|
|
@@ -500,6 +572,7 @@ const isStaticBox = (node, seen = /* @__PURE__ */ new Set()) => {
|
|
|
500
572
|
seen.add(node);
|
|
501
573
|
if (box.isUnresolvable(node) || box.isConditional(node)) return false;
|
|
502
574
|
if (!("type" in node) || node.type == null) return false;
|
|
575
|
+
if (isFromBindingDefault(node)) return false;
|
|
503
576
|
if (box.isLiteral(node) && node.value === void 0) {
|
|
504
577
|
const source = node.getNode?.();
|
|
505
578
|
return Boolean(source && Node.isIdentifier(source) && source.getText() === "undefined");
|
|
@@ -2862,8 +2935,47 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
|
|
|
2862
2935
|
//#region src/plugin.ts
|
|
2863
2936
|
const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
|
|
2864
2937
|
const NODE_MODULES = /node_modules/;
|
|
2938
|
+
/**
|
|
2939
|
+
* Queries that make Vite serve something other than the module's own source.
|
|
2940
|
+
*
|
|
2941
|
+
* `./theme.tsx?raw` is a module whose text is `export default "…"`, and `?url`, `?worker` and
|
|
2942
|
+
* `?sharedworker` are wrappers of the same kind. The query has to be stripped before the
|
|
2943
|
+
* extension is tested — otherwise nothing matches `.tsx` — and stripping it is what made these
|
|
2944
|
+
* look like the file itself. The transform then handed the wrapper's text to ts-morph *under
|
|
2945
|
+
* the real file's path*, overwriting the parsed module every fold reads for that path.
|
|
2946
|
+
*
|
|
2947
|
+
* That is not theoretical: a module folding `css(shared)` against a sibling the entry also
|
|
2948
|
+
* imported as `?raw` failed the build with "1 call(s) could not be compiled" — the compiler
|
|
2949
|
+
* had read `export default "…"` and found no `shared` to resolve. The advice it prints, to
|
|
2950
|
+
* make the value statically analyzable, is unfollowable, because the source already was.
|
|
2951
|
+
*
|
|
2952
|
+
* Whether it bites depends on which of the two ids Rollup transforms last, so the same project
|
|
2953
|
+
* can build and then stop building because an import moved.
|
|
2954
|
+
*
|
|
2955
|
+
* A deny list rather than an allow list of benign queries: dev ids carry `?t=` after an edit
|
|
2956
|
+
* and `?import` when a dynamic import is rewritten, and rejecting an unrecognised one of those
|
|
2957
|
+
* would silently stop folding a module rather than loudly refuse it.
|
|
2958
|
+
*
|
|
2959
|
+
* Exactly these four, matching Vite's own `SPECIAL_QUERY_RE`. The list was drafted wider —
|
|
2960
|
+
* `?inline`, `?no-inline`, `?worklet`, `?init` — and every one of those was wrong: Vite has no
|
|
2961
|
+
* `worklet` query at all, `?init` is `.wasm` only and that extension is already rejected below,
|
|
2962
|
+
* and `inline`/`no-inline` merely pick base64-versus-file for something that *already* matched
|
|
2963
|
+
* `raw`/`url`, so `./a.tsx?inline` is served as the module's own source. Rejecting an id that
|
|
2964
|
+
* carries real source is the expensive direction: the transform declines, its atoms never reach
|
|
2965
|
+
* the reachability set, pruning removes their rules, and the runtime still returns the class
|
|
2966
|
+
* names — unstyled elements, no error. Only names verified against Vite belong here.
|
|
2967
|
+
*
|
|
2968
|
+
* Note `?worker_file`, which is how dev serves a worker's *real* source, is deliberately absent
|
|
2969
|
+
* and must stay absent. It contains "worker" and is the obvious next entry; adding it would
|
|
2970
|
+
* stop folding every worker module in dev, silently, by the mechanism above.
|
|
2971
|
+
*
|
|
2972
|
+
* Tested against the whole id rather than a split-off query, so it cannot disagree with
|
|
2973
|
+
* `queryOf` in `css.ts` about where the query starts.
|
|
2974
|
+
*/
|
|
2975
|
+
const WRAPPED_MODULE_QUERY = /[?&](?:raw|url|worker|sharedworker)(?:&|=|$)/;
|
|
2865
2976
|
const shouldTransform = (id) => {
|
|
2866
2977
|
if (id.startsWith("\0")) return false;
|
|
2978
|
+
if (WRAPPED_MODULE_QUERY.test(id)) return false;
|
|
2867
2979
|
const [filePath] = id.split("?");
|
|
2868
2980
|
if (!filePath) return false;
|
|
2869
2981
|
if (NODE_MODULES.test(filePath)) return false;
|
|
@@ -2910,15 +3022,26 @@ const formatSkipped = (id, skipped) => {
|
|
|
2910
3022
|
* with no matching rule.
|
|
2911
3023
|
*/
|
|
2912
3024
|
const bamboocss = (options = {}) => {
|
|
2913
|
-
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates,
|
|
3025
|
+
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
|
|
2914
3026
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
3027
|
+
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.");
|
|
3028
|
+
/**
|
|
3029
|
+
* What each file's transform found, for the summary. Keyed by file rather than summed as it
|
|
3030
|
+
* goes, because a build has more than one environment and they share most of their modules.
|
|
3031
|
+
*
|
|
3032
|
+
* Running totals double-counted every shared module once per environment — a two-environment
|
|
3033
|
+
* build of one shared file and one entry each reported "2/2 across 2/4 files" for three
|
|
3034
|
+
* source modules. Coverage is a property of the source, not of how many times a bundler
|
|
3035
|
+
* handed the same file over. It also grew without bound in dev, where every HMR
|
|
3036
|
+
* re-transform of a file counted as another file.
|
|
3037
|
+
*
|
|
3038
|
+
* A second pass over a file replaces its entry rather than adding to it. Both environments
|
|
3039
|
+
* are assumed to compute the same answer for the same module — true of this compiler, though
|
|
3040
|
+
* not something the plugin can enforce, since another `pre` plugin may hand each environment
|
|
3041
|
+
* different code. Where they disagree the last one wins, which is a cosmetic number either
|
|
3042
|
+
* way.
|
|
3043
|
+
*/
|
|
3044
|
+
const perFile = /* @__PURE__ */ new Map();
|
|
2922
3045
|
const staticSession = createStaticCompilationSession();
|
|
2923
3046
|
/**
|
|
2924
3047
|
* Indexed by file, because the only bulk operation on it is "forget this one's".
|
|
@@ -2981,7 +3104,7 @@ const bamboocss = (options = {}) => {
|
|
|
2981
3104
|
configPath,
|
|
2982
3105
|
cwd,
|
|
2983
3106
|
session: staticSession,
|
|
2984
|
-
|
|
3107
|
+
pruneCss
|
|
2985
3108
|
}), {
|
|
2986
3109
|
name: "bamboocss:compiler",
|
|
2987
3110
|
enforce: "pre",
|
|
@@ -2993,10 +3116,7 @@ const bamboocss = (options = {}) => {
|
|
|
2993
3116
|
async buildStart() {
|
|
2994
3117
|
const environment = this.environment?.name ?? "default";
|
|
2995
3118
|
if (staticSession.startedEnvironments.has(environment)) {
|
|
2996
|
-
|
|
2997
|
-
totals.files = 0;
|
|
2998
|
-
totals.filesWithFolds = 0;
|
|
2999
|
-
totals.skipped.clear();
|
|
3119
|
+
perFile.clear();
|
|
3000
3120
|
survivorsByFile.clear();
|
|
3001
3121
|
recipeConfigCache.clear();
|
|
3002
3122
|
resetStaticCompilationSession(staticSession);
|
|
@@ -3071,8 +3191,10 @@ const bamboocss = (options = {}) => {
|
|
|
3071
3191
|
});
|
|
3072
3192
|
} catch (error) {
|
|
3073
3193
|
logger.caughtError("vite:transform", `Failed to compile ${filePath}`, error);
|
|
3074
|
-
|
|
3075
|
-
|
|
3194
|
+
perFile.set(filePath, {
|
|
3195
|
+
folded: 0,
|
|
3196
|
+
skipped: new Map([["compile-failed", 1]])
|
|
3197
|
+
});
|
|
3076
3198
|
addSurvivor({
|
|
3077
3199
|
file: filePath,
|
|
3078
3200
|
line: 1,
|
|
@@ -3082,10 +3204,15 @@ const bamboocss = (options = {}) => {
|
|
|
3082
3204
|
if (command === "serve") throw asError(error, `failed to compile ${filePath}`);
|
|
3083
3205
|
return null;
|
|
3084
3206
|
}
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3207
|
+
let skippedHere;
|
|
3208
|
+
for (const entry of result.skipped) {
|
|
3209
|
+
skippedHere ??= /* @__PURE__ */ new Map();
|
|
3210
|
+
skippedHere.set(entry.reason, (skippedHere.get(entry.reason) ?? 0) + 1);
|
|
3211
|
+
}
|
|
3212
|
+
perFile.set(filePath, {
|
|
3213
|
+
folded: result.folded.length,
|
|
3214
|
+
skipped: skippedHere
|
|
3215
|
+
});
|
|
3089
3216
|
if (result.folded.some((entry) => entry.kind === "class" || entry.kind === "slots")) staticSession.transformedFiles.add(resolve(filePath));
|
|
3090
3217
|
for (const entry of result.folded) for (const className of entry.classNames) staticSession.markClassUsed(className);
|
|
3091
3218
|
for (const entry of result.skipped) {
|
|
@@ -3118,7 +3245,7 @@ const bamboocss = (options = {}) => {
|
|
|
3118
3245
|
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
3246
|
unit: "class",
|
|
3120
3247
|
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({
|
|
3248
|
+
})}\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({ pruneCss: false })\` also turns pruning off entirely.`);
|
|
3122
3249
|
}
|
|
3123
3250
|
if (typeof this.getModuleInfo === "function" && !remainingEnvironments(staticSession).length) {
|
|
3124
3251
|
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.`);
|
|
@@ -3129,12 +3256,21 @@ const bamboocss = (options = {}) => {
|
|
|
3129
3256
|
})}\n\nAdd them to \`include\` in bamboo.config, or no CSS rule can back their emitted classes.`);
|
|
3130
3257
|
}
|
|
3131
3258
|
if (!reportSummary) return;
|
|
3132
|
-
|
|
3133
|
-
|
|
3259
|
+
if (command === "build" && remainingEnvironments(staticSession).length) return;
|
|
3260
|
+
let folded = 0;
|
|
3261
|
+
let filesWithFolds = 0;
|
|
3262
|
+
const skipped = /* @__PURE__ */ new Map();
|
|
3263
|
+
for (const entry of perFile.values()) {
|
|
3264
|
+
folded += entry.folded;
|
|
3265
|
+
if (entry.folded) filesWithFolds++;
|
|
3266
|
+
for (const [reason, count] of entry.skipped ?? []) skipped.set(reason, (skipped.get(reason) ?? 0) + count);
|
|
3267
|
+
}
|
|
3268
|
+
const declined = Array.from(skipped.values()).reduce((sum, count) => sum + count, 0);
|
|
3269
|
+
const total = folded + declined;
|
|
3134
3270
|
if (!total) return;
|
|
3135
|
-
const share = Math.round(
|
|
3136
|
-
const reasons = Array.from(
|
|
3137
|
-
logger.info("vite:transform", `Compiled ${
|
|
3271
|
+
const share = Math.round(folded / total * 100);
|
|
3272
|
+
const reasons = Array.from(skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
|
|
3273
|
+
logger.info("vite:transform", `Compiled ${folded}/${total} (${share}%) across ${filesWithFolds}/${perFile.size} files` + (reasons ? ` — declined: ${reasons}` : ""));
|
|
3138
3274
|
}
|
|
3139
3275
|
}];
|
|
3140
3276
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/vite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.0",
|
|
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/config": "1.
|
|
44
|
-
"@bamboocss/core": "1.
|
|
45
|
-
"@bamboocss/
|
|
46
|
-
"@bamboocss/
|
|
47
|
-
"@bamboocss/
|
|
48
|
-
"@bamboocss/
|
|
49
|
-
"@bamboocss/
|
|
43
|
+
"@bamboocss/config": "1.38.0",
|
|
44
|
+
"@bamboocss/core": "1.38.0",
|
|
45
|
+
"@bamboocss/logger": "1.38.0",
|
|
46
|
+
"@bamboocss/node": "1.38.0",
|
|
47
|
+
"@bamboocss/extractor": "1.38.0",
|
|
48
|
+
"@bamboocss/shared": "1.38.0",
|
|
49
|
+
"@bamboocss/types": "1.38.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
53
53
|
"vite": "7.2.6",
|
|
54
|
-
"@bamboocss/fixture": "1.
|
|
54
|
+
"@bamboocss/fixture": "1.38.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vite": ">=5"
|