@bamboocss/vite 1.46.1 → 1.46.2
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/fold-module.cjs +109 -4
- package/dist/fold-module.mjs +109 -5
- package/dist/index.cjs +113 -12
- package/dist/index.mjs +113 -12
- package/package.json +9 -9
package/dist/fold-module.cjs
CHANGED
|
@@ -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;
|
package/dist/fold-module.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1748
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1743
|
-
|
|
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.
|
|
3
|
+
"version": "1.46.2",
|
|
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.
|
|
46
|
-
"@bamboocss/core": "1.46.
|
|
47
|
-
"@bamboocss/
|
|
48
|
-
"@bamboocss/
|
|
49
|
-
"@bamboocss/
|
|
50
|
-
"@bamboocss/
|
|
51
|
-
"@bamboocss/
|
|
45
|
+
"@bamboocss/config": "1.46.2",
|
|
46
|
+
"@bamboocss/core": "1.46.2",
|
|
47
|
+
"@bamboocss/extractor": "1.46.2",
|
|
48
|
+
"@bamboocss/node": "1.46.2",
|
|
49
|
+
"@bamboocss/logger": "1.46.2",
|
|
50
|
+
"@bamboocss/shared": "1.46.2",
|
|
51
|
+
"@bamboocss/types": "1.46.2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
55
55
|
"vite": "7.2.6",
|
|
56
|
-
"@bamboocss/fixture": "1.46.
|
|
56
|
+
"@bamboocss/fixture": "1.46.2"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"vite": ">=5"
|