@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 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 and rename compiler-owned CSS, then give changed assets a hash of their final bytes.
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 { rename = true, prune = true, sourcemap = session.sourcemap } = options;
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, renameCssAsset = true } = options;
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, 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.
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
- rename: renameCssAsset,
500
- prune: pending.length === 0,
499
+ prune: pruneCss && pending.length === 0,
501
500
  sourcemap: environment?.config?.build?.sourcemap
502
501
  });
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, {
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, renameCssAsset = true } = options;
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
- /** Totals across the build, for the summary. */
2946
- const totals = {
2947
- folded: 0,
2948
- files: 0,
2949
- filesWithFolds: 0,
2950
- skipped: /* @__PURE__ */ new Map()
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
- renameCssAsset
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
- totals.folded = 0;
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
- totals.files++;
3105
- totals.skipped.set("compile-failed", (totals.skipped.get("compile-failed") ?? 0) + 1);
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
- totals.files++;
3116
- totals.folded += result.folded.length;
3117
- if (result.folded.length) totals.filesWithFolds++;
3118
- for (const entry of result.skipped) totals.skipped.set(entry.reason, (totals.skipped.get(entry.reason) ?? 0) + 1);
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({ renameCssAsset: false })\` also turns pruning off entirely.`);
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
- const declined = Array.from(totals.skipped.values()).reduce((sum, count) => sum + count, 0);
3163
- const total = totals.folded + declined;
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(totals.folded / total * 100);
3166
- const reasons = Array.from(totals.skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
3167
- _bamboocss_logger.logger.info("vite:transform", `Compiled ${totals.folded}/${total} (${share}%) across ${totals.filesWithFolds}/${totals.files} files` + (reasons ? ` — declined: ${reasons}` : ""));
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
- * Give the pruned stylesheet a final name derived from its own bytes.
40
+ * Remove rules for atoms no compiled module can emit. Builds only; dev never prunes.
41
41
  *
42
- * Rollup and Rolldown both expand `[hash]` before `generateBundle`, so pruning after it can
43
- * leave two different reachable subsets under one CDN key. Renaming the pruned stylesheet to
44
- * a hash of its own bytes closes that.
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
- * Turning it off does not merely skip the rename it skips the pruning with it. The two are
47
- * one operation: pruned bytes under a name describing the unpruned ones is how a stale
48
- * stylesheet outlives a deploy, which is worse than shipping a larger sheet. Reach for this
49
- * only when something downstream cannot follow a renamed asset, and expect the full
50
- * extracted stylesheet when you do.
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
- renameCssAsset?: boolean;
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
- * Give the pruned stylesheet a final name derived from its own bytes.
40
+ * Remove rules for atoms no compiled module can emit. Builds only; dev never prunes.
41
41
  *
42
- * Rollup and Rolldown both expand `[hash]` before `generateBundle`, so pruning after it can
43
- * leave two different reachable subsets under one CDN key. Renaming the pruned stylesheet to
44
- * a hash of its own bytes closes that.
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
- * Turning it off does not merely skip the rename it skips the pruning with it. The two are
47
- * one operation: pruned bytes under a name describing the unpruned ones is how a stale
48
- * stylesheet outlives a deploy, which is worse than shipping a larger sheet. Reach for this
49
- * only when something downstream cannot follow a renamed asset, and expect the full
50
- * extracted stylesheet when you do.
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
- renameCssAsset?: boolean;
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 and rename compiler-owned CSS, then give changed assets a hash of their final bytes.
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 { rename = true, prune = true, sourcemap = session.sourcemap } = options;
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, renameCssAsset = true } = options;
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, 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.
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
- rename: renameCssAsset,
470
- prune: pending.length === 0,
469
+ prune: pruneCss && pending.length === 0,
471
470
  sourcemap: environment?.config?.build?.sourcemap
472
471
  });
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, {
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, renameCssAsset = true } = options;
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
- /** Totals across the build, for the summary. */
2916
- const totals = {
2917
- folded: 0,
2918
- files: 0,
2919
- filesWithFolds: 0,
2920
- skipped: /* @__PURE__ */ new Map()
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
- renameCssAsset
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
- totals.folded = 0;
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
- totals.files++;
3075
- totals.skipped.set("compile-failed", (totals.skipped.get("compile-failed") ?? 0) + 1);
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
- totals.files++;
3086
- totals.folded += result.folded.length;
3087
- if (result.folded.length) totals.filesWithFolds++;
3088
- for (const entry of result.skipped) totals.skipped.set(entry.reason, (totals.skipped.get(entry.reason) ?? 0) + 1);
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({ renameCssAsset: false })\` also turns pruning off entirely.`);
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
- const declined = Array.from(totals.skipped.values()).reduce((sum, count) => sum + count, 0);
3133
- const total = totals.folded + declined;
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(totals.folded / total * 100);
3136
- const reasons = Array.from(totals.skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
3137
- logger.info("vite:transform", `Compiled ${totals.folded}/${total} (${share}%) across ${totals.filesWithFolds}/${totals.files} files` + (reasons ? ` — declined: ${reasons}` : ""));
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.37.13",
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.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"
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.37.13"
54
+ "@bamboocss/fixture": "1.38.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": ">=5"