@bamboocss/vite 1.37.13 → 1.39.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
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
+ let _bamboocss_config = require("@bamboocss/config");
27
28
  let _bamboocss_node = require("@bamboocss/node");
28
29
  let _bamboocss_logger = require("@bamboocss/logger");
29
30
  let _bamboocss_shared = require("@bamboocss/shared");
@@ -306,14 +307,19 @@ const replaceAssetReferences = (bundle, previous, next, sourcemap) => {
306
307
  */
307
308
  const carriesGeneratedCss = (output) => output.type === "asset" && output.fileName.endsWith(".css");
308
309
  /**
309
- * Prune and rename compiler-owned CSS, then give changed assets a hash of their final bytes.
310
+ * Prune compiler-owned CSS, then give any sheet whose bytes changed a hash of those bytes.
310
311
  *
311
312
  * Rollup has already expanded `[hash]` when `generateBundle` runs. Mutating only `source`
312
313
  * would therefore leave two different reachable subsets under one CDN key. The extra final
313
314
  * hash is not cosmetic: it makes late graph reachability cache-safe.
315
+ *
316
+ * Renaming is therefore not a choice this takes. Pruned bytes under the unpruned sheet's name
317
+ * is the one outcome that must never be reachable, and a sheet nothing was removed from keeps
318
+ * its name because its bytes are unchanged — so "rename" is a consequence of "the bytes moved",
319
+ * not a second option. `prune` is the only knob.
314
320
  */
315
321
  const optimizeStaticCssAssets = (bundle, session, options = {}) => {
316
- const { rename = true, prune = true, sourcemap = session.sourcemap } = options;
322
+ const { prune = true, sourcemap = session.sourcemap } = options;
317
323
  /** Assets in this bundle that carry the generated stylesheet, pruned or not. */
318
324
  let sheets = 0;
319
325
  for (const output of Object.values(bundle)) {
@@ -325,12 +331,7 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
325
331
  const optimized = pruneStaticCss(source, session);
326
332
  output.source = optimized;
327
333
  if (optimized === source) continue;
328
- if (!rename) {
329
- output.source = source;
330
- continue;
331
- }
332
334
  const nextName = output.fileName.replace(/\.css$/, `.b-${(0, _bamboocss_shared.toHash)(optimized)}.css`);
333
- if (nextName === output.fileName) continue;
334
335
  if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
335
336
  const previous = output.fileName;
336
337
  output.fileName = nextName;
@@ -352,7 +353,7 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
352
353
  * process just wrote, which is a race on any watch rebuild.
353
354
  */
354
355
  const bamboocssCss = (options) => {
355
- const { configPath, cwd, session, renameCssAsset = true } = options;
356
+ const { configPath, cwd, session, pruneCss = true } = options;
356
357
  /**
357
358
  * Environments whose `load` served the virtual stylesheet.
358
359
  *
@@ -426,6 +427,39 @@ const bamboocssCss = (options) => {
426
427
  configResolved(config) {
427
428
  command = config.command;
428
429
  session.sourcemap = config.build.sourcemap;
430
+ /**
431
+ * Tell Vite that `bamboo.config.ts` is a config file, so editing one restarts the server.
432
+ *
433
+ * Tokens live there, and they are what a designer iterates on most — "restart the dev
434
+ * server to see a colour change" is the wrong instruction for the file most likely to be
435
+ * edited all afternoon. Nothing watched it: `watch` is the CLI's own watcher, and a
436
+ * project running `vite dev` never reaches it.
437
+ *
438
+ * A restart rather than re-emitting the stylesheet, because this plugin and the compiler
439
+ * hold *separate* contexts and only this one reloads its config. A token *value* edit
440
+ * came out right on the next source change, and an edit that changes what compiles —
441
+ * adding a token, a condition, a utility — left the compiler naming classes from the old
442
+ * config against a sheet emitted from the new one. Half-updated is worse than stale.
443
+ *
444
+ * Through Vite's own list rather than a watcher of ours. Vite adds these paths to the
445
+ * files it watches, which is what reaches a config *outside* `root` — a monorepo with one
446
+ * config above `apps/web`, or a preset resolved into `node_modules`, neither of which the
447
+ * project watcher covers. It also means the restart is Vite's, with its own concurrency
448
+ * guard and its own error reporting, rather than a second implementation of both.
449
+ *
450
+ * The config's own import graph is resolved the way `Builder` resolves it, minus the
451
+ * tsconfig paths it has not loaded yet at this point. `dependencies` globs are not
452
+ * expanded here: they are declared as an escape hatch for a *config reload*, and turning
453
+ * every file matching one into a full server restart is not what a project asking for
454
+ * that meant.
455
+ */
456
+ if (config.command === "serve") try {
457
+ const { deps } = (0, _bamboocss_config.getConfigDependencies)((0, _bamboocss_config.findConfig)({
458
+ cwd: cwd ?? config.root,
459
+ file: configPath
460
+ }));
461
+ config.configFileDependencies.push(...deps);
462
+ } catch {}
429
463
  if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
430
464
  },
431
465
  /**
@@ -489,18 +523,18 @@ const bamboocssCss = (options) => {
489
523
  * link the pruned copy: one project lost 39% of its atoms that way, presenting as
490
524
  * rarely-used classes such as `md:{display:inline-block}` silently not applying.
491
525
  *
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.
526
+ * So the full extracted stylesheet ships instead, which is what `pruneCss: false` asks
527
+ * for by hand. Being the last environment is not the common case — frameworks build the
528
+ * client first — but it is the only one where the answer is knowable, and a framework
529
+ * that builds its server bundle first does get pruned output.
496
530
  */
497
531
  const pending = remainingEnvironments(session);
498
532
  const { sheets } = optimizeStaticCssAssets(bundle, session, {
499
- rename: renameCssAsset,
500
- prune: pending.length === 0,
533
+ prune: pruneCss && pending.length === 0,
501
534
  sourcemap: environment?.config?.build?.sourcemap
502
535
  });
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, {
536
+ if (sheets && !pruneCss) _bamboocss_logger.logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
537
+ 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
538
  unit: "environment",
505
539
  separator: ", "
506
540
  })} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
@@ -517,6 +551,78 @@ const bamboocssCss = (options) => {
517
551
  //#endregion
518
552
  //#region src/fold-analysis.ts
519
553
  /**
554
+ * Nodes that *compose* a value out of their children rather than computing one.
555
+ *
556
+ * The boundary of the walk below, and the whole of its precision. Climbing through these
557
+ * keeps a value's provenance: `'red.300'` inside `{ color: 'red.300' }` inside a default is
558
+ * still the default's. Anything else — a call, a function body, a JSX element — produces its
559
+ * value by being evaluated, so what is written inside it is an argument to that evaluation
560
+ * and not the enclosing default's value.
561
+ *
562
+ * Without the boundary, `({ cls = css({ color: 'red.300' }) }) => cls` is rejected: the call's
563
+ * own literal argument is syntactically inside a default, so an unbounded walk calls it one.
564
+ * That is correct code, and rejecting it fails the build.
565
+ */
566
+ 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);
567
+ /** Is `inner` written within `outer`? Positions rather than a walk, so it is O(1). */
568
+ const contains = (outer, inner) => outer.getSourceFile() === inner.getSourceFile() && outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
569
+ /**
570
+ * Did this value come from the `= …` of a destructuring binding?
571
+ *
572
+ * `const { tone = 'red.300' } = source` boxes as the literal `'red.300'`: the extractor's
573
+ * `maybeDefinitionValue` tests for an initializer first and returns the boxed default, never
574
+ * reaching the branch that would read `source`. So the default is reported as the value whether
575
+ * or not it is the one that applies.
576
+ *
577
+ * For extraction that is merely optimistic, and deliberately so: a CLI or PostCSS build ships a
578
+ * runtime `css()`, where the default genuinely does apply when the caller omits the key, and it
579
+ * needs a rule behind it. Folding is where the same resolution turns into a wrong answer,
580
+ * because the call is *replaced* by that value.
581
+ *
582
+ * Stops at the first non-composing parent, so a call written inside a default keeps its own
583
+ * provenance, and checks that the binding element was reached through its initializer, so
584
+ * `{ tone = X }`'s name node is not mistaken for its default.
585
+ */
586
+ const isBindingElementDefault = (node) => {
587
+ if (node && ts_morph.Node.isCallExpression(node)) return false;
588
+ let current = node;
589
+ while (current) {
590
+ const parent = current.getParent();
591
+ if (!parent) return false;
592
+ if (ts_morph.Node.isBindingElement(parent)) return parent.getInitializer() === current;
593
+ if (!composesValue(parent)) return false;
594
+ current = parent;
595
+ }
596
+ return false;
597
+ };
598
+ /**
599
+ * The same question asked of a whole box, including how it was resolved.
600
+ *
601
+ * The node a box reports is not always the one its value came from — an empty `{}` default
602
+ * boxes against the call rather than against the `{}` — so the resolution stack is consulted
603
+ * too. A binding element reaches that stack by having been resolved *through*, which is the
604
+ * signal the extractor itself reads when one of these is a conditional's test.
605
+ *
606
+ * Only the entries that are binding elements are examined. Walking up from every other entry
607
+ * was tried and never once changed a verdict across the default spellings or the sandbox's own
608
+ * modules, while accounting for most of the parent hops this does — the stack carries nodes
609
+ * that were never resolved through, including a call's own arguments, so walking from them is
610
+ * both the expensive half and the one that reaches conclusions it has no basis for.
611
+ *
612
+ * A binding element without an initializer carries no default to mistrust: `const { tone } =
613
+ * source` either resolves from `source` or does not resolve at all.
614
+ */
615
+ const isFromBindingDefault = (node) => {
616
+ const own = node.getNode?.();
617
+ if (isBindingElementDefault(own)) return true;
618
+ for (const entry of node.getStack?.() ?? []) {
619
+ if (!ts_morph.Node.isBindingElement(entry) || !entry.getInitializer()) continue;
620
+ if (own && contains(entry, own)) continue;
621
+ return true;
622
+ }
623
+ return false;
624
+ };
625
+ /**
520
626
  * Statically resolvable means: every box in the tree carries a known value.
521
627
  *
522
628
  * `unresolvable` is the extractor saying it could not evaluate a node.
@@ -530,6 +636,7 @@ const isStaticBox = (node, seen = /* @__PURE__ */ new Set()) => {
530
636
  seen.add(node);
531
637
  if (_bamboocss_extractor.box.isUnresolvable(node) || _bamboocss_extractor.box.isConditional(node)) return false;
532
638
  if (!("type" in node) || node.type == null) return false;
639
+ if (isFromBindingDefault(node)) return false;
533
640
  if (_bamboocss_extractor.box.isLiteral(node) && node.value === void 0) {
534
641
  const source = node.getNode?.();
535
642
  return Boolean(source && ts_morph.Node.isIdentifier(source) && source.getText() === "undefined");
@@ -2892,8 +2999,47 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
2892
2999
  //#region src/plugin.ts
2893
3000
  const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
2894
3001
  const NODE_MODULES = /node_modules/;
3002
+ /**
3003
+ * Queries that make Vite serve something other than the module's own source.
3004
+ *
3005
+ * `./theme.tsx?raw` is a module whose text is `export default "…"`, and `?url`, `?worker` and
3006
+ * `?sharedworker` are wrappers of the same kind. The query has to be stripped before the
3007
+ * extension is tested — otherwise nothing matches `.tsx` — and stripping it is what made these
3008
+ * look like the file itself. The transform then handed the wrapper's text to ts-morph *under
3009
+ * the real file's path*, overwriting the parsed module every fold reads for that path.
3010
+ *
3011
+ * That is not theoretical: a module folding `css(shared)` against a sibling the entry also
3012
+ * imported as `?raw` failed the build with "1 call(s) could not be compiled" — the compiler
3013
+ * had read `export default "…"` and found no `shared` to resolve. The advice it prints, to
3014
+ * make the value statically analyzable, is unfollowable, because the source already was.
3015
+ *
3016
+ * Whether it bites depends on which of the two ids Rollup transforms last, so the same project
3017
+ * can build and then stop building because an import moved.
3018
+ *
3019
+ * A deny list rather than an allow list of benign queries: dev ids carry `?t=` after an edit
3020
+ * and `?import` when a dynamic import is rewritten, and rejecting an unrecognised one of those
3021
+ * would silently stop folding a module rather than loudly refuse it.
3022
+ *
3023
+ * Exactly these four, matching Vite's own `SPECIAL_QUERY_RE`. The list was drafted wider —
3024
+ * `?inline`, `?no-inline`, `?worklet`, `?init` — and every one of those was wrong: Vite has no
3025
+ * `worklet` query at all, `?init` is `.wasm` only and that extension is already rejected below,
3026
+ * and `inline`/`no-inline` merely pick base64-versus-file for something that *already* matched
3027
+ * `raw`/`url`, so `./a.tsx?inline` is served as the module's own source. Rejecting an id that
3028
+ * carries real source is the expensive direction: the transform declines, its atoms never reach
3029
+ * the reachability set, pruning removes their rules, and the runtime still returns the class
3030
+ * names — unstyled elements, no error. Only names verified against Vite belong here.
3031
+ *
3032
+ * Note `?worker_file`, which is how dev serves a worker's *real* source, is deliberately absent
3033
+ * and must stay absent. It contains "worker" and is the obvious next entry; adding it would
3034
+ * stop folding every worker module in dev, silently, by the mechanism above.
3035
+ *
3036
+ * Tested against the whole id rather than a split-off query, so it cannot disagree with
3037
+ * `queryOf` in `css.ts` about where the query starts.
3038
+ */
3039
+ const WRAPPED_MODULE_QUERY = /[?&](?:raw|url|worker|sharedworker)(?:&|=|$)/;
2895
3040
  const shouldTransform = (id) => {
2896
3041
  if (id.startsWith("\0")) return false;
3042
+ if (WRAPPED_MODULE_QUERY.test(id)) return false;
2897
3043
  const [filePath] = id.split("?");
2898
3044
  if (!filePath) return false;
2899
3045
  if (NODE_MODULES.test(filePath)) return false;
@@ -2921,6 +3067,18 @@ const isGeneratedOutput = (filePath, ctx) => {
2921
3067
  };
2922
3068
  /** 1-indexed line of a source offset, for an error a user can navigate to. */
2923
3069
  const lineAt = (code, offset) => code.slice(0, offset).split("\n").length;
3070
+ /**
3071
+ * One spelling for a path used as a map key against paths from somewhere else.
3072
+ *
3073
+ * The fold reports dependencies as ts-morph sees them and Vite reports a changed file as its
3074
+ * watcher saw it. On Windows those differ by separator, and can differ by the case of the
3075
+ * drive letter alone — chokidar reports what the OS handed it, `path.resolve` preserves
3076
+ * whatever the cwd had. Either would make every lookup below miss and restore the exact
3077
+ * staleness they exist to fix, silently, since a miss is indistinguishable from a module that
3078
+ * folded nothing. Only the drive letter is case-folded: the rest of the path is compared as
3079
+ * written, because elsewhere the filesystem may well be case-sensitive.
3080
+ */
3081
+ const normalizeFsPath = (file) => (0, node_path.resolve)(file).replaceAll("\\", "/").replace(/^[a-z]:\//, (drive) => drive.toUpperCase());
2924
3082
  const formatSkipped = (id, skipped) => {
2925
3083
  const counts = /* @__PURE__ */ new Map();
2926
3084
  for (const entry of skipped) counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1);
@@ -2940,15 +3098,27 @@ const formatSkipped = (id, skipped) => {
2940
3098
  * with no matching rule.
2941
3099
  */
2942
3100
  const bamboocss = (options = {}) => {
2943
- const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, renameCssAsset = true } = options;
3101
+ const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
3102
+ (0, _bamboocss_node.markStaticCompilerActive)();
2944
3103
  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
- };
3104
+ 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.");
3105
+ /**
3106
+ * What each file's transform found, for the summary. Keyed by file rather than summed as it
3107
+ * goes, because a build has more than one environment and they share most of their modules.
3108
+ *
3109
+ * Running totals double-counted every shared module once per environment — a two-environment
3110
+ * build of one shared file and one entry each reported "2/2 across 2/4 files" for three
3111
+ * source modules. Coverage is a property of the source, not of how many times a bundler
3112
+ * handed the same file over. It also grew without bound in dev, where every HMR
3113
+ * re-transform of a file counted as another file.
3114
+ *
3115
+ * A second pass over a file replaces its entry rather than adding to it. Both environments
3116
+ * are assumed to compute the same answer for the same module — true of this compiler, though
3117
+ * not something the plugin can enforce, since another `pre` plugin may hand each environment
3118
+ * different code. Where they disagree the last one wins, which is a cosmetic number either
3119
+ * way.
3120
+ */
3121
+ const perFile = /* @__PURE__ */ new Map();
2952
3122
  const staticSession = createStaticCompilationSession();
2953
3123
  /**
2954
3124
  * Indexed by file, because the only bulk operation on it is "forget this one's".
@@ -2991,6 +3161,84 @@ const bamboocss = (options = {}) => {
2991
3161
  * would otherwise re-parse its module fifty times, which is the transform path.
2992
3162
  */
2993
3163
  const recipeConfigCache = /* @__PURE__ */ new Map();
3164
+ /**
3165
+ * Which modules folded a value read out of which other module, for dev invalidation.
3166
+ *
3167
+ * `addWatchFile` reports the same edges, and in a build that is enough — Rollup discards a
3168
+ * module whose watched file changed. Vite's dev server does not: a module that *statically
3169
+ * imports* the changed one is only **soft**-invalidated, which by design keeps its cached
3170
+ * transform result and rewrites nothing but the timestamps on its import specifiers. That
3171
+ * cached result is where the compiled class string lives, so the edit never reaches it.
3172
+ *
3173
+ * The recipe case is the one users meet, because it is the one where the class is compiled
3174
+ * into somebody else's module: an inline `cva` declaration is erased, and each *call site*
3175
+ * becomes a literal in the module that calls it. Editing the recipe then updates the class
3176
+ * in a module Vite has decided not to re-transform, so the browser and the SSR render keep
3177
+ * the old class — with no error, and with Vite and Bamboo both logging as if the edit landed.
3178
+ * A restart applies it, which is what makes it read as "recipes do not hot-reload".
3179
+ *
3180
+ * `css(sharedObject)` across modules fails identically; it is rarer only because a consumer
3181
+ * that folds *nothing but* recipe calls has its import erased, and an erased import is not a
3182
+ * static one, so Vite hard-invalidates it and the bug hides. Import one more value from the
3183
+ * same module — the shape any real `ui.ts` has — and the import survives, and so does the
3184
+ * stale class.
3185
+ *
3186
+ * Keyed by dependency, since "what changed" is the question asked, and tracked in the other
3187
+ * direction as well so a re-transform can retract edges the module no longer has.
3188
+ */
3189
+ const dependentsByDependency = /* @__PURE__ */ new Map();
3190
+ const dependenciesByFile = /* @__PURE__ */ new Map();
3191
+ const recordFoldDependencies = (file, dependencies) => {
3192
+ const next = new Set(dependencies.map(normalizeFsPath).filter((dependency) => dependency !== file));
3193
+ const previous = dependenciesByFile.get(file);
3194
+ for (const dependency of previous ?? []) {
3195
+ if (next.has(dependency)) continue;
3196
+ const dependents = dependentsByDependency.get(dependency);
3197
+ if (!dependents?.delete(file)) continue;
3198
+ if (!dependents.size) dependentsByDependency.delete(dependency);
3199
+ }
3200
+ if (!next.size) {
3201
+ dependenciesByFile.delete(file);
3202
+ return;
3203
+ }
3204
+ dependenciesByFile.set(file, next);
3205
+ for (const dependency of next) {
3206
+ const dependents = dependentsByDependency.get(dependency);
3207
+ if (dependents) dependents.add(file);
3208
+ else dependentsByDependency.set(dependency, new Set([file]));
3209
+ }
3210
+ };
3211
+ /**
3212
+ * Modules to re-transform because `file` changed, hard-invalidated on the way out.
3213
+ *
3214
+ * Both halves are load-bearing. Invalidating drops the stale compiled result, which is the
3215
+ * defect itself; returning the modules is what makes Vite propagate an update for them, so
3216
+ * the client applies one rather than waiting for whatever request happens next.
3217
+ *
3218
+ * Invalidating here rather than leaving it to `updateModules` also survives another plugin
3219
+ * filtering the list afterwards — a framework's own `hotUpdate` decides what its route
3220
+ * modules do, and the stale bytes have to go either way.
3221
+ *
3222
+ * A consumer Vite already reached costs nothing: `propagateUpdate` skips a module it has
3223
+ * traversed, and every consumer of a *resolvable* dependency is one, since `addWatchFile`
3224
+ * makes it an importer whether or not the import survived the fold. What is left is the
3225
+ * consumer of a dependency with no module of its own, where Vite matched nothing and would
3226
+ * have done nothing at all. That one can end in a page reload, which is the honest outcome:
3227
+ * its compiled classes really did change, and a reload is what Vite does with any update
3228
+ * nothing accepts.
3229
+ */
3230
+ const foldDependentModules = (file, modules, graph) => {
3231
+ const dependents = dependentsByDependency.get(normalizeFsPath(file));
3232
+ if (!dependents?.size) return;
3233
+ const added = [];
3234
+ for (const dependent of dependents) for (const module of graph.getModulesByFile(dependent) ?? []) {
3235
+ if (modules.includes(module) || added.includes(module)) continue;
3236
+ graph.invalidateModule(module);
3237
+ added.push(module);
3238
+ }
3239
+ if (!added.length) return;
3240
+ return [...modules, ...added];
3241
+ };
2994
3242
  let ctx;
2995
3243
  let runtimeCss;
2996
3244
  let styleCompiler;
@@ -3011,7 +3259,7 @@ const bamboocss = (options = {}) => {
3011
3259
  configPath,
3012
3260
  cwd,
3013
3261
  session: staticSession,
3014
- renameCssAsset
3262
+ pruneCss
3015
3263
  }), {
3016
3264
  name: "bamboocss:compiler",
3017
3265
  enforce: "pre",
@@ -3023,12 +3271,11 @@ const bamboocss = (options = {}) => {
3023
3271
  async buildStart() {
3024
3272
  const environment = this.environment?.name ?? "default";
3025
3273
  if (staticSession.startedEnvironments.has(environment)) {
3026
- totals.folded = 0;
3027
- totals.files = 0;
3028
- totals.filesWithFolds = 0;
3029
- totals.skipped.clear();
3274
+ perFile.clear();
3030
3275
  survivorsByFile.clear();
3031
3276
  recipeConfigCache.clear();
3277
+ dependentsByDependency.clear();
3278
+ dependenciesByFile.clear();
3032
3279
  resetStaticCompilationSession(staticSession);
3033
3280
  }
3034
3281
  staticSession.startedEnvironments.add(environment);
@@ -3066,10 +3313,40 @@ const bamboocss = (options = {}) => {
3066
3313
  recipeConfigCache.clear();
3067
3314
  if (change.event === "delete") {
3068
3315
  ctx.project.removeSourceFile(filePath);
3316
+ recordFoldDependencies(normalizeFsPath(filePath), []);
3069
3317
  return;
3070
3318
  }
3071
3319
  ctx.project.reloadSourceFile(filePath);
3072
3320
  },
3321
+ /**
3322
+ * Re-transform whatever folded a value out of the file that just changed.
3323
+ *
3324
+ * Dev only — `hotUpdate` does not run in a build, where Rollup's own invalidation already
3325
+ * covers this — and additive: the modules Vite matched are returned alongside, so this
3326
+ * decides nothing about them.
3327
+ *
3328
+ * `handleHotUpdate` below stands in on Vite 5, which has no `hotUpdate`. Not quite the
3329
+ * same thing: Vite 5 calls that hook for an update and not for a file appearing or being
3330
+ * deleted, so a recipe file *created* while the server runs leaves its consumers stale
3331
+ * there. Vite 6 and up call `hotUpdate` for all three, and never call `handleHotUpdate`
3332
+ * when a plugin has both — including its deprecation warning — so exactly one of the two
3333
+ * runs on any supported version.
3334
+ *
3335
+ * `environment` optional-chained for the same reason `addWatchFile` is in `transform`:
3336
+ * a harness driving the hook need not supply a full plugin context, and a `TypeError`
3337
+ * here is swallowed into an HMR error payload that a middleware-mode server sends
3338
+ * nowhere.
3339
+ */
3340
+ hotUpdate({ file, modules }) {
3341
+ const graph = this.environment?.moduleGraph;
3342
+ if (!graph) return;
3343
+ return foldDependentModules(file, modules, graph);
3344
+ },
3345
+ handleHotUpdate({ file, modules, server }) {
3346
+ const legacy = server;
3347
+ if (legacy.environments) return;
3348
+ return foldDependentModules(file, modules, legacy.moduleGraph);
3349
+ },
3073
3350
  async transform(code, id) {
3074
3351
  if (!shouldTransform(id)) return null;
3075
3352
  try {
@@ -3101,8 +3378,10 @@ const bamboocss = (options = {}) => {
3101
3378
  });
3102
3379
  } catch (error) {
3103
3380
  _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);
3381
+ perFile.set(filePath, {
3382
+ folded: 0,
3383
+ skipped: new Map([["compile-failed", 1]])
3384
+ });
3106
3385
  addSurvivor({
3107
3386
  file: filePath,
3108
3387
  line: 1,
@@ -3112,10 +3391,15 @@ const bamboocss = (options = {}) => {
3112
3391
  if (command === "serve") throw asError(error, `failed to compile ${filePath}`);
3113
3392
  return null;
3114
3393
  }
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);
3394
+ let skippedHere;
3395
+ for (const entry of result.skipped) {
3396
+ skippedHere ??= /* @__PURE__ */ new Map();
3397
+ skippedHere.set(entry.reason, (skippedHere.get(entry.reason) ?? 0) + 1);
3398
+ }
3399
+ perFile.set(filePath, {
3400
+ folded: result.folded.length,
3401
+ skipped: skippedHere
3402
+ });
3119
3403
  if (result.folded.some((entry) => entry.kind === "class" || entry.kind === "slots")) staticSession.transformedFiles.add((0, node_path.resolve)(filePath));
3120
3404
  for (const entry of result.folded) for (const className of entry.classNames) staticSession.markClassUsed(className);
3121
3405
  for (const entry of result.skipped) {
@@ -3130,6 +3414,7 @@ const bamboocss = (options = {}) => {
3130
3414
  }
3131
3415
  if (reportSkipped && result.skipped.length) _bamboocss_logger.logger.info("vite:transform", formatSkipped(filePath, result.skipped));
3132
3416
  for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
3417
+ recordFoldDependencies(normalizeFsPath(filePath), result.dependencies);
3133
3418
  const forFile = survivorsByFile.get(filePath);
3134
3419
  if (command === "serve" && forFile?.length) throw createSurvivorError(forFile);
3135
3420
  if (!result.folded.length) return null;
@@ -3148,7 +3433,7 @@ const bamboocss = (options = {}) => {
3148
3433
  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
3434
  unit: "class",
3150
3435
  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.`);
3436
+ })}\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
3437
  }
3153
3438
  if (typeof this.getModuleInfo === "function" && !remainingEnvironments(staticSession).length) {
3154
3439
  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 +3444,21 @@ const bamboocss = (options = {}) => {
3159
3444
  })}\n\nAdd them to \`include\` in bamboo.config, or no CSS rule can back their emitted classes.`);
3160
3445
  }
3161
3446
  if (!reportSummary) return;
3162
- const declined = Array.from(totals.skipped.values()).reduce((sum, count) => sum + count, 0);
3163
- const total = totals.folded + declined;
3447
+ if (command === "build" && remainingEnvironments(staticSession).length) return;
3448
+ let folded = 0;
3449
+ let filesWithFolds = 0;
3450
+ const skipped = /* @__PURE__ */ new Map();
3451
+ for (const entry of perFile.values()) {
3452
+ folded += entry.folded;
3453
+ if (entry.folded) filesWithFolds++;
3454
+ for (const [reason, count] of entry.skipped ?? []) skipped.set(reason, (skipped.get(reason) ?? 0) + count);
3455
+ }
3456
+ const declined = Array.from(skipped.values()).reduce((sum, count) => sum + count, 0);
3457
+ const total = folded + declined;
3164
3458
  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}` : ""));
3459
+ const share = Math.round(folded / total * 100);
3460
+ const reasons = Array.from(skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
3461
+ _bamboocss_logger.logger.info("vite:transform", `Compiled ${folded}/${total} (${share}%) across ${filesWithFolds}/${perFile.size} files` + (reasons ? ` — declined: ${reasons}` : ""));
3168
3462
  }
3169
3463
  }];
3170
3464
  };
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
@@ -1,4 +1,5 @@
1
- import { Builder, loadConfigAndCreateContext } from "@bamboocss/node";
1
+ import { findConfig, getConfigDependencies } from "@bamboocss/config";
2
+ import { Builder, loadConfigAndCreateContext, markStaticCompilerActive } from "@bamboocss/node";
2
3
  import { logger } from "@bamboocss/logger";
3
4
  import { compact, createCssUncached, createMergeCss, esc, memo, toHash, truncateList, viewTransitionClassName } from "@bamboocss/shared";
4
5
  import remapping from "@ampproject/remapping";
@@ -276,14 +277,19 @@ const replaceAssetReferences = (bundle, previous, next, sourcemap) => {
276
277
  */
277
278
  const carriesGeneratedCss = (output) => output.type === "asset" && output.fileName.endsWith(".css");
278
279
  /**
279
- * Prune and rename compiler-owned CSS, then give changed assets a hash of their final bytes.
280
+ * Prune compiler-owned CSS, then give any sheet whose bytes changed a hash of those bytes.
280
281
  *
281
282
  * Rollup has already expanded `[hash]` when `generateBundle` runs. Mutating only `source`
282
283
  * would therefore leave two different reachable subsets under one CDN key. The extra final
283
284
  * hash is not cosmetic: it makes late graph reachability cache-safe.
285
+ *
286
+ * Renaming is therefore not a choice this takes. Pruned bytes under the unpruned sheet's name
287
+ * is the one outcome that must never be reachable, and a sheet nothing was removed from keeps
288
+ * its name because its bytes are unchanged — so "rename" is a consequence of "the bytes moved",
289
+ * not a second option. `prune` is the only knob.
284
290
  */
285
291
  const optimizeStaticCssAssets = (bundle, session, options = {}) => {
286
- const { rename = true, prune = true, sourcemap = session.sourcemap } = options;
292
+ const { prune = true, sourcemap = session.sourcemap } = options;
287
293
  /** Assets in this bundle that carry the generated stylesheet, pruned or not. */
288
294
  let sheets = 0;
289
295
  for (const output of Object.values(bundle)) {
@@ -295,12 +301,7 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
295
301
  const optimized = pruneStaticCss(source, session);
296
302
  output.source = optimized;
297
303
  if (optimized === source) continue;
298
- if (!rename) {
299
- output.source = source;
300
- continue;
301
- }
302
304
  const nextName = output.fileName.replace(/\.css$/, `.b-${toHash(optimized)}.css`);
303
- if (nextName === output.fileName) continue;
304
305
  if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
305
306
  const previous = output.fileName;
306
307
  output.fileName = nextName;
@@ -322,7 +323,7 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
322
323
  * process just wrote, which is a race on any watch rebuild.
323
324
  */
324
325
  const bamboocssCss = (options) => {
325
- const { configPath, cwd, session, renameCssAsset = true } = options;
326
+ const { configPath, cwd, session, pruneCss = true } = options;
326
327
  /**
327
328
  * Environments whose `load` served the virtual stylesheet.
328
329
  *
@@ -396,6 +397,39 @@ const bamboocssCss = (options) => {
396
397
  configResolved(config) {
397
398
  command = config.command;
398
399
  session.sourcemap = config.build.sourcemap;
400
+ /**
401
+ * Tell Vite that `bamboo.config.ts` is a config file, so editing one restarts the server.
402
+ *
403
+ * Tokens live there, and they are what a designer iterates on most — "restart the dev
404
+ * server to see a colour change" is the wrong instruction for the file most likely to be
405
+ * edited all afternoon. Nothing watched it: `watch` is the CLI's own watcher, and a
406
+ * project running `vite dev` never reaches it.
407
+ *
408
+ * A restart rather than re-emitting the stylesheet, because this plugin and the compiler
409
+ * hold *separate* contexts and only this one reloads its config. A token *value* edit
410
+ * came out right on the next source change, and an edit that changes what compiles —
411
+ * adding a token, a condition, a utility — left the compiler naming classes from the old
412
+ * config against a sheet emitted from the new one. Half-updated is worse than stale.
413
+ *
414
+ * Through Vite's own list rather than a watcher of ours. Vite adds these paths to the
415
+ * files it watches, which is what reaches a config *outside* `root` — a monorepo with one
416
+ * config above `apps/web`, or a preset resolved into `node_modules`, neither of which the
417
+ * project watcher covers. It also means the restart is Vite's, with its own concurrency
418
+ * guard and its own error reporting, rather than a second implementation of both.
419
+ *
420
+ * The config's own import graph is resolved the way `Builder` resolves it, minus the
421
+ * tsconfig paths it has not loaded yet at this point. `dependencies` globs are not
422
+ * expanded here: they are declared as an escape hatch for a *config reload*, and turning
423
+ * every file matching one into a full server restart is not what a project asking for
424
+ * that meant.
425
+ */
426
+ if (config.command === "serve") try {
427
+ const { deps } = getConfigDependencies(findConfig({
428
+ cwd: cwd ?? config.root,
429
+ file: configPath
430
+ }));
431
+ config.configFileDependencies.push(...deps);
432
+ } catch {}
399
433
  if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
400
434
  },
401
435
  /**
@@ -459,18 +493,18 @@ const bamboocssCss = (options) => {
459
493
  * link the pruned copy: one project lost 39% of its atoms that way, presenting as
460
494
  * rarely-used classes such as `md:{display:inline-block}` silently not applying.
461
495
  *
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.
496
+ * So the full extracted stylesheet ships instead, which is what `pruneCss: false` asks
497
+ * for by hand. Being the last environment is not the common case — frameworks build the
498
+ * client first — but it is the only one where the answer is knowable, and a framework
499
+ * that builds its server bundle first does get pruned output.
466
500
  */
467
501
  const pending = remainingEnvironments(session);
468
502
  const { sheets } = optimizeStaticCssAssets(bundle, session, {
469
- rename: renameCssAsset,
470
- prune: pending.length === 0,
503
+ prune: pruneCss && pending.length === 0,
471
504
  sourcemap: environment?.config?.build?.sourcemap
472
505
  });
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, {
506
+ if (sheets && !pruneCss) logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
507
+ 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
508
  unit: "environment",
475
509
  separator: ", "
476
510
  })} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
@@ -487,6 +521,78 @@ const bamboocssCss = (options) => {
487
521
  //#endregion
488
522
  //#region src/fold-analysis.ts
489
523
  /**
524
+ * Nodes that *compose* a value out of their children rather than computing one.
525
+ *
526
+ * The boundary of the walk below, and the whole of its precision. Climbing through these
527
+ * keeps a value's provenance: `'red.300'` inside `{ color: 'red.300' }` inside a default is
528
+ * still the default's. Anything else — a call, a function body, a JSX element — produces its
529
+ * value by being evaluated, so what is written inside it is an argument to that evaluation
530
+ * and not the enclosing default's value.
531
+ *
532
+ * Without the boundary, `({ cls = css({ color: 'red.300' }) }) => cls` is rejected: the call's
533
+ * own literal argument is syntactically inside a default, so an unbounded walk calls it one.
534
+ * That is correct code, and rejecting it fails the build.
535
+ */
536
+ 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);
537
+ /** Is `inner` written within `outer`? Positions rather than a walk, so it is O(1). */
538
+ const contains = (outer, inner) => outer.getSourceFile() === inner.getSourceFile() && outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
539
+ /**
540
+ * Did this value come from the `= …` of a destructuring binding?
541
+ *
542
+ * `const { tone = 'red.300' } = source` boxes as the literal `'red.300'`: the extractor's
543
+ * `maybeDefinitionValue` tests for an initializer first and returns the boxed default, never
544
+ * reaching the branch that would read `source`. So the default is reported as the value whether
545
+ * or not it is the one that applies.
546
+ *
547
+ * For extraction that is merely optimistic, and deliberately so: a CLI or PostCSS build ships a
548
+ * runtime `css()`, where the default genuinely does apply when the caller omits the key, and it
549
+ * needs a rule behind it. Folding is where the same resolution turns into a wrong answer,
550
+ * because the call is *replaced* by that value.
551
+ *
552
+ * Stops at the first non-composing parent, so a call written inside a default keeps its own
553
+ * provenance, and checks that the binding element was reached through its initializer, so
554
+ * `{ tone = X }`'s name node is not mistaken for its default.
555
+ */
556
+ const isBindingElementDefault = (node) => {
557
+ if (node && Node.isCallExpression(node)) return false;
558
+ let current = node;
559
+ while (current) {
560
+ const parent = current.getParent();
561
+ if (!parent) return false;
562
+ if (Node.isBindingElement(parent)) return parent.getInitializer() === current;
563
+ if (!composesValue(parent)) return false;
564
+ current = parent;
565
+ }
566
+ return false;
567
+ };
568
+ /**
569
+ * The same question asked of a whole box, including how it was resolved.
570
+ *
571
+ * The node a box reports is not always the one its value came from — an empty `{}` default
572
+ * boxes against the call rather than against the `{}` — so the resolution stack is consulted
573
+ * too. A binding element reaches that stack by having been resolved *through*, which is the
574
+ * signal the extractor itself reads when one of these is a conditional's test.
575
+ *
576
+ * Only the entries that are binding elements are examined. Walking up from every other entry
577
+ * was tried and never once changed a verdict across the default spellings or the sandbox's own
578
+ * modules, while accounting for most of the parent hops this does — the stack carries nodes
579
+ * that were never resolved through, including a call's own arguments, so walking from them is
580
+ * both the expensive half and the one that reaches conclusions it has no basis for.
581
+ *
582
+ * A binding element without an initializer carries no default to mistrust: `const { tone } =
583
+ * source` either resolves from `source` or does not resolve at all.
584
+ */
585
+ const isFromBindingDefault = (node) => {
586
+ const own = node.getNode?.();
587
+ if (isBindingElementDefault(own)) return true;
588
+ for (const entry of node.getStack?.() ?? []) {
589
+ if (!Node.isBindingElement(entry) || !entry.getInitializer()) continue;
590
+ if (own && contains(entry, own)) continue;
591
+ return true;
592
+ }
593
+ return false;
594
+ };
595
+ /**
490
596
  * Statically resolvable means: every box in the tree carries a known value.
491
597
  *
492
598
  * `unresolvable` is the extractor saying it could not evaluate a node.
@@ -500,6 +606,7 @@ const isStaticBox = (node, seen = /* @__PURE__ */ new Set()) => {
500
606
  seen.add(node);
501
607
  if (box.isUnresolvable(node) || box.isConditional(node)) return false;
502
608
  if (!("type" in node) || node.type == null) return false;
609
+ if (isFromBindingDefault(node)) return false;
503
610
  if (box.isLiteral(node) && node.value === void 0) {
504
611
  const source = node.getNode?.();
505
612
  return Boolean(source && Node.isIdentifier(source) && source.getText() === "undefined");
@@ -2862,8 +2969,47 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
2862
2969
  //#region src/plugin.ts
2863
2970
  const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
2864
2971
  const NODE_MODULES = /node_modules/;
2972
+ /**
2973
+ * Queries that make Vite serve something other than the module's own source.
2974
+ *
2975
+ * `./theme.tsx?raw` is a module whose text is `export default "…"`, and `?url`, `?worker` and
2976
+ * `?sharedworker` are wrappers of the same kind. The query has to be stripped before the
2977
+ * extension is tested — otherwise nothing matches `.tsx` — and stripping it is what made these
2978
+ * look like the file itself. The transform then handed the wrapper's text to ts-morph *under
2979
+ * the real file's path*, overwriting the parsed module every fold reads for that path.
2980
+ *
2981
+ * That is not theoretical: a module folding `css(shared)` against a sibling the entry also
2982
+ * imported as `?raw` failed the build with "1 call(s) could not be compiled" — the compiler
2983
+ * had read `export default "…"` and found no `shared` to resolve. The advice it prints, to
2984
+ * make the value statically analyzable, is unfollowable, because the source already was.
2985
+ *
2986
+ * Whether it bites depends on which of the two ids Rollup transforms last, so the same project
2987
+ * can build and then stop building because an import moved.
2988
+ *
2989
+ * A deny list rather than an allow list of benign queries: dev ids carry `?t=` after an edit
2990
+ * and `?import` when a dynamic import is rewritten, and rejecting an unrecognised one of those
2991
+ * would silently stop folding a module rather than loudly refuse it.
2992
+ *
2993
+ * Exactly these four, matching Vite's own `SPECIAL_QUERY_RE`. The list was drafted wider —
2994
+ * `?inline`, `?no-inline`, `?worklet`, `?init` — and every one of those was wrong: Vite has no
2995
+ * `worklet` query at all, `?init` is `.wasm` only and that extension is already rejected below,
2996
+ * and `inline`/`no-inline` merely pick base64-versus-file for something that *already* matched
2997
+ * `raw`/`url`, so `./a.tsx?inline` is served as the module's own source. Rejecting an id that
2998
+ * carries real source is the expensive direction: the transform declines, its atoms never reach
2999
+ * the reachability set, pruning removes their rules, and the runtime still returns the class
3000
+ * names — unstyled elements, no error. Only names verified against Vite belong here.
3001
+ *
3002
+ * Note `?worker_file`, which is how dev serves a worker's *real* source, is deliberately absent
3003
+ * and must stay absent. It contains "worker" and is the obvious next entry; adding it would
3004
+ * stop folding every worker module in dev, silently, by the mechanism above.
3005
+ *
3006
+ * Tested against the whole id rather than a split-off query, so it cannot disagree with
3007
+ * `queryOf` in `css.ts` about where the query starts.
3008
+ */
3009
+ const WRAPPED_MODULE_QUERY = /[?&](?:raw|url|worker|sharedworker)(?:&|=|$)/;
2865
3010
  const shouldTransform = (id) => {
2866
3011
  if (id.startsWith("\0")) return false;
3012
+ if (WRAPPED_MODULE_QUERY.test(id)) return false;
2867
3013
  const [filePath] = id.split("?");
2868
3014
  if (!filePath) return false;
2869
3015
  if (NODE_MODULES.test(filePath)) return false;
@@ -2891,6 +3037,18 @@ const isGeneratedOutput = (filePath, ctx) => {
2891
3037
  };
2892
3038
  /** 1-indexed line of a source offset, for an error a user can navigate to. */
2893
3039
  const lineAt = (code, offset) => code.slice(0, offset).split("\n").length;
3040
+ /**
3041
+ * One spelling for a path used as a map key against paths from somewhere else.
3042
+ *
3043
+ * The fold reports dependencies as ts-morph sees them and Vite reports a changed file as its
3044
+ * watcher saw it. On Windows those differ by separator, and can differ by the case of the
3045
+ * drive letter alone — chokidar reports what the OS handed it, `path.resolve` preserves
3046
+ * whatever the cwd had. Either would make every lookup below miss and restore the exact
3047
+ * staleness they exist to fix, silently, since a miss is indistinguishable from a module that
3048
+ * folded nothing. Only the drive letter is case-folded: the rest of the path is compared as
3049
+ * written, because elsewhere the filesystem may well be case-sensitive.
3050
+ */
3051
+ const normalizeFsPath = (file) => resolve(file).replaceAll("\\", "/").replace(/^[a-z]:\//, (drive) => drive.toUpperCase());
2894
3052
  const formatSkipped = (id, skipped) => {
2895
3053
  const counts = /* @__PURE__ */ new Map();
2896
3054
  for (const entry of skipped) counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1);
@@ -2910,15 +3068,27 @@ const formatSkipped = (id, skipped) => {
2910
3068
  * with no matching rule.
2911
3069
  */
2912
3070
  const bamboocss = (options = {}) => {
2913
- const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, renameCssAsset = true } = options;
3071
+ const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
3072
+ markStaticCompilerActive();
2914
3073
  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
- };
3074
+ 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.");
3075
+ /**
3076
+ * What each file's transform found, for the summary. Keyed by file rather than summed as it
3077
+ * goes, because a build has more than one environment and they share most of their modules.
3078
+ *
3079
+ * Running totals double-counted every shared module once per environment — a two-environment
3080
+ * build of one shared file and one entry each reported "2/2 across 2/4 files" for three
3081
+ * source modules. Coverage is a property of the source, not of how many times a bundler
3082
+ * handed the same file over. It also grew without bound in dev, where every HMR
3083
+ * re-transform of a file counted as another file.
3084
+ *
3085
+ * A second pass over a file replaces its entry rather than adding to it. Both environments
3086
+ * are assumed to compute the same answer for the same module — true of this compiler, though
3087
+ * not something the plugin can enforce, since another `pre` plugin may hand each environment
3088
+ * different code. Where they disagree the last one wins, which is a cosmetic number either
3089
+ * way.
3090
+ */
3091
+ const perFile = /* @__PURE__ */ new Map();
2922
3092
  const staticSession = createStaticCompilationSession();
2923
3093
  /**
2924
3094
  * Indexed by file, because the only bulk operation on it is "forget this one's".
@@ -2961,6 +3131,84 @@ const bamboocss = (options = {}) => {
2961
3131
  * would otherwise re-parse its module fifty times, which is the transform path.
2962
3132
  */
2963
3133
  const recipeConfigCache = /* @__PURE__ */ new Map();
3134
+ /**
3135
+ * Which modules folded a value read out of which other module, for dev invalidation.
3136
+ *
3137
+ * `addWatchFile` reports the same edges, and in a build that is enough — Rollup discards a
3138
+ * module whose watched file changed. Vite's dev server does not: a module that *statically
3139
+ * imports* the changed one is only **soft**-invalidated, which by design keeps its cached
3140
+ * transform result and rewrites nothing but the timestamps on its import specifiers. That
3141
+ * cached result is where the compiled class string lives, so the edit never reaches it.
3142
+ *
3143
+ * The recipe case is the one users meet, because it is the one where the class is compiled
3144
+ * into somebody else's module: an inline `cva` declaration is erased, and each *call site*
3145
+ * becomes a literal in the module that calls it. Editing the recipe then updates the class
3146
+ * in a module Vite has decided not to re-transform, so the browser and the SSR render keep
3147
+ * the old class — with no error, and with Vite and Bamboo both logging as if the edit landed.
3148
+ * A restart applies it, which is what makes it read as "recipes do not hot-reload".
3149
+ *
3150
+ * `css(sharedObject)` across modules fails identically; it is rarer only because a consumer
3151
+ * that folds *nothing but* recipe calls has its import erased, and an erased import is not a
3152
+ * static one, so Vite hard-invalidates it and the bug hides. Import one more value from the
3153
+ * same module — the shape any real `ui.ts` has — and the import survives, and so does the
3154
+ * stale class.
3155
+ *
3156
+ * Keyed by dependency, since "what changed" is the question asked, and tracked in the other
3157
+ * direction as well so a re-transform can retract edges the module no longer has.
3158
+ */
3159
+ const dependentsByDependency = /* @__PURE__ */ new Map();
3160
+ const dependenciesByFile = /* @__PURE__ */ new Map();
3161
+ const recordFoldDependencies = (file, dependencies) => {
3162
+ const next = new Set(dependencies.map(normalizeFsPath).filter((dependency) => dependency !== file));
3163
+ const previous = dependenciesByFile.get(file);
3164
+ for (const dependency of previous ?? []) {
3165
+ if (next.has(dependency)) continue;
3166
+ const dependents = dependentsByDependency.get(dependency);
3167
+ if (!dependents?.delete(file)) continue;
3168
+ if (!dependents.size) dependentsByDependency.delete(dependency);
3169
+ }
3170
+ if (!next.size) {
3171
+ dependenciesByFile.delete(file);
3172
+ return;
3173
+ }
3174
+ dependenciesByFile.set(file, next);
3175
+ for (const dependency of next) {
3176
+ const dependents = dependentsByDependency.get(dependency);
3177
+ if (dependents) dependents.add(file);
3178
+ else dependentsByDependency.set(dependency, new Set([file]));
3179
+ }
3180
+ };
3181
+ /**
3182
+ * Modules to re-transform because `file` changed, hard-invalidated on the way out.
3183
+ *
3184
+ * Both halves are load-bearing. Invalidating drops the stale compiled result, which is the
3185
+ * defect itself; returning the modules is what makes Vite propagate an update for them, so
3186
+ * the client applies one rather than waiting for whatever request happens next.
3187
+ *
3188
+ * Invalidating here rather than leaving it to `updateModules` also survives another plugin
3189
+ * filtering the list afterwards — a framework's own `hotUpdate` decides what its route
3190
+ * modules do, and the stale bytes have to go either way.
3191
+ *
3192
+ * A consumer Vite already reached costs nothing: `propagateUpdate` skips a module it has
3193
+ * traversed, and every consumer of a *resolvable* dependency is one, since `addWatchFile`
3194
+ * makes it an importer whether or not the import survived the fold. What is left is the
3195
+ * consumer of a dependency with no module of its own, where Vite matched nothing and would
3196
+ * have done nothing at all. That one can end in a page reload, which is the honest outcome:
3197
+ * its compiled classes really did change, and a reload is what Vite does with any update
3198
+ * nothing accepts.
3199
+ */
3200
+ const foldDependentModules = (file, modules, graph) => {
3201
+ const dependents = dependentsByDependency.get(normalizeFsPath(file));
3202
+ if (!dependents?.size) return;
3203
+ const added = [];
3204
+ for (const dependent of dependents) for (const module of graph.getModulesByFile(dependent) ?? []) {
3205
+ if (modules.includes(module) || added.includes(module)) continue;
3206
+ graph.invalidateModule(module);
3207
+ added.push(module);
3208
+ }
3209
+ if (!added.length) return;
3210
+ return [...modules, ...added];
3211
+ };
2964
3212
  let ctx;
2965
3213
  let runtimeCss;
2966
3214
  let styleCompiler;
@@ -2981,7 +3229,7 @@ const bamboocss = (options = {}) => {
2981
3229
  configPath,
2982
3230
  cwd,
2983
3231
  session: staticSession,
2984
- renameCssAsset
3232
+ pruneCss
2985
3233
  }), {
2986
3234
  name: "bamboocss:compiler",
2987
3235
  enforce: "pre",
@@ -2993,12 +3241,11 @@ const bamboocss = (options = {}) => {
2993
3241
  async buildStart() {
2994
3242
  const environment = this.environment?.name ?? "default";
2995
3243
  if (staticSession.startedEnvironments.has(environment)) {
2996
- totals.folded = 0;
2997
- totals.files = 0;
2998
- totals.filesWithFolds = 0;
2999
- totals.skipped.clear();
3244
+ perFile.clear();
3000
3245
  survivorsByFile.clear();
3001
3246
  recipeConfigCache.clear();
3247
+ dependentsByDependency.clear();
3248
+ dependenciesByFile.clear();
3002
3249
  resetStaticCompilationSession(staticSession);
3003
3250
  }
3004
3251
  staticSession.startedEnvironments.add(environment);
@@ -3036,10 +3283,40 @@ const bamboocss = (options = {}) => {
3036
3283
  recipeConfigCache.clear();
3037
3284
  if (change.event === "delete") {
3038
3285
  ctx.project.removeSourceFile(filePath);
3286
+ recordFoldDependencies(normalizeFsPath(filePath), []);
3039
3287
  return;
3040
3288
  }
3041
3289
  ctx.project.reloadSourceFile(filePath);
3042
3290
  },
3291
+ /**
3292
+ * Re-transform whatever folded a value out of the file that just changed.
3293
+ *
3294
+ * Dev only — `hotUpdate` does not run in a build, where Rollup's own invalidation already
3295
+ * covers this — and additive: the modules Vite matched are returned alongside, so this
3296
+ * decides nothing about them.
3297
+ *
3298
+ * `handleHotUpdate` below stands in on Vite 5, which has no `hotUpdate`. Not quite the
3299
+ * same thing: Vite 5 calls that hook for an update and not for a file appearing or being
3300
+ * deleted, so a recipe file *created* while the server runs leaves its consumers stale
3301
+ * there. Vite 6 and up call `hotUpdate` for all three, and never call `handleHotUpdate`
3302
+ * when a plugin has both — including its deprecation warning — so exactly one of the two
3303
+ * runs on any supported version.
3304
+ *
3305
+ * `environment` optional-chained for the same reason `addWatchFile` is in `transform`:
3306
+ * a harness driving the hook need not supply a full plugin context, and a `TypeError`
3307
+ * here is swallowed into an HMR error payload that a middleware-mode server sends
3308
+ * nowhere.
3309
+ */
3310
+ hotUpdate({ file, modules }) {
3311
+ const graph = this.environment?.moduleGraph;
3312
+ if (!graph) return;
3313
+ return foldDependentModules(file, modules, graph);
3314
+ },
3315
+ handleHotUpdate({ file, modules, server }) {
3316
+ const legacy = server;
3317
+ if (legacy.environments) return;
3318
+ return foldDependentModules(file, modules, legacy.moduleGraph);
3319
+ },
3043
3320
  async transform(code, id) {
3044
3321
  if (!shouldTransform(id)) return null;
3045
3322
  try {
@@ -3071,8 +3348,10 @@ const bamboocss = (options = {}) => {
3071
3348
  });
3072
3349
  } catch (error) {
3073
3350
  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);
3351
+ perFile.set(filePath, {
3352
+ folded: 0,
3353
+ skipped: new Map([["compile-failed", 1]])
3354
+ });
3076
3355
  addSurvivor({
3077
3356
  file: filePath,
3078
3357
  line: 1,
@@ -3082,10 +3361,15 @@ const bamboocss = (options = {}) => {
3082
3361
  if (command === "serve") throw asError(error, `failed to compile ${filePath}`);
3083
3362
  return null;
3084
3363
  }
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);
3364
+ let skippedHere;
3365
+ for (const entry of result.skipped) {
3366
+ skippedHere ??= /* @__PURE__ */ new Map();
3367
+ skippedHere.set(entry.reason, (skippedHere.get(entry.reason) ?? 0) + 1);
3368
+ }
3369
+ perFile.set(filePath, {
3370
+ folded: result.folded.length,
3371
+ skipped: skippedHere
3372
+ });
3089
3373
  if (result.folded.some((entry) => entry.kind === "class" || entry.kind === "slots")) staticSession.transformedFiles.add(resolve(filePath));
3090
3374
  for (const entry of result.folded) for (const className of entry.classNames) staticSession.markClassUsed(className);
3091
3375
  for (const entry of result.skipped) {
@@ -3100,6 +3384,7 @@ const bamboocss = (options = {}) => {
3100
3384
  }
3101
3385
  if (reportSkipped && result.skipped.length) logger.info("vite:transform", formatSkipped(filePath, result.skipped));
3102
3386
  for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
3387
+ recordFoldDependencies(normalizeFsPath(filePath), result.dependencies);
3103
3388
  const forFile = survivorsByFile.get(filePath);
3104
3389
  if (command === "serve" && forFile?.length) throw createSurvivorError(forFile);
3105
3390
  if (!result.folded.length) return null;
@@ -3118,7 +3403,7 @@ const bamboocss = (options = {}) => {
3118
3403
  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
3404
  unit: "class",
3120
3405
  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.`);
3406
+ })}\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
3407
  }
3123
3408
  if (typeof this.getModuleInfo === "function" && !remainingEnvironments(staticSession).length) {
3124
3409
  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 +3414,21 @@ const bamboocss = (options = {}) => {
3129
3414
  })}\n\nAdd them to \`include\` in bamboo.config, or no CSS rule can back their emitted classes.`);
