@bamboocss/vite 1.46.0 → 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 +252 -47
- package/dist/index.mjs +252 -47
- 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
|
};
|
|
@@ -208,6 +209,23 @@ const bamboocssCss = (options) => {
|
|
|
208
209
|
* stylesheet twice over.
|
|
209
210
|
*/
|
|
210
211
|
let pending;
|
|
212
|
+
/**
|
|
213
|
+
* Which change the current `pending` was generated for, and the validated sheet it produced.
|
|
214
|
+
*
|
|
215
|
+
* Every environment loads the virtual stylesheet — a react-router dev server loads it once
|
|
216
|
+
* for the client graph and once for SSR — and each load used to run a complete extraction
|
|
217
|
+
* and optimization pass to produce byte-identical CSS. The sheet is a function of the source
|
|
218
|
+
* files alone, and the watcher below is the single point every event that can reach it
|
|
219
|
+
* passes through — Vite's own propagation only arrives via the watch edges `load` registers,
|
|
220
|
+
* over the same extracted files the watcher checks. A monotonic counter bumped there is
|
|
221
|
+
* therefore enough to know whether a build already reflects the world a load is asking about.
|
|
222
|
+
*
|
|
223
|
+
* Dev only. A production build has no dev watcher to advance the counter, so serving the
|
|
224
|
+
* memo there would hand `vite build --watch` a stale sheet; builds regenerate per load.
|
|
225
|
+
*/
|
|
226
|
+
let changeGeneration = 0;
|
|
227
|
+
let pendingGeneration = -1;
|
|
228
|
+
let servedCss;
|
|
211
229
|
const build = async () => {
|
|
212
230
|
const builder = await ensureBuilder();
|
|
213
231
|
await builder.setup({
|
|
@@ -217,6 +235,7 @@ const bamboocssCss = (options) => {
|
|
|
217
235
|
});
|
|
218
236
|
await builder.emit();
|
|
219
237
|
builder.extract();
|
|
238
|
+
await new Promise((settle) => setImmediate(settle));
|
|
220
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.");
|
|
221
240
|
if (builder.context) {
|
|
222
241
|
session.utilityLayer = builder.context.config.layers?.utilities ?? "utilities";
|
|
@@ -245,6 +264,8 @@ const bamboocssCss = (options) => {
|
|
|
245
264
|
return css;
|
|
246
265
|
};
|
|
247
266
|
const generate = () => {
|
|
267
|
+
if (command === "serve" && pending && pendingGeneration === changeGeneration) return pending;
|
|
268
|
+
pendingGeneration = changeGeneration;
|
|
248
269
|
pending = Promise.resolve(pending).catch(() => void 0).then(build);
|
|
249
270
|
return pending;
|
|
250
271
|
};
|
|
@@ -393,6 +414,11 @@ const bamboocssCss = (options) => {
|
|
|
393
414
|
const query = queryOf(id);
|
|
394
415
|
if (id.slice(0, id.length - query.length) !== RESOLVED_ID) return null;
|
|
395
416
|
session.cssLoaded = true;
|
|
417
|
+
const generationAtStart = changeGeneration;
|
|
418
|
+
if (command === "serve" && servedCss?.generation === generationAtStart) {
|
|
419
|
+
if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
|
|
420
|
+
return servedCss.css;
|
|
421
|
+
}
|
|
396
422
|
let css;
|
|
397
423
|
try {
|
|
398
424
|
const validateDevCss = command === "serve" ? (await loadCssOutput()).pruneStaticCss : void 0;
|
|
@@ -403,7 +429,11 @@ const bamboocssCss = (options) => {
|
|
|
403
429
|
} catch (error) {
|
|
404
430
|
throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
|
|
405
431
|
}
|
|
406
|
-
if (
|
|
432
|
+
if (command === "serve" && generationAtStart === changeGeneration) servedCss = {
|
|
433
|
+
generation: generationAtStart,
|
|
434
|
+
css
|
|
435
|
+
};
|
|
436
|
+
if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
|
|
407
437
|
return css;
|
|
408
438
|
},
|
|
409
439
|
configureServer(devServer) {
|
|
@@ -427,6 +457,7 @@ const bamboocssCss = (options) => {
|
|
|
427
457
|
if (!ctx) return;
|
|
428
458
|
const absoluteFile = ctx.runtime.path.abs(ctx.config.cwd, file);
|
|
429
459
|
if (!session.extractedFiles.has(absoluteFile)) return;
|
|
460
|
+
changeGeneration++;
|
|
430
461
|
prebuilt = void 0;
|
|
431
462
|
const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
|
|
432
463
|
if (!mod) return;
|
|
@@ -694,6 +725,45 @@ const bamboocss = (options = {}) => {
|
|
|
694
725
|
return /* @__PURE__ */ new Error(`bamboocss: cached transform metadata for ${JSON.stringify(id)} in the ${JSON.stringify(environment)} environment ${problem}.\n\nBamboo cannot safely rebuild from this entry because cached JavaScript may still name CSS classes whose rules would be dropped. Restart Vite to invalidate its in-memory transform cache. If this persists, clear Vite's cache directory and rebuild.`);
|
|
695
726
|
};
|
|
696
727
|
const transformStateByEnvironment = /* @__PURE__ */ new Map();
|
|
728
|
+
/**
|
|
729
|
+
* One fold per file content per change event, shared across environments and hooks.
|
|
730
|
+
*
|
|
731
|
+
* A single edit folds the same bytes repeatedly: `hotUpdate` provisionally re-folds every
|
|
732
|
+
* dependent once per environment to decide what to invalidate, then `transform` folds the
|
|
733
|
+
* edited module for the client graph, again for SSR, and once more for each update a
|
|
734
|
+
* framework re-drives — react-router's server-change trigger calls `reloadModule` per pass.
|
|
735
|
+
* All of them read the same shared ts-morph project under the same config, so the result is
|
|
736
|
+
* a function of the bytes alone and the repeats were pure cost: on the app this was measured
|
|
737
|
+
* on, four transforms of a 46 kB route module per edit, ~10 ms each.
|
|
738
|
+
*
|
|
739
|
+
* `watchChange` clears it, which is the exact validity window: entries are correct until a
|
|
740
|
+
* file event changes what a fold could resolve, and `watchChange` is the one hook Vite calls
|
|
741
|
+
* for every such event before any update work begins. The per-environment resolution closure
|
|
742
|
+
* is deliberately not memoized — `withResolutionClosure` is recomputed per consumer against
|
|
743
|
+
* that environment's own recorded dependencies.
|
|
744
|
+
*
|
|
745
|
+
* Keyed by path *and* content digest, not path alone, because one physical file is served
|
|
746
|
+
* as more than one module shape in the same event — react-router clips a route module down
|
|
747
|
+
* to its route exports for the client graph while SSR gets the full file — and a last-write
|
|
748
|
+
* key would make the two shapes evict each other on every pass.
|
|
749
|
+
*
|
|
750
|
+
* Dev only, like the verdict memo it sits beside: a build transforms each module once per
|
|
751
|
+
* environment with no bracketing events, and holding every module's fold for the length of a
|
|
752
|
+
* build is memory a one-shot pass has no reason to spend.
|
|
753
|
+
*/
|
|
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();
|
|
757
|
+
const foldMemoKey = (filePath, inputDigest) => `${filePath}\0${inputDigest}`;
|
|
758
|
+
/**
|
|
759
|
+
* The Project resolution walk `withResolutionClosure` runs, memoized per change event.
|
|
760
|
+
*
|
|
761
|
+
* The walk is the dominant per-dependent cost once the fold itself is memoized — it runs
|
|
762
|
+
* once per dependent per environment with identical inputs, since both environments record
|
|
763
|
+
* the same fold dependencies for byte-identical source. Same bracketing as the fold memo:
|
|
764
|
+
* `watchChange` clears it, so no entry outlives the project state it was computed against.
|
|
765
|
+
*/
|
|
766
|
+
const resolutionClosureMemo = /* @__PURE__ */ new Map();
|
|
697
767
|
/** The immutable generation currently occupying each configured output on disk. */
|
|
698
768
|
const liveOutputSlotsByEnvironment = /* @__PURE__ */ new Map();
|
|
699
769
|
/** Graphs which passed `buildEnd`, but whose output has not succeeded yet. */
|
|
@@ -728,7 +798,8 @@ const bamboocss = (options = {}) => {
|
|
|
728
798
|
transformedModulesThisRun: /* @__PURE__ */ new Set(),
|
|
729
799
|
unchangedFolds: /* @__PURE__ */ new Map(),
|
|
730
800
|
changedRun: 0,
|
|
731
|
-
cssLoaded: false
|
|
801
|
+
cssLoaded: false,
|
|
802
|
+
exportReadsByModule: /* @__PURE__ */ new Map()
|
|
732
803
|
});
|
|
733
804
|
const cloneEnvironmentState = (state) => ({
|
|
734
805
|
transformArtifactsByModule: new Map(state.transformArtifactsByModule),
|
|
@@ -740,7 +811,8 @@ const bamboocss = (options = {}) => {
|
|
|
740
811
|
transformedModulesThisRun: new Set(state.transformedModulesThisRun),
|
|
741
812
|
unchangedFolds: new Map(state.unchangedFolds),
|
|
742
813
|
changedRun: state.changedRun,
|
|
743
|
-
cssLoaded: state.cssLoaded
|
|
814
|
+
cssLoaded: state.cssLoaded,
|
|
815
|
+
exportReadsByModule: new Map(state.exportReadsByModule)
|
|
744
816
|
});
|
|
745
817
|
const environmentState = (context) => {
|
|
746
818
|
const identity = environmentName(context);
|
|
@@ -1218,36 +1290,75 @@ const bamboocss = (options = {}) => {
|
|
|
1218
1290
|
* that returns nothing, a fold that throws — because "changed" is what this path did before,
|
|
1219
1291
|
* and a wrong "unchanged" is a stale class string in the browser.
|
|
1220
1292
|
*/
|
|
1221
|
-
const foldOutputUnchanged = (state, dependent) => {
|
|
1293
|
+
const foldOutputUnchanged = (state, dependent, changedFile) => {
|
|
1222
1294
|
const memoized = state.unchangedFolds.get(dependent);
|
|
1223
1295
|
if (memoized !== void 0) return memoized;
|
|
1224
|
-
const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent);
|
|
1296
|
+
const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent, changedFile);
|
|
1225
1297
|
state.changedRun = unchanged ? 0 : state.changedRun + 1;
|
|
1226
1298
|
state.unchangedFolds.set(dependent, unchanged);
|
|
1227
1299
|
return unchanged;
|
|
1228
1300
|
};
|
|
1229
|
-
const refoldMatchesSignature = (state, dependent) => {
|
|
1301
|
+
const refoldMatchesSignature = (state, dependent, changedFile) => {
|
|
1230
1302
|
const signature = state.foldSignatures.get(dependent);
|
|
1231
1303
|
if (!signature || !ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return false;
|
|
1232
1304
|
try {
|
|
1233
1305
|
const code = (0, node_fs.readFileSync)(signature.path, "utf8");
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1306
|
+
const inputDigest = digest(code);
|
|
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
|
+
}
|
|
1329
|
+
let raw;
|
|
1330
|
+
let parserDependencies;
|
|
1331
|
+
const memoKey = foldMemoKey(signature.path, inputDigest);
|
|
1332
|
+
const memoized = foldMemoByContent.get(memoKey);
|
|
1333
|
+
if (memoized) {
|
|
1334
|
+
raw = memoized.result;
|
|
1335
|
+
parserDependencies = memoized.parserDependencies;
|
|
1336
|
+
} else {
|
|
1337
|
+
const sourceFile = ctx.project.addSourceFile(signature.path, code);
|
|
1338
|
+
const parserResult = ctx.project.parseSourceFile(signature.path);
|
|
1339
|
+
if (!parserResult) return false;
|
|
1340
|
+
raw = foldSourceImpl({
|
|
1341
|
+
ctx,
|
|
1342
|
+
code,
|
|
1343
|
+
parserResult,
|
|
1344
|
+
filePath: signature.path,
|
|
1345
|
+
runtimeCss,
|
|
1346
|
+
styleCompiler,
|
|
1347
|
+
maxRecipeStates,
|
|
1348
|
+
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
1349
|
+
recipeConfigCache: state.recipeConfigCache,
|
|
1350
|
+
reportSurvivors: false,
|
|
1351
|
+
sourceFile
|
|
1352
|
+
});
|
|
1353
|
+
parserDependencies = parserResult.getDependencies();
|
|
1354
|
+
foldMemoByContent.set(memoKey, {
|
|
1355
|
+
result: raw,
|
|
1356
|
+
parserDependencies,
|
|
1357
|
+
valueReads: parserResult.getExportReads?.() ?? [],
|
|
1358
|
+
reportedSurvivors: false
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
const result = withResolutionClosure(signature.path, raw, parserDependencies, state.dependenciesByModule.get(dependent));
|
|
1251
1362
|
const unchanged = digest(result.code) === signature.output;
|
|
1252
1363
|
/**
|
|
1253
1364
|
* Edges re-recorded exactly on the way to suppressing a module. A changed provisional
|
|
@@ -1303,7 +1414,7 @@ const bamboocss = (options = {}) => {
|
|
|
1303
1414
|
* one can end in a page reload, which is the honest outcome: its compiled classes really did
|
|
1304
1415
|
* change, and a reload is what Vite does with any update nothing accepts.
|
|
1305
1416
|
*/
|
|
1306
|
-
const foldDependentModules = (state, file, modules, graph) => {
|
|
1417
|
+
const foldDependentModules = (state, file, modules, graph, verify = true) => {
|
|
1307
1418
|
const dependents = state.dependentsByDependency.get(normalizeFsPath(file));
|
|
1308
1419
|
if (!dependents?.size) return;
|
|
1309
1420
|
const added = [];
|
|
@@ -1341,7 +1452,7 @@ const bamboocss = (options = {}) => {
|
|
|
1341
1452
|
* the edited module for a runtime value is still reached by `propagateUpdate` exactly as
|
|
1342
1453
|
* it would be with no plugin here at all — that direction was never this list's to decide.
|
|
1343
1454
|
*/
|
|
1344
|
-
if (foldOutputUnchanged(state, dependent)) continue;
|
|
1455
|
+
if (verify && foldOutputUnchanged(state, dependent, file)) continue;
|
|
1345
1456
|
const exact = graph.getModuleById?.(dependent);
|
|
1346
1457
|
const dependentFile = state.filesByModule.get(dependent) ?? dependent;
|
|
1347
1458
|
const candidates = exact ? [exact] : graph.getModulesByFile(normalizeFsPath(dependentFile)) ?? [];
|
|
@@ -1375,6 +1486,7 @@ const bamboocss = (options = {}) => {
|
|
|
1375
1486
|
};
|
|
1376
1487
|
let ctx;
|
|
1377
1488
|
let foldSourceImpl;
|
|
1489
|
+
let verifyExportReadsImpl;
|
|
1378
1490
|
let runtimeCss;
|
|
1379
1491
|
let styleCompiler;
|
|
1380
1492
|
let command = "build";
|
|
@@ -1397,7 +1509,14 @@ const bamboocss = (options = {}) => {
|
|
|
1397
1509
|
const targets = new Set(dependencies);
|
|
1398
1510
|
for (const dependency of previousDependencies ?? []) targets.add(dependency);
|
|
1399
1511
|
if (!targets.size) return result;
|
|
1400
|
-
|
|
1512
|
+
const targetList = [...targets];
|
|
1513
|
+
const closureKey = command === "serve" ? `${filePath}\0${targetList.slice().sort().join("|")}` : void 0;
|
|
1514
|
+
let reachable = closureKey ? resolutionClosureMemo.get(closureKey) : void 0;
|
|
1515
|
+
if (!reachable) {
|
|
1516
|
+
reachable = ctx.project.getDependencies(filePath, targetList);
|
|
1517
|
+
if (closureKey) resolutionClosureMemo.set(closureKey, reachable);
|
|
1518
|
+
}
|
|
1519
|
+
for (const dependency of reachable) if (!isGeneratedOutput(dependency, ctx)) dependencies.add(dependency);
|
|
1401
1520
|
const expanded = [...dependencies];
|
|
1402
1521
|
if (expanded.length === result.dependencies.length && expanded.every((dependency, index) => dependency === result.dependencies[index])) return result;
|
|
1403
1522
|
return {
|
|
@@ -1421,6 +1540,7 @@ const bamboocss = (options = {}) => {
|
|
|
1421
1540
|
const loaded = await loadCompilerState();
|
|
1422
1541
|
ctx = loaded.context;
|
|
1423
1542
|
foldSourceImpl = loaded.foldSource;
|
|
1543
|
+
verifyExportReadsImpl = loaded.verifyExportReads;
|
|
1424
1544
|
runtimeCss = loaded.runtimeCss;
|
|
1425
1545
|
styleCompiler = loaded.styleCompiler;
|
|
1426
1546
|
};
|
|
@@ -1581,6 +1701,9 @@ const bamboocss = (options = {}) => {
|
|
|
1581
1701
|
* the parser still holds the file.
|
|
1582
1702
|
*/
|
|
1583
1703
|
watchChange(id, change) {
|
|
1704
|
+
foldMemoByContent.clear();
|
|
1705
|
+
resolutionClosureMemo.clear();
|
|
1706
|
+
verifyDigestMemo.clear();
|
|
1584
1707
|
for (const state of transformStateByEnvironment.values()) {
|
|
1585
1708
|
state.unchangedFolds.clear();
|
|
1586
1709
|
state.changedRun = 0;
|
|
@@ -1603,6 +1726,52 @@ const bamboocss = (options = {}) => {
|
|
|
1603
1726
|
return;
|
|
1604
1727
|
}
|
|
1605
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
|
+
});
|
|
1606
1775
|
},
|
|
1607
1776
|
/**
|
|
1608
1777
|
* Re-transform whatever folded a value out of the file that just changed.
|
|
@@ -1626,7 +1795,22 @@ const bamboocss = (options = {}) => {
|
|
|
1626
1795
|
hotUpdate({ file, modules }) {
|
|
1627
1796
|
const graph = this.environment?.moduleGraph;
|
|
1628
1797
|
if (!graph) return;
|
|
1629
|
-
|
|
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");
|
|
1630
1814
|
},
|
|
1631
1815
|
handleHotUpdate({ file, modules, server }) {
|
|
1632
1816
|
const legacy = server;
|
|
@@ -1650,27 +1834,48 @@ const bamboocss = (options = {}) => {
|
|
|
1650
1834
|
const previousDependencies = previousSignature && previousSignature.input === (inputDigest ??= digest(code)) ? state.dependenciesByModule.get(id) : void 0;
|
|
1651
1835
|
let result;
|
|
1652
1836
|
try {
|
|
1653
|
-
const
|
|
1654
|
-
const
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1837
|
+
const memoKey = command === "serve" ? foldMemoKey(filePath, inputDigest ??= digest(code)) : void 0;
|
|
1838
|
+
const memoized = memoKey ? foldMemoByContent.get(memoKey) : void 0;
|
|
1839
|
+
let valueReads = [];
|
|
1840
|
+
if (memoized?.reportedSurvivors) {
|
|
1841
|
+
valueReads = memoized.valueReads;
|
|
1842
|
+
result = withResolutionClosure(filePath, memoized.result, memoized.parserDependencies, previousDependencies);
|
|
1843
|
+
} else {
|
|
1844
|
+
const sourceFile = ctx.project.addSourceFile(filePath, code);
|
|
1845
|
+
const parserResult = ctx.project.parseSourceFile(filePath);
|
|
1846
|
+
if (!parserResult) {
|
|
1847
|
+
state.transformArtifactsByModule.delete(id);
|
|
1848
|
+
recordFoldDependencies(state, id, filePath, []);
|
|
1849
|
+
state.foldSignatures.delete(id);
|
|
1850
|
+
return null;
|
|
1851
|
+
}
|
|
1852
|
+
const folded = foldSourceImpl({
|
|
1853
|
+
ctx,
|
|
1854
|
+
code,
|
|
1855
|
+
parserResult,
|
|
1856
|
+
filePath,
|
|
1857
|
+
runtimeCss,
|
|
1858
|
+
styleCompiler,
|
|
1859
|
+
maxRecipeStates,
|
|
1860
|
+
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
1861
|
+
recipeConfigCache: state.recipeConfigCache,
|
|
1862
|
+
reportSurvivors: true,
|
|
1863
|
+
sourceFile
|
|
1864
|
+
});
|
|
1865
|
+
const parserDependencies = parserResult.getDependencies();
|
|
1866
|
+
valueReads = parserResult.getExportReads?.() ?? [];
|
|
1867
|
+
if (memoKey) foldMemoByContent.set(memoKey, {
|
|
1868
|
+
result: folded,
|
|
1869
|
+
parserDependencies,
|
|
1870
|
+
valueReads,
|
|
1871
|
+
reportedSurvivors: true
|
|
1872
|
+
});
|
|
1873
|
+
result = withResolutionClosure(filePath, folded, parserDependencies, previousDependencies);
|
|
1660
1874
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
filePath,
|
|
1666
|
-
runtimeCss,
|
|
1667
|
-
styleCompiler,
|
|
1668
|
-
maxRecipeStates,
|
|
1669
|
-
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
1670
|
-
recipeConfigCache: state.recipeConfigCache,
|
|
1671
|
-
reportSurvivors: true,
|
|
1672
|
-
sourceFile
|
|
1673
|
-
}), parserResult.getDependencies(), previousDependencies);
|
|
1875
|
+
state.exportReadsByModule.set(id, [...valueReads.map((read) => ({
|
|
1876
|
+
kind: "value",
|
|
1877
|
+
...read
|
|
1878
|
+
})), ...result.exportReads]);
|
|
1674
1879
|
} catch (error) {
|
|
1675
1880
|
_bamboocss_logger.logger.caughtError("vite:transform", `Failed to compile ${filePath}`, error);
|
|
1676
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
|
};
|
|
@@ -203,6 +204,23 @@ const bamboocssCss = (options) => {
|
|
|
203
204
|
* stylesheet twice over.
|
|
204
205
|
*/
|
|
205
206
|
let pending;
|
|
207
|
+
/**
|
|
208
|
+
* Which change the current `pending` was generated for, and the validated sheet it produced.
|
|
209
|
+
*
|
|
210
|
+
* Every environment loads the virtual stylesheet — a react-router dev server loads it once
|
|
211
|
+
* for the client graph and once for SSR — and each load used to run a complete extraction
|
|
212
|
+
* and optimization pass to produce byte-identical CSS. The sheet is a function of the source
|
|
213
|
+
* files alone, and the watcher below is the single point every event that can reach it
|
|
214
|
+
* passes through — Vite's own propagation only arrives via the watch edges `load` registers,
|
|
215
|
+
* over the same extracted files the watcher checks. A monotonic counter bumped there is
|
|
216
|
+
* therefore enough to know whether a build already reflects the world a load is asking about.
|
|
217
|
+
*
|
|
218
|
+
* Dev only. A production build has no dev watcher to advance the counter, so serving the
|
|
219
|
+
* memo there would hand `vite build --watch` a stale sheet; builds regenerate per load.
|
|
220
|
+
*/
|
|
221
|
+
let changeGeneration = 0;
|
|
222
|
+
let pendingGeneration = -1;
|
|
223
|
+
let servedCss;
|
|
206
224
|
const build = async () => {
|
|
207
225
|
const builder = await ensureBuilder();
|
|
208
226
|
await builder.setup({
|
|
@@ -212,6 +230,7 @@ const bamboocssCss = (options) => {
|
|
|
212
230
|
});
|
|
213
231
|
await builder.emit();
|
|
214
232
|
builder.extract();
|
|
233
|
+
await new Promise((settle) => setImmediate(settle));
|
|
215
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.");
|
|
216
235
|
if (builder.context) {
|
|
217
236
|
session.utilityLayer = builder.context.config.layers?.utilities ?? "utilities";
|
|
@@ -240,6 +259,8 @@ const bamboocssCss = (options) => {
|
|
|
240
259
|
return css;
|
|
241
260
|
};
|
|
242
261
|
const generate = () => {
|
|
262
|
+
if (command === "serve" && pending && pendingGeneration === changeGeneration) return pending;
|
|
263
|
+
pendingGeneration = changeGeneration;
|
|
243
264
|
pending = Promise.resolve(pending).catch(() => void 0).then(build);
|
|
244
265
|
return pending;
|
|
245
266
|
};
|
|
@@ -388,6 +409,11 @@ const bamboocssCss = (options) => {
|
|
|
388
409
|
const query = queryOf(id);
|
|
389
410
|
if (id.slice(0, id.length - query.length) !== RESOLVED_ID) return null;
|
|
390
411
|
session.cssLoaded = true;
|
|
412
|
+
const generationAtStart = changeGeneration;
|
|
413
|
+
if (command === "serve" && servedCss?.generation === generationAtStart) {
|
|
414
|
+
if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
|
|
415
|
+
return servedCss.css;
|
|
416
|
+
}
|
|
391
417
|
let css;
|
|
392
418
|
try {
|
|
393
419
|
const validateDevCss = command === "serve" ? (await loadCssOutput()).pruneStaticCss : void 0;
|
|
@@ -398,7 +424,11 @@ const bamboocssCss = (options) => {
|
|
|
398
424
|
} catch (error) {
|
|
399
425
|
throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
|
|
400
426
|
}
|
|
401
|
-
if (
|
|
427
|
+
if (command === "serve" && generationAtStart === changeGeneration) servedCss = {
|
|
428
|
+
generation: generationAtStart,
|
|
429
|
+
css
|
|
430
|
+
};
|
|
431
|
+
if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
|
|
402
432
|
return css;
|
|
403
433
|
},
|
|
404
434
|
configureServer(devServer) {
|
|
@@ -422,6 +452,7 @@ const bamboocssCss = (options) => {
|
|
|
422
452
|
if (!ctx) return;
|
|
423
453
|
const absoluteFile = ctx.runtime.path.abs(ctx.config.cwd, file);
|
|
424
454
|
if (!session.extractedFiles.has(absoluteFile)) return;
|
|
455
|
+
changeGeneration++;
|
|
425
456
|
prebuilt = void 0;
|
|
426
457
|
const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
|
|
427
458
|
if (!mod) return;
|
|
@@ -689,6 +720,45 @@ const bamboocss = (options = {}) => {
|
|
|
689
720
|
return /* @__PURE__ */ new Error(`bamboocss: cached transform metadata for ${JSON.stringify(id)} in the ${JSON.stringify(environment)} environment ${problem}.\n\nBamboo cannot safely rebuild from this entry because cached JavaScript may still name CSS classes whose rules would be dropped. Restart Vite to invalidate its in-memory transform cache. If this persists, clear Vite's cache directory and rebuild.`);
|
|
690
721
|
};
|
|
691
722
|
const transformStateByEnvironment = /* @__PURE__ */ new Map();
|
|
723
|
+
/**
|
|
724
|
+
* One fold per file content per change event, shared across environments and hooks.
|
|
725
|
+
*
|
|
726
|
+
* A single edit folds the same bytes repeatedly: `hotUpdate` provisionally re-folds every
|
|
727
|
+
* dependent once per environment to decide what to invalidate, then `transform` folds the
|
|
728
|
+
* edited module for the client graph, again for SSR, and once more for each update a
|
|
729
|
+
* framework re-drives — react-router's server-change trigger calls `reloadModule` per pass.
|
|
730
|
+
* All of them read the same shared ts-morph project under the same config, so the result is
|
|
731
|
+
* a function of the bytes alone and the repeats were pure cost: on the app this was measured
|
|
732
|
+
* on, four transforms of a 46 kB route module per edit, ~10 ms each.
|
|
733
|
+
*
|
|
734
|
+
* `watchChange` clears it, which is the exact validity window: entries are correct until a
|
|
735
|
+
* file event changes what a fold could resolve, and `watchChange` is the one hook Vite calls
|
|
736
|
+
* for every such event before any update work begins. The per-environment resolution closure
|
|
737
|
+
* is deliberately not memoized — `withResolutionClosure` is recomputed per consumer against
|
|
738
|
+
* that environment's own recorded dependencies.
|
|
739
|
+
*
|
|
740
|
+
* Keyed by path *and* content digest, not path alone, because one physical file is served
|
|
741
|
+
* as more than one module shape in the same event — react-router clips a route module down
|
|
742
|
+
* to its route exports for the client graph while SSR gets the full file — and a last-write
|
|
743
|
+
* key would make the two shapes evict each other on every pass.
|
|
744
|
+
*
|
|
745
|
+
* Dev only, like the verdict memo it sits beside: a build transforms each module once per
|
|
746
|
+
* environment with no bracketing events, and holding every module's fold for the length of a
|
|
747
|
+
* build is memory a one-shot pass has no reason to spend.
|
|
748
|
+
*/
|
|
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();
|
|
752
|
+
const foldMemoKey = (filePath, inputDigest) => `${filePath}\0${inputDigest}`;
|
|
753
|
+
/**
|
|
754
|
+
* The Project resolution walk `withResolutionClosure` runs, memoized per change event.
|
|
755
|
+
*
|
|
756
|
+
* The walk is the dominant per-dependent cost once the fold itself is memoized — it runs
|
|
757
|
+
* once per dependent per environment with identical inputs, since both environments record
|
|
758
|
+
* the same fold dependencies for byte-identical source. Same bracketing as the fold memo:
|
|
759
|
+
* `watchChange` clears it, so no entry outlives the project state it was computed against.
|
|
760
|
+
*/
|
|
761
|
+
const resolutionClosureMemo = /* @__PURE__ */ new Map();
|
|
692
762
|
/** The immutable generation currently occupying each configured output on disk. */
|
|
693
763
|
const liveOutputSlotsByEnvironment = /* @__PURE__ */ new Map();
|
|
694
764
|
/** Graphs which passed `buildEnd`, but whose output has not succeeded yet. */
|
|
@@ -723,7 +793,8 @@ const bamboocss = (options = {}) => {
|
|
|
723
793
|
transformedModulesThisRun: /* @__PURE__ */ new Set(),
|
|
724
794
|
unchangedFolds: /* @__PURE__ */ new Map(),
|
|
725
795
|
changedRun: 0,
|
|
726
|
-
cssLoaded: false
|
|
796
|
+
cssLoaded: false,
|
|
797
|
+
exportReadsByModule: /* @__PURE__ */ new Map()
|
|
727
798
|
});
|
|
728
799
|
const cloneEnvironmentState = (state) => ({
|
|
729
800
|
transformArtifactsByModule: new Map(state.transformArtifactsByModule),
|
|
@@ -735,7 +806,8 @@ const bamboocss = (options = {}) => {
|
|
|
735
806
|
transformedModulesThisRun: new Set(state.transformedModulesThisRun),
|
|
736
807
|
unchangedFolds: new Map(state.unchangedFolds),
|
|
737
808
|
changedRun: state.changedRun,
|
|
738
|
-
cssLoaded: state.cssLoaded
|
|
809
|
+
cssLoaded: state.cssLoaded,
|
|
810
|
+
exportReadsByModule: new Map(state.exportReadsByModule)
|
|
739
811
|
});
|
|
740
812
|
const environmentState = (context) => {
|
|
741
813
|
const identity = environmentName(context);
|
|
@@ -1213,36 +1285,75 @@ const bamboocss = (options = {}) => {
|
|
|
1213
1285
|
* that returns nothing, a fold that throws — because "changed" is what this path did before,
|
|
1214
1286
|
* and a wrong "unchanged" is a stale class string in the browser.
|
|
1215
1287
|
*/
|
|
1216
|
-
const foldOutputUnchanged = (state, dependent) => {
|
|
1288
|
+
const foldOutputUnchanged = (state, dependent, changedFile) => {
|
|
1217
1289
|
const memoized = state.unchangedFolds.get(dependent);
|
|
1218
1290
|
if (memoized !== void 0) return memoized;
|
|
1219
|
-
const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent);
|
|
1291
|
+
const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent, changedFile);
|
|
1220
1292
|
state.changedRun = unchanged ? 0 : state.changedRun + 1;
|
|
1221
1293
|
state.unchangedFolds.set(dependent, unchanged);
|
|
1222
1294
|
return unchanged;
|
|
1223
1295
|
};
|
|
1224
|
-
const refoldMatchesSignature = (state, dependent) => {
|
|
1296
|
+
const refoldMatchesSignature = (state, dependent, changedFile) => {
|
|
1225
1297
|
const signature = state.foldSignatures.get(dependent);
|
|
1226
1298
|
if (!signature || !ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return false;
|
|
1227
1299
|
try {
|
|
1228
1300
|
const code = readFileSync(signature.path, "utf8");
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1301
|
+
const inputDigest = digest(code);
|
|
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
|
+
}
|
|
1324
|
+
let raw;
|
|
1325
|
+
let parserDependencies;
|
|
1326
|
+
const memoKey = foldMemoKey(signature.path, inputDigest);
|
|
1327
|
+
const memoized = foldMemoByContent.get(memoKey);
|
|
1328
|
+
if (memoized) {
|
|
1329
|
+
raw = memoized.result;
|
|
1330
|
+
parserDependencies = memoized.parserDependencies;
|
|
1331
|
+
} else {
|
|
1332
|
+
const sourceFile = ctx.project.addSourceFile(signature.path, code);
|
|
1333
|
+
const parserResult = ctx.project.parseSourceFile(signature.path);
|
|
1334
|
+
if (!parserResult) return false;
|
|
1335
|
+
raw = foldSourceImpl({
|
|
1336
|
+
ctx,
|
|
1337
|
+
code,
|
|
1338
|
+
parserResult,
|
|
1339
|
+
filePath: signature.path,
|
|
1340
|
+
runtimeCss,
|
|
1341
|
+
styleCompiler,
|
|
1342
|
+
maxRecipeStates,
|
|
1343
|
+
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
1344
|
+
recipeConfigCache: state.recipeConfigCache,
|
|
1345
|
+
reportSurvivors: false,
|
|
1346
|
+
sourceFile
|
|
1347
|
+
});
|
|
1348
|
+
parserDependencies = parserResult.getDependencies();
|
|
1349
|
+
foldMemoByContent.set(memoKey, {
|
|
1350
|
+
result: raw,
|
|
1351
|
+
parserDependencies,
|
|
1352
|
+
valueReads: parserResult.getExportReads?.() ?? [],
|
|
1353
|
+
reportedSurvivors: false
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
const result = withResolutionClosure(signature.path, raw, parserDependencies, state.dependenciesByModule.get(dependent));
|
|
1246
1357
|
const unchanged = digest(result.code) === signature.output;
|
|
1247
1358
|
/**
|
|
1248
1359
|
* Edges re-recorded exactly on the way to suppressing a module. A changed provisional
|
|
@@ -1298,7 +1409,7 @@ const bamboocss = (options = {}) => {
|
|
|
1298
1409
|
* one can end in a page reload, which is the honest outcome: its compiled classes really did
|
|
1299
1410
|
* change, and a reload is what Vite does with any update nothing accepts.
|
|
1300
1411
|
*/
|
|
1301
|
-
const foldDependentModules = (state, file, modules, graph) => {
|
|
1412
|
+
const foldDependentModules = (state, file, modules, graph, verify = true) => {
|
|
1302
1413
|
const dependents = state.dependentsByDependency.get(normalizeFsPath(file));
|
|
1303
1414
|
if (!dependents?.size) return;
|
|
1304
1415
|
const added = [];
|
|
@@ -1336,7 +1447,7 @@ const bamboocss = (options = {}) => {
|
|
|
1336
1447
|
* the edited module for a runtime value is still reached by `propagateUpdate` exactly as
|
|
1337
1448
|
* it would be with no plugin here at all — that direction was never this list's to decide.
|
|
1338
1449
|
*/
|
|
1339
|
-
if (foldOutputUnchanged(state, dependent)) continue;
|
|
1450
|
+
if (verify && foldOutputUnchanged(state, dependent, file)) continue;
|
|
1340
1451
|
const exact = graph.getModuleById?.(dependent);
|
|
1341
1452
|
const dependentFile = state.filesByModule.get(dependent) ?? dependent;
|
|
1342
1453
|
const candidates = exact ? [exact] : graph.getModulesByFile(normalizeFsPath(dependentFile)) ?? [];
|
|
@@ -1370,6 +1481,7 @@ const bamboocss = (options = {}) => {
|
|
|
1370
1481
|
};
|
|
1371
1482
|
let ctx;
|
|
1372
1483
|
let foldSourceImpl;
|
|
1484
|
+
let verifyExportReadsImpl;
|
|
1373
1485
|
let runtimeCss;
|
|
1374
1486
|
let styleCompiler;
|
|
1375
1487
|
let command = "build";
|
|
@@ -1392,7 +1504,14 @@ const bamboocss = (options = {}) => {
|
|
|
1392
1504
|
const targets = new Set(dependencies);
|
|
1393
1505
|
for (const dependency of previousDependencies ?? []) targets.add(dependency);
|
|
1394
1506
|
if (!targets.size) return result;
|
|
1395
|
-
|
|
1507
|
+
const targetList = [...targets];
|
|
1508
|
+
const closureKey = command === "serve" ? `${filePath}\0${targetList.slice().sort().join("|")}` : void 0;
|
|
1509
|
+
let reachable = closureKey ? resolutionClosureMemo.get(closureKey) : void 0;
|
|
1510
|
+
if (!reachable) {
|
|
1511
|
+
reachable = ctx.project.getDependencies(filePath, targetList);
|
|
1512
|
+
if (closureKey) resolutionClosureMemo.set(closureKey, reachable);
|
|
1513
|
+
}
|
|
1514
|
+
for (const dependency of reachable) if (!isGeneratedOutput(dependency, ctx)) dependencies.add(dependency);
|
|
1396
1515
|
const expanded = [...dependencies];
|
|
1397
1516
|
if (expanded.length === result.dependencies.length && expanded.every((dependency, index) => dependency === result.dependencies[index])) return result;
|
|
1398
1517
|
return {
|
|
@@ -1416,6 +1535,7 @@ const bamboocss = (options = {}) => {
|
|
|
1416
1535
|
const loaded = await loadCompilerState();
|
|
1417
1536
|
ctx = loaded.context;
|
|
1418
1537
|
foldSourceImpl = loaded.foldSource;
|
|
1538
|
+
verifyExportReadsImpl = loaded.verifyExportReads;
|
|
1419
1539
|
runtimeCss = loaded.runtimeCss;
|
|
1420
1540
|
styleCompiler = loaded.styleCompiler;
|
|
1421
1541
|
};
|
|
@@ -1576,6 +1696,9 @@ const bamboocss = (options = {}) => {
|
|
|
1576
1696
|
* the parser still holds the file.
|
|
1577
1697
|
*/
|
|
1578
1698
|
watchChange(id, change) {
|
|
1699
|
+
foldMemoByContent.clear();
|
|
1700
|
+
resolutionClosureMemo.clear();
|
|
1701
|
+
verifyDigestMemo.clear();
|
|
1579
1702
|
for (const state of transformStateByEnvironment.values()) {
|
|
1580
1703
|
state.unchangedFolds.clear();
|
|
1581
1704
|
state.changedRun = 0;
|
|
@@ -1598,6 +1721,52 @@ const bamboocss = (options = {}) => {
|
|
|
1598
1721
|
return;
|
|
1599
1722
|
}
|
|
1600
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
|
+
});
|
|
1601
1770
|
},
|
|
1602
1771
|
/**
|
|
1603
1772
|
* Re-transform whatever folded a value out of the file that just changed.
|
|
@@ -1621,7 +1790,22 @@ const bamboocss = (options = {}) => {
|
|
|
1621
1790
|
hotUpdate({ file, modules }) {
|
|
1622
1791
|
const graph = this.environment?.moduleGraph;
|
|
1623
1792
|
if (!graph) return;
|
|
1624
|
-
|
|
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");
|
|
1625
1809
|
},
|
|
1626
1810
|
handleHotUpdate({ file, modules, server }) {
|
|
1627
1811
|
const legacy = server;
|
|
@@ -1645,27 +1829,48 @@ const bamboocss = (options = {}) => {
|
|
|
1645
1829
|
const previousDependencies = previousSignature && previousSignature.input === (inputDigest ??= digest(code)) ? state.dependenciesByModule.get(id) : void 0;
|
|
1646
1830
|
let result;
|
|
1647
1831
|
try {
|
|
1648
|
-
const
|
|
1649
|
-
const
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1832
|
+
const memoKey = command === "serve" ? foldMemoKey(filePath, inputDigest ??= digest(code)) : void 0;
|
|
1833
|
+
const memoized = memoKey ? foldMemoByContent.get(memoKey) : void 0;
|
|
1834
|
+
let valueReads = [];
|
|
1835
|
+
if (memoized?.reportedSurvivors) {
|
|
1836
|
+
valueReads = memoized.valueReads;
|
|
1837
|
+
result = withResolutionClosure(filePath, memoized.result, memoized.parserDependencies, previousDependencies);
|
|
1838
|
+
} else {
|
|
1839
|
+
const sourceFile = ctx.project.addSourceFile(filePath, code);
|
|
1840
|
+
const parserResult = ctx.project.parseSourceFile(filePath);
|
|
1841
|
+
if (!parserResult) {
|
|
1842
|
+
state.transformArtifactsByModule.delete(id);
|
|
1843
|
+
recordFoldDependencies(state, id, filePath, []);
|
|
1844
|
+
state.foldSignatures.delete(id);
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
const folded = foldSourceImpl({
|
|
1848
|
+
ctx,
|
|
1849
|
+
code,
|
|
1850
|
+
parserResult,
|
|
1851
|
+
filePath,
|
|
1852
|
+
runtimeCss,
|
|
1853
|
+
styleCompiler,
|
|
1854
|
+
maxRecipeStates,
|
|
1855
|
+
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
1856
|
+
recipeConfigCache: state.recipeConfigCache,
|
|
1857
|
+
reportSurvivors: true,
|
|
1858
|
+
sourceFile
|
|
1859
|
+
});
|
|
1860
|
+
const parserDependencies = parserResult.getDependencies();
|
|
1861
|
+
valueReads = parserResult.getExportReads?.() ?? [];
|
|
1862
|
+
if (memoKey) foldMemoByContent.set(memoKey, {
|
|
1863
|
+
result: folded,
|
|
1864
|
+
parserDependencies,
|
|
1865
|
+
valueReads,
|
|
1866
|
+
reportedSurvivors: true
|
|
1867
|
+
});
|
|
1868
|
+
result = withResolutionClosure(filePath, folded, parserDependencies, previousDependencies);
|
|
1655
1869
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
filePath,
|
|
1661
|
-
runtimeCss,
|
|
1662
|
-
styleCompiler,
|
|
1663
|
-
maxRecipeStates,
|
|
1664
|
-
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
1665
|
-
recipeConfigCache: state.recipeConfigCache,
|
|
1666
|
-
reportSurvivors: true,
|
|
1667
|
-
sourceFile
|
|
1668
|
-
}), parserResult.getDependencies(), previousDependencies);
|
|
1870
|
+
state.exportReadsByModule.set(id, [...valueReads.map((read) => ({
|
|
1871
|
+
kind: "value",
|
|
1872
|
+
...read
|
|
1873
|
+
})), ...result.exportReads]);
|
|
1669
1874
|
} catch (error) {
|
|
1670
1875
|
logger.caughtError("vite:transform", `Failed to compile ${filePath}`, error);
|
|
1671
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/extractor": "1.46.
|
|
48
|
-
"@bamboocss/
|
|
49
|
-
"@bamboocss/
|
|
50
|
-
"@bamboocss/shared": "1.46.
|
|
51
|
-
"@bamboocss/types": "1.46.
|
|
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"
|