@bamboocss/vite 1.46.1 → 1.46.3

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.
@@ -103,10 +103,11 @@ const pruneStaticCss = (css, session, { environment, prune = true, requiredClass
103
103
  return ` ${className}\n (${extracted}; no rule in the sheet` + (near.length ? `; a rule exists under ${near.map((n) => JSON.stringify(n)).join(", ")}` : "") + `)`;
104
104
  };
105
105
  const environmentDescription = environment ? ` for the ${JSON.stringify(environment)} environment` : "";
106
+ const guidance = orphaned.every((className) => prunable.has(require_class_name.bare(className))) ? "Extraction saw every one of these calls, but rule generation produced no declarations for them — the shape of a value nothing resolves, such as a `mixin` naming a composition the theme does not define. Check the class name's property and value against the theme, and look for a `🎋 warn [utility]` line above naming the same value. Set `unresolvedToken: 'error'` to fail fast at the exact call next time." : "The current source generation no longer provides every rule required by the JavaScript outputs still on disk. Bamboo refused to replace the prior stylesheet. Finish rebuilding every output which retains an older generation, then rebuild the stylesheet. If every output is already current, report this as a compiler bug with the block above.";
106
107
  throw new Error(`bamboocss: ${orphaned.length} compiled class(es) still named by live output have no rule in the candidate stylesheet${environmentDescription}. Elements carrying them would render unstyled.\n\n${(0, _bamboocss_shared.truncateList)(orphaned.map(describe), {
107
108
  unit: "class",
108
109
  separator: "\n"
109
- })}\n\nThe current source generation no longer provides every rule required by the JavaScript outputs still on disk. Bamboo refused to replace the prior stylesheet. Finish rebuilding every output which retains an older generation, then rebuild the stylesheet. If every output is already current, report this as a compiler bug with the block above.`);
110
+ })}\n\n` + guidance);
110
111
  }
111
112
  return root.toString();
112
113
  };
@@ -98,10 +98,11 @@ const pruneStaticCss = (css, session, { environment, prune = true, requiredClass
98
98
  return ` ${className}\n (${extracted}; no rule in the sheet` + (near.length ? `; a rule exists under ${near.map((n) => JSON.stringify(n)).join(", ")}` : "") + `)`;
99
99
  };
100
100
  const environmentDescription = environment ? ` for the ${JSON.stringify(environment)} environment` : "";
101
+ const guidance = orphaned.every((className) => prunable.has(bare(className))) ? "Extraction saw every one of these calls, but rule generation produced no declarations for them — the shape of a value nothing resolves, such as a `mixin` naming a composition the theme does not define. Check the class name's property and value against the theme, and look for a `🎋 warn [utility]` line above naming the same value. Set `unresolvedToken: 'error'` to fail fast at the exact call next time." : "The current source generation no longer provides every rule required by the JavaScript outputs still on disk. Bamboo refused to replace the prior stylesheet. Finish rebuilding every output which retains an older generation, then rebuild the stylesheet. If every output is already current, report this as a compiler bug with the block above.";
101
102
  throw new Error(`bamboocss: ${orphaned.length} compiled class(es) still named by live output have no rule in the candidate stylesheet${environmentDescription}. Elements carrying them would render unstyled.\n\n${truncateList(orphaned.map(describe), {
102
103
  unit: "class",
103
104
  separator: "\n"
104
- })}\n\nThe current source generation no longer provides every rule required by the JavaScript outputs still on disk. Bamboo refused to replace the prior stylesheet. Finish rebuilding every output which retains an older generation, then rebuild the stylesheet. If every output is already current, report this as a compiler bug with the block above.`);
105
+ })}\n\n` + guidance);
105
106
  }
106
107
  return root.toString();
107
108
  };
@@ -1,5 +1,6 @@
1
1
  const require_chunk = require("./chunk.cjs");
2
2
  let _bamboocss_shared = require("@bamboocss/shared");
3
+ let node_crypto = require("node:crypto");
3
4
  let node_path = require("node:path");
4
5
  let magic_string = require("magic-string");
5
6
  magic_string = require_chunk.__toESM(magic_string);
@@ -1007,6 +1008,97 @@ const createRuntimeToken = (ctx) => (path) => tokenValuesFor(ctx).get(path)?.var
1007
1008
  //#endregion
1008
1009
  //#region src/fold.ts