3130
3415
  }
3131
3416
  if (!reportSummary) return;
3132
- const declined = Array.from(totals.skipped.values()).reduce((sum, count) => sum + count, 0);
3133
- const total = totals.folded + declined;
3417
+ if (command === "build" && remainingEnvironments(staticSession).length) return;
3418
+ let folded = 0;
3419
+ let filesWithFolds = 0;
3420
+ const skipped = /* @__PURE__ */ new Map();
3421
+ for (const entry of perFile.values()) {
3422
+ folded += entry.folded;
3423
+ if (entry.folded) filesWithFolds++;
3424
+ for (const [reason, count] of entry.skipped ?? []) skipped.set(reason, (skipped.get(reason) ?? 0) + count);
3425
+ }
3426
+ const declined = Array.from(skipped.values()).reduce((sum, count) => sum + count, 0);
3427
+ const total = folded + declined;
3134
3428
  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}` : ""));
3429
+ const share = Math.round(folded / total * 100);
3430
+ const reasons = Array.from(skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
3431
+ logger.info("vite:transform", `Compiled ${folded}/${total} (${share}%) across ${filesWithFolds}/${perFile.size} files` + (reasons ? ` — declined: ${reasons}` : ""));
3138
3432
  }
3139
3433
  }];
3140
3434
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.37.13",
3
+ "version": "1.39.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/core": "1.39.0",
44
+ "@bamboocss/extractor": "1.39.0",
45
+ "@bamboocss/config": "1.39.0",
46
+ "@bamboocss/logger": "1.39.0",
47
+ "@bamboocss/node": "1.39.0",
48
+ "@bamboocss/shared": "1.39.0",
49
+ "@bamboocss/types": "1.39.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.39.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": ">=5"