1009
1010
  /**
1011
+ * Decide whether an edit to `changedFile` moved anything a dependent's fold actually read.
1012
+ *
1013
+ * `'unchanged'` means every recorded read of the edited file re-digests to the same value —
1014
+ * the dependent's fold inputs did not move, so its output cannot have, and the re-fold can be
1015
+ * skipped outright. `'changed'` means a read definitely differs, which is the same verdict
1016
+ * the re-fold would reach after doing all the work. `'unknown'` is every other situation —
1017
+ * no read names the edited file (the relationship runs through a channel these records do
1018
+ * not witness, a barrel hop in a recipe binding walk, say), a digest could not be pinned
1019
+ * down on either side — and sends the caller to the full re-fold this replaced.
1020
+ *
1021
+ * `digestMemo` is the per-event cache: many dependents verify against the same edited file,
1022
+ * and each distinct `(kind, file, name)` needs digesting once, not once per dependent.
1023
+ */
1024
+ const verifyExportReads = (ctx, parseModule, reads, changedFile, digestMemo) => {
1025
+ const relevant = reads.filter((read) => read.file === changedFile);
1026
+ if (!relevant.length) return {
1027
+ verdict: "unknown",
1028
+ crossings: []
1029
+ };
1030
+ const crossings = /* @__PURE__ */ new Set();
1031
+ for (const read of relevant) {
1032
+ if (read.digest === void 0) return {
1033
+ verdict: "unknown",
1034
+ crossings: []
1035
+ };
1036
+ const key = `${read.kind}\u0000${read.file}\u0000${read.name}`;
1037
+ if (!digestMemo.has(key)) if (read.kind === "value") {
1038
+ const crossed = [];
1039
+ const digest = ctx.project?.digestExportRead?.(read.file, read.name, (path) => crossed.push(path));
1040
+ digestMemo.set(key, {
1041
+ digest,
1042
+ crossings: crossed
1043
+ });
1044
+ } else {
1045
+ const batched = `recipe-file\u0000${read.file}`;
1046
+ if (!digestMemo.has(batched)) {
1047
+ try {
1048
+ const result = parseModule(read.file);
1049
+ if (result) for (const [name, entry] of collectRecipeConfigs(result)) digestMemo.set(`recipe\u0000${read.file}\u0000${name}`, {
1050
+ digest: entry === AMBIGUOUS ? "bamboo:export-missing" : digestRecipeConfig(entry),
1051
+ crossings: []
1052
+ });
1053
+ } catch {}
1054
+ digestMemo.set(batched, {
1055
+ digest: "bamboo:batched",
1056
+ crossings: []
1057
+ });
1058
+ }
1059
+ if (!digestMemo.has(key)) digestMemo.set(key, {
1060
+ digest: digestRecipeReadNow(parseModule, read.file, read.name),
1061
+ crossings: []
1062
+ });
1063
+ }
1064
+ const entry = digestMemo.get(key);
1065
+ if (entry.digest === void 0) return {
1066
+ verdict: "unknown",
1067
+ crossings: []
1068
+ };
1069
+ if (entry.digest !== read.digest) return {
1070
+ verdict: "changed",
1071
+ crossings: []
1072
+ };
1073
+ for (const path of entry.crossings) crossings.add(path);
1074
+ }
1075
+ return {
1076
+ verdict: "unchanged",
1077
+ crossings: [...crossings]
1078
+ };
1079
+ };
1080
+ const digestRecipeReadNow = (parseModule, file, name) => {
1081
+ try {
1082
+ const result = parseModule(file);
1083
+ if (!result) return "bamboo:module-missing";
1084
+ const entry = collectRecipeConfigs(result).get(name);
1085
+ if (!entry || entry === AMBIGUOUS) return "bamboo:export-missing";
1086
+ return digestRecipeConfig(entry);
1087
+ } catch {
1088
+ return;
1089
+ }
1090
+ };
1091
+ /** The verification witness for a foreign recipe read: the config bytes, order preserved. */
1092
+ const digestRecipeConfig = (entry) => {
1093
+ try {
1094
+ const json = JSON.stringify(entry.config, (_key, value) => value === void 0 ? "bamboo:undefined" : value);
1095
+ if (json === void 0) return void 0;
1096
+ return (0, node_crypto.createHash)("sha256").update(json).digest("base64");
1097
+ } catch {
1098
+ return;
1099
+ }
1100
+ };
1101
+ /**
1010
1102
  * `cva`/`sva` return a function, so their definitions are compile-time declarations rather
1011
1103
  * than class-producing calls; once their uses are lowered, the factory calls are erased.
1012
1104
  * `token` also resolves to no class, but it does resolve to a literal, so it compiles through
@@ -1480,6 +1572,8 @@ const foldSource = (options) => {
1480
1572
  const helperModules = /* @__PURE__ */ new Map();
1481
1573
  /** Declaring modules a fold read, recorded as paths because their nodes do not persist. */
1482
1574
  const foreignDependencies = /* @__PURE__ */ new Set();
1575
+ /** Foreign recipe configs consumed, digested at read time for later verification. */
1576
+ const exportReads = [];
1483
1577
  /** Resolutions for this module's own call sites, keyed by the name the call site writes. */
1484
1578
  const importedRecipes = /* @__PURE__ */ new Map();
1485
1579
  /**
@@ -1520,6 +1614,12 @@ const foldSource = (options) => {
1520
1614
  const entry = foreign.configs.get(origin.name);
1521
1615
  if (!entry || entry === AMBIGUOUS) return void 0;
1522
1616
  foreignDependencies.add(origin.filePath);
1617
+ exportReads.push({
1618
+ kind: "recipe",
1619
+ file: origin.filePath,
1620
+ name: origin.name,
1621
+ digest: digestRecipeConfig(entry)
1622
+ });
1523
1623
  helperModules.set(name, foreign.cssSpecifier ? rebaseSpecifier(foreign.cssSpecifier, origin.filePath, consuming.getFilePath()) : void 0);
1524
1624
  return entry;
1525
1625
  };
@@ -2092,7 +2192,8 @@ const foldSource = (options) => {
2092
2192
  map: null,
2093
2193
  folded,
2094
2194
  skipped,
2095
- dependencies: []
2195
+ dependencies: [],
2196
+ exportReads: []
2096
2197
  };
2097
2198
  }
2098
2199
  const rewriteSourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
@@ -2101,7 +2202,8 @@ const foldSource = (options) => {
2101
2202
  map: null,
2102
2203
  folded,
2103
2204
  skipped,
2104
- dependencies: []
2205
+ dependencies: [],
2206
+ exportReads: []
2105
2207
  };
2106
2208
  const dependencyScan = createDependencyScan(rewriteSourceFile);
2107
2209
  candidates.sort((a, b) => a.start - b.start || b.end - a.end);
@@ -2408,7 +2510,8 @@ const foldSource = (options) => {
2408
2510
  map: null,
2409
2511
  folded,
2410
2512
  skipped,
2411
- dependencies: []
2513
+ dependencies: [],
2514
+ exportReads: []
2412
2515
  };
2413
2516
  return {
2414
2517
  code: magic.toString(),
@@ -2419,7 +2522,8 @@ const foldSource = (options) => {
2419
2522
  }),
2420
2523
  folded,
2421
2524
  skipped,
2422
- dependencies: [...dependencyScan.results, ...foreignDependencies]
2525
+ dependencies: [...dependencyScan.results, ...foreignDependencies],
2526
+ exportReads
2423
2527
  };
2424
2528
  };
2425
2529
  //#endregion
@@ -2485,3 +2589,4 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
2485
2589
  exports.createRuntimeCss = createRuntimeCss;
2486
2590
  exports.createStaticStyleSetCompiler = createStaticStyleSetCompiler;
2487
2591
  exports.foldSource = foldSource;
2592
+ exports.verifyExportReads = verifyExportReads;
@@ -1,4 +1,5 @@
1
1
  import { compact, createCssUncached, createMergeCss, memo, viewTransitionClassName } from "@bamboocss/shared";
2
+ import { createHash } from "node:crypto";
2
3
  import { dirname, relative, resolve } from "node:path";
3
4
  import MagicString from "magic-string";
4
5
  import { resolveTsPathPattern } from "@bamboocss/config/ts-path";
@@ -1005,6 +1006,97 @@ const createRuntimeToken = (ctx) => (path) => tokenValuesFor(ctx).get(path)?.var
1005
1006
  //#endregion
1006
1007
  //#region src/fold.ts
1007
1008
  /**
1009
+ * Decide whether an edit to `changedFile` moved anything a dependent's fold actually read.
1010
+ *
1011
+ * `'unchanged'` means every recorded read of the edited file re-digests to the same value —
1012
+ * the dependent's fold inputs did not move, so its output cannot have, and the re-fold can be
1013
+ * skipped outright. `'changed'` means a read definitely differs, which is the same verdict
1014
+ * the re-fold would reach after doing all the work. `'unknown'` is every other situation —
1015
+ * no read names the edited file (the relationship runs through a channel these records do
1016
+ * not witness, a barrel hop in a recipe binding walk, say), a digest could not be pinned
1017
+ * down on either side — and sends the caller to the full re-fold this replaced.
1018
+ *
1019
+ * `digestMemo` is the per-event cache: many dependents verify against the same edited file,
1020
+ * and each distinct `(kind, file, name)` needs digesting once, not once per dependent.
1021
+ */
1022
+ const verifyExportReads = (ctx, parseModule, reads, changedFile, digestMemo) => {
1023
+ const relevant = reads.filter((read) => read.file === changedFile);
1024
+ if (!relevant.length) return {
1025
+ verdict: "unknown",
1026
+ crossings: []
1027
+ };
1028
+ const crossings = /* @__PURE__ */ new Set();
1029
+ for (const read of relevant) {
1030
+ if (read.digest === void 0) return {
1031
+ verdict: "unknown",
1032
+ crossings: []
1033
+ };
1034
+ const key = `${read.kind}\u0000${read.file}\u0000${read.name}`;
1035
+ if (!digestMemo.has(key)) if (read.kind === "value") {
1036
+ const crossed = [];
1037
+ const digest = ctx.project?.digestExportRead?.(read.file, read.name, (path) => crossed.push(path));
1038
+ digestMemo.set(key, {
1039
+ digest,
1040
+ crossings: crossed
1041
+ });
1042
+ } else {
1043
+ const batched = `recipe-file\u0000${read.file}`;
1044
+ if (!digestMemo.has(batched)) {
1045
+ try {
1046
+ const result = parseModule(read.file);
1047
+ if (result) for (const [name, entry] of collectRecipeConfigs(result)) digestMemo.set(`recipe\u0000${read.file}\u0000${name}`, {
1048
+ digest: entry === AMBIGUOUS ? "bamboo:export-missing" : digestRecipeConfig(entry),
1049
+ crossings: []
1050
+ });
1051
+ } catch {}
1052
+ digestMemo.set(batched, {
1053
+ digest: "bamboo:batched",
1054
+ crossings: []
1055
+ });
1056
+ }
1057
+ if (!digestMemo.has(key)) digestMemo.set(key, {
1058
+ digest: digestRecipeReadNow(parseModule, read.file, read.name),
1059
+ crossings: []
1060
+ });
1061
+ }
1062
+ const entry = digestMemo.get(key);
1063
+ if (entry.digest === void 0) return {
1064
+ verdict: "unknown",
1065
+ crossings: []
1066
+ };
1067
+ if (entry.digest !== read.digest) return {
1068
+ verdict: "changed",
1069
+ crossings: []
1070
+ };
1071
+ for (const path of entry.crossings) crossings.add(path);
1072
+ }
1073
+ return {
1074
+ verdict: "unchanged",
1075
+ crossings: [...crossings]
1076
+ };
1077
+ };
1078
+ const digestRecipeReadNow = (parseModule, file, name) => {
1079
+ try {
1080
+ const result = parseModule(file);
1081
+ if (!result) return "bamboo:module-missing";
1082
+ const entry = collectRecipeConfigs(result).get(name);
1083
+ if (!entry || entry === AMBIGUOUS) return "bamboo:export-missing";
1084
+ return digestRecipeConfig(entry);
1085
+ } catch {
1086
+ return;
1087
+ }
1088
+ };
1089
+ /** The verification witness for a foreign recipe read: the config bytes, order preserved. */
1090
+ const digestRecipeConfig = (entry) => {
1091
+ try {
1092
+ const json = JSON.stringify(entry.config, (_key, value) => value === void 0 ? "bamboo:undefined" : value);
1093
+ if (json === void 0) return void 0;
1094
+ return createHash("sha256").update(json).digest("base64");
1095
+ } catch {
1096
+ return;
1097
+ }
1098
+ };
1099
+ /**
1008
1100
  * `cva`/`sva` return a function, so their definitions are compile-time declarations rather
1009
1101
  * than class-producing calls; once their uses are lowered, the factory calls are erased.
1010
1102
  * `token` also resolves to no class, but it does resolve to a literal, so it compiles through
@@ -1478,6 +1570,8 @@ const foldSource = (options) => {
1478
1570
  const helperModules = /* @__PURE__ */ new Map();
1479
1571
  /** Declaring modules a fold read, recorded as paths because their nodes do not persist. */
1480
1572
  const foreignDependencies = /* @__PURE__ */ new Set();
1573
+ /** Foreign recipe configs consumed, digested at read time for later verification. */
1574
+ const exportReads = [];
1481
1575
  /** Resolutions for this module's own call sites, keyed by the name the call site writes. */
1482
1576
  const importedRecipes = /* @__PURE__ */ new Map();
1483
1577
  /**
@@ -1518,6 +1612,12 @@ const foldSource = (options) => {
1518
1612
  const entry = foreign.configs.get(origin.name);
1519
1613
  if (!entry || entry === AMBIGUOUS) return void 0;
1520
1614
  foreignDependencies.add(origin.filePath);
1615
+ exportReads.push({
1616
+ kind: "recipe",
1617
+ file: origin.filePath,
1618
+ name: origin.name,
1619
+ digest: digestRecipeConfig(entry)
1620
+ });
1521
1621
  helperModules.set(name, foreign.cssSpecifier ? rebaseSpecifier(foreign.cssSpecifier, origin.filePath, consuming.getFilePath()) : void 0);
1522
1622
  return entry;
1523
1623
  };
@@ -2090,7 +2190,8 @@ const foldSource = (options) => {
2090
2190
  map: null,
2091
2191
  folded,
2092
2192
  skipped,
2093
- dependencies: []
2193
+ dependencies: [],
2194
+ exportReads: []
2094
2195
  };
2095
2196
  }
2096
2197
  const rewriteSourceFile = ownSourceFile ?? candidates[0]?.node.getSourceFile() ?? recipeDefinitions[0]?.call.getSourceFile();
@@ -2099,7 +2200,8 @@ const foldSource = (options) => {
2099
2200
  map: null,
2100
2201
  folded,
2101
2202
  skipped,
2102
- dependencies: []
2203
+ dependencies: [],
2204
+ exportReads: []
2103
2205
  };
2104
2206
  const dependencyScan = createDependencyScan(rewriteSourceFile);
2105
2207
  candidates.sort((a, b) => a.start - b.start || b.end - a.end);
@@ -2406,7 +2508,8 @@ const foldSource = (options) => {
2406
2508
  map: null,
2407
2509
  folded,
2408
2510
  skipped,
2409
- dependencies: []
2511
+ dependencies: [],
2512
+ exportReads: []
2410
2513
  };
2411
2514
  return {
2412
2515
  code: magic.toString(),
@@ -2417,7 +2520,8 @@ const foldSource = (options) => {
2417
2520
  }),
2418
2521
  folded,
2419
2522
  skipped,
2420
- dependencies: [...dependencyScan.results, ...foreignDependencies]
2523
+ dependencies: [...dependencyScan.results, ...foreignDependencies],
2524
+ exportReads
2421
2525
  };
2422
2526
  };
2423
2527
  //#endregion
@@ -2480,4 +2584,4 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
2480
2584
  };
2481
2585
  };
2482
2586
  //#endregion
2483
- export { createRuntimeCss, createStaticStyleSetCompiler, foldSource };
2587
+ export { createRuntimeCss, createStaticStyleSetCompiler, foldSource, verifyExportReads };
package/dist/index.cjs CHANGED
@@ -61,6 +61,7 @@ const createLazyCompilerState = (loadContext, loadFold) => createRetryableLazy(a
61
61
  return {
62
62
  context,
63
63
  foldSource: fold.foldSource,
64
+ verifyExportReads: fold.verifyExportReads,
64
65
  runtimeCss,
65
66
  styleCompiler
66
67
  };
@@ -234,6 +235,7 @@ const bamboocssCss = (options) => {
234
235
  });
235
236
  await builder.emit();
236
237
  builder.extract();
238
+ await new Promise((settle) => setImmediate(settle));
237
239
  if (builder.context?.config.polyfill) throw new Error("bamboocss: the cascade-layer polyfill is incompatible with compiled atomic styles. The polyfill removes the utility-layer boundary required for safe atom reachability and renaming.");
238
240
  if (builder.context) {
239
241
  session.utilityLayer = builder.context.config.layers?.utilities ?? "utilities";
@@ -414,7 +416,7 @@ const bamboocssCss = (options) => {
414
416
  session.cssLoaded = true;
415
417
  const generationAtStart = changeGeneration;
416
418
  if (command === "serve" && servedCss?.generation === generationAtStart) {
417
- if (this.addWatchFile) for (const file of extractedSourceFiles()) this.addWatchFile(file);
419
+ if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
418
420
  return servedCss.css;
419
421
  }
420
422
  let css;
@@ -431,7 +433,7 @@ const bamboocssCss = (options) => {
431
433
  generation: generationAtStart,
432
434
  css
433
435
  };
434
- if (this.addWatchFile) for (const file of extractedSourceFiles()) this.addWatchFile(file);
436
+ if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
435
437
  return css;
436
438
  },
437
439
  configureServer(devServer) {
@@ -750,6 +752,8 @@ const bamboocss = (options = {}) => {
750
752
  * build is memory a one-shot pass has no reason to spend.
751
753
  */
752
754
  const foldMemoByContent = /* @__PURE__ */ new Map();
755
+ /** Per-event digests of the edited file's read values, shared across every dependent. */
756
+ const verifyDigestMemo = /* @__PURE__ */ new Map();
753
757
  const foldMemoKey = (filePath, inputDigest) => `${filePath}\0${inputDigest}`;
754
758
  /**
755
759
  * The Project resolution walk `withResolutionClosure` runs, memoized per change event.
@@ -794,7 +798,8 @@ const bamboocss = (options = {}) => {
794
798
  transformedModulesThisRun: /* @__PURE__ */ new Set(),
795
799
  unchangedFolds: /* @__PURE__ */ new Map(),
796
800
  changedRun: 0,
797
- cssLoaded: false
801
+ cssLoaded: false,
802
+ exportReadsByModule: /* @__PURE__ */ new Map()
798
803
  });
799
804
  const cloneEnvironmentState = (state) => ({
800
805
  transformArtifactsByModule: new Map(state.transformArtifactsByModule),
@@ -806,7 +811,8 @@ const bamboocss = (options = {}) => {
806
811
  transformedModulesThisRun: new Set(state.transformedModulesThisRun),
807
812
  unchangedFolds: new Map(state.unchangedFolds),
808
813
  changedRun: state.changedRun,
809
- cssLoaded: state.cssLoaded
814
+ cssLoaded: state.cssLoaded,
815
+ exportReadsByModule: new Map(state.exportReadsByModule)
810
816
  });
811
817
  const environmentState = (context) => {
812
818
  const identity = environmentName(context);
@@ -1284,21 +1290,42 @@ const bamboocss = (options = {}) => {
1284
1290
  * that returns nothing, a fold that throws — because "changed" is what this path did before,
1285
1291
  * and a wrong "unchanged" is a stale class string in the browser.
1286
1292
  */
1287
- const foldOutputUnchanged = (state, dependent) => {
1293
+ const foldOutputUnchanged = (state, dependent, changedFile) => {
1288
1294
  const memoized = state.unchangedFolds.get(dependent);
1289
1295
  if (memoized !== void 0) return memoized;
1290
- const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent);
1296
+ const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent, changedFile);
1291
1297
  state.changedRun = unchanged ? 0 : state.changedRun + 1;
1292
1298
  state.unchangedFolds.set(dependent, unchanged);
1293
1299
  return unchanged;
1294
1300
  };
1295
- const refoldMatchesSignature = (state, dependent) => {
1301
+ const refoldMatchesSignature = (state, dependent, changedFile) => {
1296
1302
  const signature = state.foldSignatures.get(dependent);
1297
1303
  if (!signature || !ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return false;
1298
1304
  try {
1299
1305
  const code = (0, node_fs.readFileSync)(signature.path, "utf8");
1300
1306
  const inputDigest = digest(code);
1301
1307
  if (inputDigest !== signature.input) return false;
1308
+ /**
1309
+ * Try to answer from what the fold *read* before re-running it.
1310
+ *
1311
+ * The recorded reads carry the digest of every cross-file value and recipe config this
1312
+ * dependent's fold consumed. When each read of the edited file re-digests identically,
1313
+ * the fold's inputs did not move and its output cannot have — the whole re-fold below
1314
+ * is skipped, which is most of what an edit to a shared module used to cost. Any gap —
1315
+ * no read naming the edited file, an unverifiable digest, the verifier chunk not loaded
1316
+ * — falls through to the full re-fold, which is exactly the previous behavior. A
1317
+ * definite mismatch is equally final in the other direction: the re-fold would only
1318
+ * rediscover the change.
1319
+ */
1320
+ const reads = state.exportReadsByModule.get(dependent);
1321
+ if (reads?.length && verifyExportReadsImpl) {
1322
+ const { verdict, crossings } = verifyExportReadsImpl(ctx, (path) => ctx?.project.parseSourceFile(path), reads, normalizeFsPath(changedFile), verifyDigestMemo);
1323
+ if (verdict === "unchanged") {
1324
+ recordFoldDependencies(state, dependent, signature.path, [...state.dependenciesByModule.get(dependent) ?? [], ...crossings]);
1325
+ return true;
1326
+ }
1327
+ if (verdict === "changed") return false;
1328
+ }
1302
1329
  let raw;
1303
1330
  let parserDependencies;
1304
1331
  const memoKey = foldMemoKey(signature.path, inputDigest);
@@ -1327,6 +1354,7 @@ const bamboocss = (options = {}) => {
1327
1354
  foldMemoByContent.set(memoKey, {
1328
1355
  result: raw,
1329
1356
  parserDependencies,
1357
+ valueReads: parserResult.getExportReads?.() ?? [],
1330
1358
  reportedSurvivors: false
1331
1359
  });
1332
1360
  }
@@ -1386,7 +1414,7 @@ const bamboocss = (options = {}) => {
1386
1414
  * one can end in a page reload, which is the honest outcome: its compiled classes really did
1387
1415
  * change, and a reload is what Vite does with any update nothing accepts.
1388
1416
  */
1389
- const foldDependentModules = (state, file, modules, graph) => {
1417
+ const foldDependentModules = (state, file, modules, graph, verify = true) => {
1390
1418
  const dependents = state.dependentsByDependency.get(normalizeFsPath(file));
1391
1419
  if (!dependents?.size) return;
1392
1420
  const added = [];
@@ -1424,7 +1452,7 @@ const bamboocss = (options = {}) => {
1424
1452
  * the edited module for a runtime value is still reached by `propagateUpdate` exactly as
1425
1453
  * it would be with no plugin here at all — that direction was never this list's to decide.
1426
1454
  */
1427
- if (foldOutputUnchanged(state, dependent)) continue;
1455
+ if (verify && foldOutputUnchanged(state, dependent, file)) continue;
1428
1456
  const exact = graph.getModuleById?.(dependent);
1429
1457
  const dependentFile = state.filesByModule.get(dependent) ?? dependent;
1430
1458
  const candidates = exact ? [exact] : graph.getModulesByFile(normalizeFsPath(dependentFile)) ?? [];
@@ -1458,6 +1486,7 @@ const bamboocss = (options = {}) => {
1458
1486
  };
1459
1487
  let ctx;
1460
1488
  let foldSourceImpl;
1489
+ let verifyExportReadsImpl;
1461
1490
  let runtimeCss;
1462
1491
  let styleCompiler;
1463
1492
  let command = "build";
@@ -1511,6 +1540,7 @@ const bamboocss = (options = {}) => {
1511
1540
  const loaded = await loadCompilerState();
1512
1541
  ctx = loaded.context;
1513
1542
  foldSourceImpl = loaded.foldSource;
1543
+ verifyExportReadsImpl = loaded.verifyExportReads;
1514
1544
  runtimeCss = loaded.runtimeCss;
1515
1545
  styleCompiler = loaded.styleCompiler;
1516
1546
  };
@@ -1673,6 +1703,7 @@ const bamboocss = (options = {}) => {
1673
1703
  watchChange(id, change) {
1674
1704
  foldMemoByContent.clear();
1675
1705
  resolutionClosureMemo.clear();
1706
+ verifyDigestMemo.clear();
1676
1707
  for (const state of transformStateByEnvironment.values()) {
1677
1708
  state.unchangedFolds.clear();
1678
1709
  state.changedRun = 0;
@@ -1695,6 +1726,52 @@ const bamboocss = (options = {}) => {
1695
1726
  return;
1696
1727
  }
1697
1728
  ctx.project.reloadSourceFile(filePath);
1729
+ /**
1730
+ * Fold the edited file before the browser asks for it.
1731
+ *
1732
+ * The first transform after an edit is the one fold the memo cannot already hold — the
1733
+ * bytes are new — and it sits on the repaint path: the websocket round trip plus the
1734
+ * module refetch land ~15-30ms after this hook, and the fold costs ~5-13ms of that
1735
+ * budget on a route-sized module. Folding one macrotask later, after the update hooks
1736
+ * have run and the broadcast is out, has the memo hot before the request arrives.
1737
+ *
1738
+ * `setImmediate` is the load-bearing part: this hook is awaited before Vite announces
1739
+ * anything, so the work must not run inline. Content-keyed like every memo entry, so a
1740
+ * racing save cannot poison anything — the entry states what these exact bytes fold to,
1741
+ * and a later event's `watchChange` clears the memo before that event's transforms run.
1742
+ * Failures are swallowed here; the real transform runs the same fold and owns the
1743
+ * diagnostics.
1744
+ */
1745
+ if (command === "serve") setImmediate(() => {
1746
+ if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return;
1747
+ try {
1748
+ const code = (0, node_fs.readFileSync)(filePath, "utf8");
1749
+ const memoKey = foldMemoKey(filePath, digest(code));
1750
+ if (foldMemoByContent.has(memoKey)) return;
1751
+ const sourceFile = ctx.project.addSourceFile(filePath, code);
1752
+ const parserResult = ctx.project.parseSourceFile(filePath);
1753
+ if (!parserResult) return;
1754
+ const folded = foldSourceImpl({
1755
+ ctx,
1756
+ code,
1757
+ parserResult,
1758
+ filePath,
1759
+ runtimeCss,
1760
+ styleCompiler,
1761
+ maxRecipeStates,
1762
+ parseModule: (path) => ctx?.project.parseSourceFile(path),
1763
+ recipeConfigCache: transformStateByEnvironment.get("client")?.recipeConfigCache ?? /* @__PURE__ */ new Map(),
1764
+ reportSurvivors: true,
1765
+ sourceFile
1766
+ });
1767
+ foldMemoByContent.set(memoKey, {
1768
+ result: folded,
1769
+ parserDependencies: parserResult.getDependencies(),
1770
+ valueReads: parserResult.getExportReads?.() ?? [],
1771
+ reportedSurvivors: true
1772
+ });
1773
+ } catch {}
1774
+ });
1698
1775
  },
1699
1776
  /**
1700
1777
  * Re-transform whatever folded a value out of the file that just changed.
@@ -1718,7 +1795,22 @@ const bamboocss = (options = {}) => {
1718
1795
  hotUpdate({ file, modules }) {
1719
1796
  const graph = this.environment?.moduleGraph;
1720
1797
  if (!graph) return;
1721
- return foldDependentModules(environmentState(this), file, modules, graph);
1798
+ /**
1799
+ * The provisional re-folds exist to spare the *browser*: an announced client module is a
1800
+ * refetch round trip, and behind a framework that re-drives HMR per entry, a router
1801
+ * revalidation — that is what deciding "unchanged" before Vite is told anything buys.
1802
+ *
1803
+ * A server graph has none of that economy. Its modules are re-transformed by this same
1804
+ * process the next time something renders, nothing is announced by invalidating quietly,
1805
+ * and the verification runs on the awaited path *before* the client's update can be
1806
+ * broadcast — on a react-router app, re-folding every SSR consumer of a shared style
1807
+ * module added ~15ms to each edit's repaint for work whose only reader was the next
1808
+ * `.data` revalidation. Invalidate outright there and let the next render pay lazily,
1809
+ * off the repaint path. `verify` stays on when the consumer kind is unknown — a harness
1810
+ * without environment config keeps the conservative shape.
1811
+ */
1812
+ const consumer = this.environment?.config?.consumer;
1813
+ return foldDependentModules(environmentState(this), file, modules, graph, consumer !== "server");
1722
1814
  },
1723
1815
  handleHotUpdate({ file, modules, server }) {
1724
1816
  const legacy = server;
@@ -1744,8 +1836,11 @@ const bamboocss = (options = {}) => {
1744
1836
  try {
1745
1837
  const memoKey = command === "serve" ? foldMemoKey(filePath, inputDigest ??= digest(code)) : void 0;
1746
1838
  const memoized = memoKey ? foldMemoByContent.get(memoKey) : void 0;
1747
- if (memoized?.reportedSurvivors) result = withResolutionClosure(filePath, memoized.result, memoized.parserDependencies, previousDependencies);
1748
- else {
1839
+ let valueReads = [];
1840
+ if (memoized?.reportedSurvivors) {
1841
+ valueReads = memoized.valueReads;
1842
+ result = withResolutionClosure(filePath, memoized.result, memoized.parserDependencies, previousDependencies);
1843
+ } else {
1749
1844
  const sourceFile = ctx.project.addSourceFile(filePath, code);
1750
1845
  const parserResult = ctx.project.parseSourceFile(filePath);
1751
1846
  if (!parserResult) {
@@ -1768,13 +1863,19 @@ const bamboocss = (options = {}) => {
1768
1863
  sourceFile
1769
1864
  });
1770
1865
  const parserDependencies = parserResult.getDependencies();
1866
+ valueReads = parserResult.getExportReads?.() ?? [];
1771
1867
  if (memoKey) foldMemoByContent.set(memoKey, {
1772
1868
  result: folded,
1773
1869
  parserDependencies,
1870
+ valueReads,
1774
1871
  reportedSurvivors: true
1775
1872
  });
1776
1873
  result = withResolutionClosure(filePath, folded, parserDependencies, previousDependencies);
1777
1874
  }
1875
+ state.exportReadsByModule.set(id, [...valueReads.map((read) => ({
1876
+ kind: "value",
1877
+ ...read
1878
+ })), ...result.exportReads]);
1778
1879
  } catch (error) {
1779
1880
  _bamboocss_logger.logger.caughtError("vite:transform", `Failed to compile ${filePath}`, error);
1780
1881
  const previousDependencies = [...state.dependenciesByModule.get(id) ?? []];
package/dist/index.mjs CHANGED
@@ -56,6 +56,7 @@ const createLazyCompilerState = (loadContext, loadFold) => createRetryableLazy(a
56
56
  return {
57
57
  context,
58
58
  foldSource: fold.foldSource,
59
+ verifyExportReads: fold.verifyExportReads,
59
60
  runtimeCss,
60
61
  styleCompiler
61
62
  };
@@ -229,6 +230,7 @@ const bamboocssCss = (options) => {
229
230
  });
230
231
  await builder.emit();
231
232
  builder.extract();
233
+ await new Promise((settle) => setImmediate(settle));
232
234
  if (builder.context?.config.polyfill) throw new Error("bamboocss: the cascade-layer polyfill is incompatible with compiled atomic styles. The polyfill removes the utility-layer boundary required for safe atom reachability and renaming.");
233
235
  if (builder.context) {
234
236
  session.utilityLayer = builder.context.config.layers?.utilities ?? "utilities";
@@ -409,7 +411,7 @@ const bamboocssCss = (options) => {
409
411
  session.cssLoaded = true;
410
412
  const generationAtStart = changeGeneration;
411
413
  if (command === "serve" && servedCss?.generation === generationAtStart) {
412
- if (this.addWatchFile) for (const file of extractedSourceFiles()) this.addWatchFile(file);
414
+ if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
413
415
  return servedCss.css;
414
416
  }
415
417
  let css;
@@ -426,7 +428,7 @@ const bamboocssCss = (options) => {
426
428
  generation: generationAtStart,
427
429
  css
428
430
  };
429
- if (this.addWatchFile) for (const file of extractedSourceFiles()) this.addWatchFile(file);
431
+ if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
430
432
  return css;
431
433
  },
432
434
  configureServer(devServer) {
@@ -745,6 +747,8 @@ const bamboocss = (options = {}) => {
745
747
  * build is memory a one-shot pass has no reason to spend.
746
748
  */
747
749
  const foldMemoByContent = /* @__PURE__ */ new Map();
750
+ /** Per-event digests of the edited file's read values, shared across every dependent. */
751
+ const verifyDigestMemo = /* @__PURE__ */ new Map();
748
752
  const foldMemoKey = (filePath, inputDigest) => `${filePath}\0${inputDigest}`;
749
753
  /**
750
754
  * The Project resolution walk `withResolutionClosure` runs, memoized per change event.
@@ -789,7 +793,8 @@ const bamboocss = (options = {}) => {
789
793
  transformedModulesThisRun: /* @__PURE__ */ new Set(),
790
794
  unchangedFolds: /* @__PURE__ */ new Map(),
791
795
  changedRun: 0,
792
- cssLoaded: false
796
+ cssLoaded: false,
797
+ exportReadsByModule: /* @__PURE__ */ new Map()
793
798
  });
794
799
  const cloneEnvironmentState = (state) => ({
795
800
  transformArtifactsByModule: new Map(state.transformArtifactsByModule),
@@ -801,7 +806,8 @@ const bamboocss = (options = {}) => {
801
806
  transformedModulesThisRun: new Set(state.transformedModulesThisRun),
802
807
  unchangedFolds: new Map(state.unchangedFolds),
803
808
  changedRun: state.changedRun,
804
- cssLoaded: state.cssLoaded
809
+ cssLoaded: state.cssLoaded,
810
+ exportReadsByModule: new Map(state.exportReadsByModule)
805
811
  });
806
812
  const environmentState = (context) => {
807
813
  const identity = environmentName(context);
@@ -1279,21 +1285,42 @@ const bamboocss = (options = {}) => {
1279
1285
  * that returns nothing, a fold that throws — because "changed" is what this path did before,
1280
1286
  * and a wrong "unchanged" is a stale class string in the browser.
1281
1287
  */
1282
- const foldOutputUnchanged = (state, dependent) => {
1288
+ const foldOutputUnchanged = (state, dependent, changedFile) => {
1283
1289
  const memoized = state.unchangedFolds.get(dependent);
1284
1290
  if (memoized !== void 0) return memoized;
1285
- const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent);
1291
+ const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent, changedFile);
1286
1292
  state.changedRun = unchanged ? 0 : state.changedRun + 1;
1287
1293
  state.unchangedFolds.set(dependent, unchanged);
1288
1294
  return unchanged;
1289
1295
  };
1290
- const refoldMatchesSignature = (state, dependent) => {
1296
+ const refoldMatchesSignature = (state, dependent, changedFile) => {
1291
1297
  const signature = state.foldSignatures.get(dependent);
1292
1298
  if (!signature || !ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return false;
1293
1299
  try {
1294
1300
  const code = readFileSync(signature.path, "utf8");
1295
1301
  const inputDigest = digest(code);
1296
1302
  if (inputDigest !== signature.input) return false;
1303
+ /**
1304
+ * Try to answer from what the fold *read* before re-running it.
1305
+ *
1306
+ * The recorded reads carry the digest of every cross-file value and recipe config this
1307
+ * dependent's fold consumed. When each read of the edited file re-digests identically,
1308
+ * the fold's inputs did not move and its output cannot have — the whole re-fold below
1309
+ * is skipped, which is most of what an edit to a shared module used to cost. Any gap —
1310
+ * no read naming the edited file, an unverifiable digest, the verifier chunk not loaded
1311
+ * — falls through to the full re-fold, which is exactly the previous behavior. A
1312
+ * definite mismatch is equally final in the other direction: the re-fold would only
1313
+ * rediscover the change.
1314
+ */
1315
+ const reads = state.exportReadsByModule.get(dependent);
1316
+ if (reads?.length && verifyExportReadsImpl) {
1317
+ const { verdict, crossings } = verifyExportReadsImpl(ctx, (path) => ctx?.project.parseSourceFile(path), reads, normalizeFsPath(changedFile), verifyDigestMemo);
1318
+ if (verdict === "unchanged") {
1319
+ recordFoldDependencies(state, dependent, signature.path, [...state.dependenciesByModule.get(dependent) ?? [], ...crossings]);
1320
+ return true;
1321
+ }
1322
+ if (verdict === "changed") return false;
1323
+ }
1297
1324
  let raw;
1298
1325
  let parserDependencies;
1299
1326
  const memoKey = foldMemoKey(signature.path, inputDigest);
@@ -1322,6 +1349,7 @@ const bamboocss = (options = {}) => {
1322
1349
  foldMemoByContent.set(memoKey, {
1323
1350
  result: raw,
1324
1351
  parserDependencies,
1352
+ valueReads: parserResult.getExportReads?.() ?? [],
1325
1353
  reportedSurvivors: false
1326
1354
  });
1327
1355
  }
@@ -1381,7 +1409,7 @@ const bamboocss = (options = {}) => {
1381
1409
  * one can end in a page reload, which is the honest outcome: its compiled classes really did
1382
1410
  * change, and a reload is what Vite does with any update nothing accepts.
1383
1411
  */
1384
- const foldDependentModules = (state, file, modules, graph) => {
1412
+ const foldDependentModules = (state, file, modules, graph, verify = true) => {
1385
1413
  const dependents = state.dependentsByDependency.get(normalizeFsPath(file));
1386
1414
  if (!dependents?.size) return;
1387
1415
  const added = [];
@@ -1419,7 +1447,7 @@ const bamboocss = (options = {}) => {
1419
1447
  * the edited module for a runtime value is still reached by `propagateUpdate` exactly as
1420
1448
  * it would be with no plugin here at all — that direction was never this list's to decide.
1421
1449
  */
1422
- if (foldOutputUnchanged(state, dependent)) continue;
1450
+ if (verify && foldOutputUnchanged(state, dependent, file)) continue;
1423
1451
  const exact = graph.getModuleById?.(dependent);
1424
1452
  const dependentFile = state.filesByModule.get(dependent) ?? dependent;
1425
1453
  const candidates = exact ? [exact] : graph.getModulesByFile(normalizeFsPath(dependentFile)) ?? [];
@@ -1453,6 +1481,7 @@ const bamboocss = (options = {}) => {
1453
1481
  };
1454
1482
  let ctx;
1455
1483
  let foldSourceImpl;
1484
+ let verifyExportReadsImpl;
1456
1485
  let runtimeCss;
1457
1486
  let styleCompiler;
1458
1487
  let command = "build";
@@ -1506,6 +1535,7 @@ const bamboocss = (options = {}) => {
1506
1535
  const loaded = await loadCompilerState();
1507
1536
  ctx = loaded.context;
1508
1537
  foldSourceImpl = loaded.foldSource;
1538
+ verifyExportReadsImpl = loaded.verifyExportReads;
1509
1539
  runtimeCss = loaded.runtimeCss;
1510
1540
  styleCompiler = loaded.styleCompiler;
1511
1541
  };
@@ -1668,6 +1698,7 @@ const bamboocss = (options = {}) => {
1668
1698
  watchChange(id, change) {
1669
1699
  foldMemoByContent.clear();
1670
1700
  resolutionClosureMemo.clear();
1701
+ verifyDigestMemo.clear();
1671
1702
  for (const state of transformStateByEnvironment.values()) {
1672
1703
  state.unchangedFolds.clear();
1673
1704
  state.changedRun = 0;
@@ -1690,6 +1721,52 @@ const bamboocss = (options = {}) => {
1690
1721
  return;
1691
1722
  }
1692
1723
  ctx.project.reloadSourceFile(filePath);
1724
+ /**
1725
+ * Fold the edited file before the browser asks for it.
1726
+ *
1727
+ * The first transform after an edit is the one fold the memo cannot already hold — the
1728
+ * bytes are new — and it sits on the repaint path: the websocket round trip plus the
1729
+ * module refetch land ~15-30ms after this hook, and the fold costs ~5-13ms of that
1730
+ * budget on a route-sized module. Folding one macrotask later, after the update hooks
1731
+ * have run and the broadcast is out, has the memo hot before the request arrives.
1732
+ *
1733
+ * `setImmediate` is the load-bearing part: this hook is awaited before Vite announces
1734
+ * anything, so the work must not run inline. Content-keyed like every memo entry, so a
1735
+ * racing save cannot poison anything — the entry states what these exact bytes fold to,
1736
+ * and a later event's `watchChange` clears the memo before that event's transforms run.
1737
+ * Failures are swallowed here; the real transform runs the same fold and owns the
1738
+ * diagnostics.
1739
+ */
1740
+ if (command === "serve") setImmediate(() => {
1741
+ if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return;
1742
+ try {
1743
+ const code = readFileSync(filePath, "utf8");
1744
+ const memoKey = foldMemoKey(filePath, digest(code));
1745
+ if (foldMemoByContent.has(memoKey)) return;
1746
+ const sourceFile = ctx.project.addSourceFile(filePath, code);
1747
+ const parserResult = ctx.project.parseSourceFile(filePath);
1748
+ if (!parserResult) return;
1749
+ const folded = foldSourceImpl({
1750
+ ctx,
1751
+ code,
1752
+ parserResult,
1753
+ filePath,
1754
+ runtimeCss,
1755
+ styleCompiler,
1756
+ maxRecipeStates,
1757
+ parseModule: (path) => ctx?.project.parseSourceFile(path),
1758
+ recipeConfigCache: transformStateByEnvironment.get("client")?.recipeConfigCache ?? /* @__PURE__ */ new Map(),
1759
+ reportSurvivors: true,
1760
+ sourceFile
1761
+ });
1762
+ foldMemoByContent.set(memoKey, {
1763
+ result: folded,
1764
+ parserDependencies: parserResult.getDependencies(),
1765
+ valueReads: parserResult.getExportReads?.() ?? [],
1766
+ reportedSurvivors: true
1767
+ });
1768
+ } catch {}
1769
+ });
1693
1770
  },
1694
1771
  /**
1695
1772
  * Re-transform whatever folded a value out of the file that just changed.
@@ -1713,7 +1790,22 @@ const bamboocss = (options = {}) => {
1713
1790
  hotUpdate({ file, modules }) {
1714
1791
  const graph = this.environment?.moduleGraph;
1715
1792
  if (!graph) return;
1716
- return foldDependentModules(environmentState(this), file, modules, graph);
1793
+ /**
1794
+ * The provisional re-folds exist to spare the *browser*: an announced client module is a
1795
+ * refetch round trip, and behind a framework that re-drives HMR per entry, a router
1796
+ * revalidation — that is what deciding "unchanged" before Vite is told anything buys.
1797
+ *
1798
+ * A server graph has none of that economy. Its modules are re-transformed by this same
1799
+ * process the next time something renders, nothing is announced by invalidating quietly,
1800
+ * and the verification runs on the awaited path *before* the client's update can be
1801
+ * broadcast — on a react-router app, re-folding every SSR consumer of a shared style
1802
+ * module added ~15ms to each edit's repaint for work whose only reader was the next
1803
+ * `.data` revalidation. Invalidate outright there and let the next render pay lazily,
1804
+ * off the repaint path. `verify` stays on when the consumer kind is unknown — a harness
1805
+ * without environment config keeps the conservative shape.
1806
+ */
1807
+ const consumer = this.environment?.config?.consumer;
1808
+ return foldDependentModules(environmentState(this), file, modules, graph, consumer !== "server");
1717
1809
  },
1718
1810
  handleHotUpdate({ file, modules, server }) {
1719
1811
  const legacy = server;
@@ -1739,8 +1831,11 @@ const bamboocss = (options = {}) => {
1739
1831
  try {
1740
1832
  const memoKey = command === "serve" ? foldMemoKey(filePath, inputDigest ??= digest(code)) : void 0;
1741
1833
  const memoized = memoKey ? foldMemoByContent.get(memoKey) : void 0;
1742
- if (memoized?.reportedSurvivors) result = withResolutionClosure(filePath, memoized.result, memoized.parserDependencies, previousDependencies);
1743
- else {
1834
+ let valueReads = [];
1835
+ if (memoized?.reportedSurvivors) {
1836
+ valueReads = memoized.valueReads;
1837
+ result = withResolutionClosure(filePath, memoized.result, memoized.parserDependencies, previousDependencies);
1838
+ } else {
1744
1839
  const sourceFile = ctx.project.addSourceFile(filePath, code);
1745
1840
  const parserResult = ctx.project.parseSourceFile(filePath);
1746
1841
  if (!parserResult) {
@@ -1763,13 +1858,19 @@ const bamboocss = (options = {}) => {
1763
1858
  sourceFile
1764
1859
  });
1765
1860
  const parserDependencies = parserResult.getDependencies();
1861
+ valueReads = parserResult.getExportReads?.() ?? [];
1766
1862
  if (memoKey) foldMemoByContent.set(memoKey, {
1767
1863
  result: folded,
1768
1864
  parserDependencies,
1865
+ valueReads,
1769
1866
  reportedSurvivors: true
1770
1867
  });
1771
1868
  result = withResolutionClosure(filePath, folded, parserDependencies, previousDependencies);
1772
1869
  }
1870
+ state.exportReadsByModule.set(id, [...valueReads.map((read) => ({
1871
+ kind: "value",
1872
+ ...read
1873
+ })), ...result.exportReads]);
1773
1874
  } catch (error) {
1774
1875
  logger.caughtError("vite:transform", `Failed to compile ${filePath}`, error);
1775
1876
  const previousDependencies = [...state.dependenciesByModule.get(id) ?? []];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.46.1",
3
+ "version": "1.46.3",
4
4
  "description": "Vite integration for Bamboo CSS",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -42,18 +42,18 @@
42
42
  "postcss": "8.5.26",
43
43
  "postcss-selector-parser": "7.1.5",
44
44
  "ts-morph": "28.0.0",
45
- "@bamboocss/config": "1.46.1",
46
- "@bamboocss/core": "1.46.1",
47
- "@bamboocss/node": "1.46.1",
48
- "@bamboocss/shared": "1.46.1",
49
- "@bamboocss/extractor": "1.46.1",
50
- "@bamboocss/types": "1.46.1",
51
- "@bamboocss/logger": "1.46.1"
45
+ "@bamboocss/config": "1.46.3",
46
+ "@bamboocss/core": "1.46.3",
47
+ "@bamboocss/extractor": "1.46.3",
48
+ "@bamboocss/logger": "1.46.3",
49
+ "@bamboocss/node": "1.46.3",
50
+ "@bamboocss/shared": "1.46.3",
51
+ "@bamboocss/types": "1.46.3"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@jridgewell/trace-mapping": "^0.3.31",
55
55
  "vite": "7.2.6",
56
- "@bamboocss/fixture": "1.46.1"
56
+ "@bamboocss/fixture": "1.46.3"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "vite": ">=5"