@bamboocss/vite 1.47.0 → 1.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -48,24 +48,156 @@ const loadCssOutputModule = createLazyCssOutputModule();
48
48
  const createLazyFoldModule = (loadFold = () => Promise.resolve().then(() => require("./fold-module.cjs"))) => createRetryableLazy(loadFold);
49
49
  /** One process-wide fold-module load shared by every plugin instance and Vite environment. */
50
50
  const loadFoldModule = createLazyFoldModule();
51
- /** One Builder per CSS plugin instance, created only when a hook first needs it. */
51
+ /**
52
+ * The one Builder a run compiles against, created only when a hook first needs it.
53
+ *
54
+ * Per host rather than per plugin instance: the compiler and the stylesheet share it now.
55
+ * @see `createCompilationHost`
56
+ */
52
57
  const createLazyBuilder = (loadNode = loadNodeModule) => createRetryableLazy(async () => {
53
58
  const { Builder } = await loadNode();
54
59
  return new Builder();
55
60
  });
56
- /** Build and publish one complete fold state; no caller can observe partial initialization. */
57
- const createLazyCompilerState = (loadContext, loadFold) => createRetryableLazy(async () => {
58
- const [context, fold] = await Promise.all([loadContext(), loadFold()]);
59
- const runtimeCss = fold.createRuntimeCss(context);
60
- const styleCompiler = fold.createStaticStyleSetCompiler(context, runtimeCss);
61
+ //#endregion
62
+ //#region src/compilation-host.ts
63
+ const createCompilationHost = (options = {}) => {
64
+ const { configPath, cwd } = options;
65
+ const loadBuilder = options.loadBuilder ?? createLazyBuilder();
66
+ let command = "build";
67
+ let builder;
68
+ let generation;
69
+ let nextGenerationId = 0;
70
+ /**
71
+ * The setup covering the pass currently open.
72
+ *
73
+ * A cold start reaches this twice — the compiler's `pre` `buildStart`, then the CSS
74
+ * plugin's — for one instant in which nothing can have changed on disk. Sharing one attempt
75
+ * across both is what keeps a project from loading and evaluating its config twice per
76
+ * build. Cleared once a stylesheet pass consumes it, and on any source mutation, so no
77
+ * later pass can be answered by a setup taken before an edit.
78
+ */
79
+ let openSetup;
80
+ let openSetupStale = false;
81
+ let cssPass;
82
+ let changedSourceFiles = /* @__PURE__ */ new Set();
83
+ let needsInventoryScan = false;
84
+ let needsConfigReload = false;
85
+ const recordSourceChange = (filePath, event, options) => {
86
+ changedSourceFiles.add(filePath);
87
+ if (event !== "update") needsInventoryScan = true;
88
+ needsConfigReload ||= options?.needsConfigReload === true;
89
+ openSetupStale = true;
90
+ };
91
+ const takeSourceChanges = () => {
92
+ const changes = {
93
+ files: [...changedSourceFiles].sort(),
94
+ needsInventoryScan,
95
+ ...needsConfigReload ? { needsConfigReload: true } : {}
96
+ };
97
+ changedSourceFiles = /* @__PURE__ */ new Set();
98
+ needsInventoryScan = false;
99
+ needsConfigReload = false;
100
+ return changes;
101
+ };
102
+ const restoreSourceChanges = (changes) => {
103
+ for (const file of changes.files) changedSourceFiles.add(file);
104
+ needsInventoryScan ||= changes.needsInventoryScan === true;
105
+ needsConfigReload ||= changes.needsConfigReload === true;
106
+ };
107
+ const settled = async (attempt) => {
108
+ try {
109
+ await attempt;
110
+ } catch {}
111
+ };
112
+ const publish = () => {
113
+ const context = builder.getContextOrThrow();
114
+ if (generation?.context !== context) generation = {
115
+ id: ++nextGenerationId,
116
+ context,
117
+ encoder: context.encoder.clone()
118
+ };
119
+ return generation;
120
+ };
121
+ const runSetup = async () => {
122
+ builder ??= await loadBuilder();
123
+ const sourceChanges = takeSourceChanges();
124
+ try {
125
+ await builder.setup({
126
+ configPath,
127
+ cwd,
128
+ dev: command === "serve",
129
+ ...command === "serve" ? { sourceChanges } : {}
130
+ });
131
+ return publish();
132
+ } catch (error) {
133
+ if (command === "serve") restoreSourceChanges(sourceChanges);
134
+ throw error;
135
+ }
136
+ };
137
+ /**
138
+ * The setup covering the pass currently open, started at most once.
139
+ *
140
+ * Started through a resolved promise, so a synchronous throw becomes the same
141
+ * rejected-attempt contract a failed module load has and a later hook can retry it. A
142
+ * source mutation observed while one is in flight does not cancel it — two overlapping
143
+ * `Builder.setup` calls would interleave their change detection — it queues a fresh one
144
+ * behind it.
145
+ */
146
+ const setupOnce = () => {
147
+ const previous = openSetup;
148
+ if (previous && !openSetupStale) return previous;
149
+ openSetupStale = false;
150
+ const attempt = previous ? settled(previous).then(runSetup) : Promise.resolve().then(runSetup);
151
+ openSetup = attempt;
152
+ attempt.catch(() => {
153
+ if (openSetup === attempt) openSetup = void 0;
154
+ });
155
+ return attempt;
156
+ };
61
157
  return {
62
- context,
63
- foldSource: fold.foldSource,
64
- verifyExportReads: fold.verifyExportReads,
65
- runtimeCss,
66
- styleCompiler
158
+ setCommand(next) {
159
+ command = next;
160
+ },
161
+ current: () => generation,
162
+ async ensureGeneration() {
163
+ if (cssPass) await settled(cssPass);
164
+ if (generation) return Promise.resolve(generation);
165
+ return setupOnce();
166
+ },
167
+ isCssPassActive: () => cssPass !== void 0,
168
+ async runCssPass(run) {
169
+ while (cssPass) await settled(cssPass);
170
+ let release;
171
+ cssPass = new Promise((resolve) => {
172
+ release = resolve;
173
+ });
174
+ try {
175
+ const passGeneration = await setupOnce();
176
+ return await run(builder, passGeneration);
177
+ } finally {
178
+ openSetup = void 0;
179
+ openSetupStale = false;
180
+ cssPass = void 0;
181
+ release();
182
+ }
183
+ },
184
+ async runCompilerWork(run) {
185
+ while (cssPass) await settled(cssPass);
186
+ return run();
187
+ },
188
+ noteSourceChange(filePath, event, options) {
189
+ if (command === "serve") recordSourceChange(filePath, event, options);
190
+ },
191
+ reloadSource(filePath) {
192
+ recordSourceChange(filePath, "update");
193
+ builder?.reloadSource(filePath);
194
+ },
195
+ removeSource(filePath) {
196
+ recordSourceChange(filePath, "update");
197
+ builder?.removeSource(filePath);
198
+ }
67
199
  };
68
- });
200
+ };
69
201
  //#endregion
70
202
  //#region src/static-session.ts
71
203
  const createStaticCompilationSession = () => {
@@ -184,24 +316,26 @@ const asError = (error, context) => error instanceof Error ? error : new Error(`
184
316
  * process just wrote, which is a race on any watch rebuild.
185
317
  */
186
318
  const bamboocssCss = (options) => {
187
- const { configPath, cwd, loadCssOutput = loadCssOutputModule, session, pruneCss = true } = options;
319
+ const { configPath, cwd, loadCssOutput = loadCssOutputModule, session, host = createCompilationHost({
320
+ configPath,
321
+ cwd
322
+ }), pruneCss = true } = options;
188
323
  let builder;
189
- const loadBuilder = createLazyBuilder();
190
- const ensureBuilder = async () => {
191
- const loaded = await loadBuilder();
192
- builder = loaded;
193
- return loaded;
194
- };
195
324
  let server;
196
325
  let command = "build";
197
326
  /** The run's own `build` options, for a bundler with no per-environment config. */
198
327
  let ssrBuildOptions;
199
- /** Included owners plus local modules their extraction actually resolved and read. */
328
+ /** Every source, resolver input and expanded config dependency which can change the sheet. */
200
329
  const extractedSourceFiles = () => {
201
330
  const activeBuilder = builder;
202
331
  const context = activeBuilder?.context;
203
332
  if (!context) return [];
204
- return [...new Set([...context.getFiles(), ...activeBuilder.getResolutionReadFiles()].map((file) => context.runtime.path.abs(context.config.cwd, file)))];
333
+ return [...new Set([
334
+ ...activeBuilder.getSourceFiles(),
335
+ ...activeBuilder.getResolutionReadFiles(),
336
+ ...activeBuilder.getResolutionConfigurationFiles(),
337
+ ...context.explicitDeps
338
+ ].map((file) => context.runtime.path.abs(context.config.cwd, file)))];
205
339
  };
206
340
  /**
207
341
  * Serialised, because both `load` and the watcher can reach it and `Builder` keeps one
@@ -226,34 +360,36 @@ const bamboocssCss = (options) => {
226
360
  let changeGeneration = 0;
227
361
  let pendingGeneration = -1;
228
362
  let servedCss;
229
- const build = async () => {
230
- const builder = await ensureBuilder();
231
- await builder.setup({
232
- configPath,
233
- cwd,
234
- dev: command === "serve"
235
- });
236
- await builder.emit();
237
- builder.extract();
363
+ /**
364
+ * Held by the host for its whole length, rather than only around each mutation.
365
+ *
366
+ * Extraction fills the encoder this sheet is emitted from and `toCss` reads it back, with a
367
+ * deliberate macrotask between them. The compiler shares the AST both halves run against,
368
+ * so a transform folding a module in that window would re-prepare a source the extraction
369
+ * pass has already read and `toCss` has not finished reporting on. The host makes compiler
370
+ * work wait instead; a fold is a few milliseconds and this is the one place correctness
371
+ * depends on it.
372
+ */
373
+ const build = () => host.runCssPass(async (activeBuilder) => {
374
+ builder = activeBuilder;
375
+ await activeBuilder.emit();
376
+ activeBuilder.extract();
238
377
  await new Promise((settle) => setImmediate(settle));
239
- if (builder.context) {
240
- session.utilityLayer = builder.context.config.layers?.utilities ?? "utilities";
378
+ if (activeBuilder.context) {
379
+ session.utilityLayer = activeBuilder.context.config.layers?.utilities ?? "utilities";
241
380
  session.extractedFiles.clear();
242
381
  for (const file of extractedSourceFiles()) session.extractedFiles.add(file);
243
382
  }
244
383
  let graphAtomHashes;
245
- if (builder.context) {
246
- builder.context.encoder.atomizeObservedRecipes();
247
- graphAtomHashes = new Set(builder.context.encoder.atomic);
384
+ if (activeBuilder.context) {
385
+ activeBuilder.context.encoder.atomizeObservedRecipes();
386
+ graphAtomHashes = new Set(activeBuilder.context.encoder.atomic);
248
387
  }
249
- const css = builder.toCss({
250
- layerParams: true,
251
- includeRecipes: false
252
- });
388
+ const css = activeBuilder.toCss({ layerParams: true });
253
389
  session.prunableClasses.clear();
254
390
  session.viewTransitionClasses.clear();
255
- if (graphAtomHashes && builder.context) {
256
- const decoder = builder.context.decoder.collect(builder.context.encoder);
391
+ if (graphAtomHashes && activeBuilder.context) {
392
+ const decoder = activeBuilder.context.decoder.collect(activeBuilder.context.encoder);
257
393
  for (const atom of decoder.atomic) if (graphAtomHashes.has(atom.hash)) session.prunableClasses.add(atom.className);
258
394
  for (const transition of decoder.view_transitions) {
259
395
  session.viewTransitionClasses.add(transition.className);
@@ -261,7 +397,7 @@ const bamboocssCss = (options) => {
261
397
  }
262
398
  }
263
399
  return css;
264
- };
400
+ });
265
401
  const generate = () => {
266
402
  if (command === "serve" && pending && pendingGeneration === changeGeneration) return pending;
267
403
  pendingGeneration = changeGeneration;
@@ -341,6 +477,7 @@ const bamboocssCss = (options) => {
341
477
  sharedDuringBuild: true,
342
478
  async configResolved(config) {
343
479
  command = config.command;
480
+ host.setCommand(config.command);
344
481
  session.sourcemap = config.build.sourcemap;
345
482
  ssrBuildOptions = {
346
483
  ssr: config.build.ssr,
@@ -354,11 +491,12 @@ const bamboocssCss = (options) => {
354
491
  * edited all afternoon. Nothing watched it: `watch` is the CLI's own watcher, and a
355
492
  * project running `vite dev` never reaches it.
356
493
  *
357
- * A restart rather than re-emitting the stylesheet, because this plugin and the compiler
358
- * hold *separate* contexts and only this one reloads its config. A token *value* edit
359
- * came out right on the next source change, and an edit that changes what compiles
360
- * adding a token, a condition, a utility left the compiler naming classes from the old
361
- * config against a sheet emitted from the new one. Half-updated is worse than stale.
494
+ * A restart rather than re-emitting the stylesheet. The two plugins share one context now,
495
+ * and the compiler re-derives everything it holds when `Builder.setup` replaces it so the
496
+ * half-updated state this used to prevent, with the compiler naming classes from the old
497
+ * config against a sheet emitted from the new one, can no longer happen. What a restart
498
+ * still buys is the rest of the server: a changed `outdir`, a preset that adds an entry
499
+ * point, and every module Vite has already transformed against the previous config.
362
500
  *
363
501
  * Through Vite's own list rather than a watcher of ours. Vite adds these paths to the
364
502
  * files it watches, which is what reaches a config *outside* `root` — a monorepo with one
@@ -451,23 +589,27 @@ const bamboocssCss = (options) => {
451
589
  * about. Vite 5 has one graph and no `environments`, where the question is exact.
452
590
  */
453
591
  const clientGraph = devServer.environments?.client?.moduleGraph ?? devServer.moduleGraph;
454
- const invalidate = (file) => {
455
- const ctx = builder?.context;
592
+ const invalidate = (file, event) => {
593
+ const activeBuilder = builder;
594
+ const ctx = activeBuilder?.context;
456
595
  if (!ctx) return;
457
596
  const absoluteFile = ctx.runtime.path.abs(ctx.config.cwd, file);
458
- if (!session.extractedFiles.has(absoluteFile)) return;
597
+ const wasExtracted = session.extractedFiles.has(absoluteFile);
598
+ const changesConfigMembership = event !== "update" && activeBuilder.isPotentialConfigDependency(absoluteFile);
599
+ if (!wasExtracted && (event !== "create" || !activeBuilder.isPotentialSourceFile(absoluteFile) && !changesConfigMembership)) return;
600
+ host.noteSourceChange(absoluteFile, event, { needsConfigReload: changesConfigMembership });
459
601
  changeGeneration++;
460
602
  prebuilt = void 0;
461
603
  const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
462
604
  if (!mod) return;
463
- if (clientGraph.getModulesByFile(absoluteFile)?.size) return;
605
+ if (wasExtracted && clientGraph.getModulesByFile(absoluteFile)?.size) return;
464
606
  server?.moduleGraph.invalidateModule(mod);
465
607
  server?.reloadModule(mod);
466
608
  _bamboocss_logger.logger.debug("vite", `styles invalidated by ${absoluteFile}`);
467
609
  };
468
- devServer.watcher.on("change", invalidate);
469
- devServer.watcher.on("add", invalidate);
470
- devServer.watcher.on("unlink", invalidate);
610
+ devServer.watcher.on("change", (file) => invalidate(file, "update"));
611
+ devServer.watcher.on("add", (file) => invalidate(file, "create"));
612
+ devServer.watcher.on("unlink", (file) => invalidate(file, "delete"));
471
613
  },
472
614
  generateBundle: {
473
615
  order: "post",
@@ -628,6 +770,22 @@ const compilerParsePath = (id, code) => {
628
770
  return `${filePath}.__bamboo__.ts`;
629
771
  };
630
772
  /**
773
+ * Where to park a transform's text when it is not what the shared Project holds for the file.
774
+ *
775
+ * The compiler folds the bundler's view of a module — after every `enforce: 'pre'` plugin
776
+ * before it, and after Vite's own load. The stylesheet pass reads the same file off disk
777
+ * through the same ts-morph Project. When the two texts differ and the compiler writes its
778
+ * own under the file's path, that transform silently becomes the canonical source for the
779
+ * next extraction pass: the CSS would then be generated from a bundler artifact rather than
780
+ * from the checkout. Under a sibling path both readings exist and neither overwrites the
781
+ * other, which is the same reason `compilerParsePath` already does this for SFC submodules.
782
+ *
783
+ * The extension carries JSX-ness across, since it is what ts-morph keys its script kind on:
784
+ * anything but an unambiguously non-JSX `.ts`/`.mts`/`.cts` is parsed as `.tsx`, so a `<div>`
785
+ * in a `.js` file still parses and a `<T>value` assertion in a `.ts` file still means a cast.
786
+ */
787
+ const auxiliaryParsePath = (filePath) => `${filePath}.__bamboo__.${/\.[cm]?ts$/i.test(filePath) ? "ts" : "tsx"}`;
788
+ /**
631
789
  * Is this file part of the generated `styled-system` rather than the user's source?
632
790
  *
633
791
  * Resolved to a path and compared as a prefix, rather than by looking for the outdir's
@@ -682,6 +840,17 @@ const bamboocss = (options = {}) => {
682
840
  if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
683
841
  if ("renameCssAsset" in options) throw new Error("bamboocss: `renameCssAsset` has been replaced by `pruneCss`. Use `pruneCss: false` for what `renameCssAsset: false` did — it always disabled the pruning as well, since pruned bytes under the unpruned sheet's name is what lets a CDN serve a stale stylesheet. The new name says which of the two it is really about.");
684
842
  const staticSession = createStaticCompilationSession();
843
+ /**
844
+ * One Builder, one resolved config, one context and one ts-morph project for the run.
845
+ *
846
+ * Created here rather than by either plugin because both need it and neither may own it:
847
+ * the compiler used to load a second config of its own, which is why a token edit could
848
+ * leave it naming classes from the old one against a sheet emitted from the new one.
849
+ */
850
+ const host = createCompilationHost({
851
+ configPath,
852
+ cwd
853
+ });
685
854
  const transformArtifactIntegrityKey = (0, node_crypto.randomBytes)(32);
686
855
  const serializeTransformArtifact = (environment, artifact) => JSON.stringify([
687
856
  TRANSFORM_META_KEY,
@@ -1326,6 +1495,8 @@ const bamboocss = (options = {}) => {
1326
1495
  const foldOutputUnchanged = (state, dependent, changedFile) => {
1327
1496
  const memoized = state.unchangedFolds.get(dependent);
1328
1497
  if (memoized !== void 0) return memoized;
1498
+ if (host.isCssPassActive()) return false;
1499
+ if (!compilerStateIsCurrent()) return false;
1329
1500
  const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent, changedFile);
1330
1501
  state.changedRun = unchanged ? 0 : state.changedRun + 1;
1331
1502
  state.unchangedFolds.set(dependent, unchanged);
@@ -1337,7 +1508,8 @@ const bamboocss = (options = {}) => {
1337
1508
  try {
1338
1509
  const retained = state.foldInputsByModule.get(dependent);
1339
1510
  const code = retained?.input === signature.input ? retained.code : (0, node_fs.readFileSync)(signature.path, "utf8");
1340
- const parsePath = retained?.input === signature.input ? retained.parsePath : signature.path;
1511
+ const requestedParsePath = retained?.input === signature.input ? retained.parsePath : signature.path;
1512
+ const parsePath = compilerSourcePath(signature.path, requestedParsePath, code);
1341
1513
  const inputDigest = digest(code);
1342
1514
  if (inputDigest !== signature.input) return false;
1343
1515
  /**
@@ -1354,7 +1526,7 @@ const bamboocss = (options = {}) => {
1354
1526
  */
1355
1527
  const reads = state.exportReadsByModule.get(dependent);
1356
1528
  if (reads?.length && verifyExportReadsImpl) {
1357
- const { verdict, crossings } = verifyExportReadsImpl(ctx, (path) => ctx?.project.parseSourceFile(path), reads, normalizeFsPath(changedFile), verifyDigestMemo);
1529
+ const { verdict, crossings } = verifyExportReadsImpl(ctx, parseForCompiler, reads, normalizeFsPath(changedFile), verifyDigestMemo);
1358
1530
  if (verdict === "unchanged") {
1359
1531
  recordFoldDependencies(state, dependent, signature.path, [...state.dependenciesByModule.get(dependent) ?? [], ...crossings]);
1360
1532
  return true;
@@ -1369,8 +1541,9 @@ const bamboocss = (options = {}) => {
1369
1541
  raw = memoized.result;
1370
1542
  parserDependencies = memoized.parserDependencies;
1371
1543
  } else {
1372
- const sourceFile = ctx.project.addSourceFile(parsePath, code);
1373
- const parserResult = ctx.project.parseSourceFile(parsePath);
1544
+ const sourceFile = addCompilerSource(signature.path, parsePath, code);
1545
+ if (!sourceFile) return false;
1546
+ const parserResult = parseForCompiler(parsePath, requestedParsePath === signature.path ? signature.path : parsePath);
1374
1547
  if (!parserResult) return false;
1375
1548
  raw = foldSourceImpl({
1376
1549
  ctx,
@@ -1380,7 +1553,7 @@ const bamboocss = (options = {}) => {
1380
1553
  runtimeCss,
1381
1554
  styleCompiler,
1382
1555
  maxRecipeStates,
1383
- parseModule: (path) => ctx?.project.parseSourceFile(path),
1556
+ parseModule: parseForCompiler,
1384
1557
  recipeConfigCache: state.recipeConfigCache,
1385
1558
  reportSurvivors: false,
1386
1559
  sourceFile
@@ -1520,6 +1693,8 @@ const bamboocss = (options = {}) => {
1520
1693
  return [...modules, ...added];
1521
1694
  };
1522
1695
  let ctx;
1696
+ /** The compiler's private parse sink for `ctx`. @see `CompilationGeneration.encoder` */
1697
+ let parseEncoder;
1523
1698
  let foldSourceImpl;
1524
1699
  let verifyExportReadsImpl;
1525
1700
  let runtimeCss;
@@ -1559,25 +1734,97 @@ const bamboocss = (options = {}) => {
1559
1734
  dependencies: expanded
1560
1735
  };
1561
1736
  };
1562
- const loadContext = createRetryableLazy(async () => {
1563
- const { loadConfigAndCreateContext } = await loadNodeModule();
1564
- return loadConfigAndCreateContext({
1565
- configPath,
1566
- cwd,
1567
- dev: command === "serve"
1568
- });
1569
- });
1570
- const loadCompilerState = createLazyCompilerState(loadContext, loadFoldModule);
1737
+ /** Which context the published derivations below were built from. */
1738
+ let derivedGeneration = -1;
1739
+ /** Compiler-only sibling ASTs retained for each physical module. */
1740
+ const auxiliarySourcesByFile = /* @__PURE__ */ new Map();
1741
+ /**
1742
+ * Whether the compiler state below still describes the context the host is on.
1743
+ *
1744
+ * Only `ensureCompilerState` re-derives, and only an awaited hook may call it — so the two
1745
+ * synchronous entry points, the speculative prefold and the unchanged-dependent check, can
1746
+ * be reached after a stylesheet pass has published a config reload they have not seen. Both
1747
+ * decline rather than fold against a runtime `css` from the previous config.
1748
+ */
1749
+ const compilerStateIsCurrent = () => {
1750
+ const current = host.current();
1751
+ return current !== void 0 && current.id === derivedGeneration;
1752
+ };
1571
1753
  const ensureContext = async () => {
1572
- ctx = await loadContext();
1754
+ ctx = (await host.ensureGeneration()).context;
1573
1755
  };
1756
+ /**
1757
+ * Load the fold chunk and derive everything that depends on the resolved context.
1758
+ *
1759
+ * Keyed on context *identity* rather than derived once. `Builder.setup` replaces its context
1760
+ * on a config reload, and the runtime `css`, the style-set compiler and the parse sink are
1761
+ * all closures over the previous one — a stale `runtimeCss` names classes from the old
1762
+ * config while the stylesheet is emitted from the new one, and nothing downstream can see
1763
+ * the difference. Re-derivation is cheap; both factories are a handful of bound methods.
1764
+ *
1765
+ * Published as a set, and only once every part of the attempt has succeeded, so a failed
1766
+ * chunk load leaves no half-compiler visible to HMR.
1767
+ */
1574
1768
  const ensureCompilerState = async () => {
1575
- const loaded = await loadCompilerState();
1576
- ctx = loaded.context;
1577
- foldSourceImpl = loaded.foldSource;
1578
- verifyExportReadsImpl = loaded.verifyExportReads;
1579
- runtimeCss = loaded.runtimeCss;
1580
- styleCompiler = loaded.styleCompiler;
1769
+ const [initialGeneration, fold] = await Promise.all([host.ensureGeneration(), loadFoldModule()]);
1770
+ const currentGeneration = await host.ensureGeneration();
1771
+ const generation = currentGeneration.id === initialGeneration.id ? initialGeneration : currentGeneration;
1772
+ if (derivedGeneration === generation.id && foldSourceImpl) {
1773
+ ctx = generation.context;
1774
+ return;
1775
+ }
1776
+ const derivedRuntimeCss = fold.createRuntimeCss(generation.context);
1777
+ const derivedStyleCompiler = fold.createStaticStyleSetCompiler(generation.context, derivedRuntimeCss);
1778
+ ctx = generation.context;
1779
+ parseEncoder = generation.encoder;
1780
+ foldSourceImpl = fold.foldSource;
1781
+ verifyExportReadsImpl = fold.verifyExportReads;
1782
+ runtimeCss = derivedRuntimeCss;
1783
+ styleCompiler = derivedStyleCompiler;
1784
+ derivedGeneration = generation.id;
1785
+ auxiliarySourcesByFile.clear();
1786
+ };
1787
+ /**
1788
+ * Parse a module for the compiler, never for the stylesheet.
1789
+ *
1790
+ * Every compiler parse goes through here so the private encoder cannot be forgotten at one
1791
+ * call site. Forgetting it at any of them puts that module's reading into the encoder the
1792
+ * sheet is emitted from, under a `parse` owner nothing retracts.
1793
+ */
1794
+ const parseForCompiler = (filePath, hookFilePath = filePath) => ctx?.project.parseSourceFile(filePath, parseEncoder, { hookFilePath });
1795
+ /**
1796
+ * Where the compiler may hold `code` for `filePath` without displacing the checkout.
1797
+ *
1798
+ * The file's own path exactly when the shared Project already holds these bytes — then
1799
+ * `addSourceFile` is a lookup and there is nothing to displace. @see `auxiliaryParsePath`
1800
+ */
1801
+ const compilerSourcePath = (filePath, requested, code) => {
1802
+ if (requested !== filePath) return requested;
1803
+ return ctx?.project.getSourceFile(filePath)?.getFullText() === code ? filePath : auxiliaryParsePath(filePath);
1804
+ };
1805
+ /** Add one compiler-owned source without letting it displace or outlive its physical file. */
1806
+ const addCompilerSource = (filePath, parsePath, code) => {
1807
+ if (!ctx) return;
1808
+ const auxiliary = parsePath !== filePath;
1809
+ const sourceFile = ctx.project.addSourceFile(parsePath, code, { auxiliary });
1810
+ if (auxiliary) {
1811
+ const physical = normalizeFsPath(filePath);
1812
+ const paths = auxiliarySourcesByFile.get(physical) ?? /* @__PURE__ */ new Set();
1813
+ paths.add(parsePath);
1814
+ auxiliarySourcesByFile.set(physical, paths);
1815
+ }
1816
+ return sourceFile;
1817
+ };
1818
+ /** Release compiler encoder owners and sibling ASTs when their physical module disappears. */
1819
+ const releaseCompilerSources = (filePath) => {
1820
+ if (!ctx) return;
1821
+ parseEncoder?.releaseFile(filePath);
1822
+ const physical = normalizeFsPath(filePath);
1823
+ for (const auxiliary of auxiliarySourcesByFile.get(physical) ?? []) {
1824
+ parseEncoder?.releaseFile(auxiliary);
1825
+ ctx.project.removeSourceFile(auxiliary);
1826
+ }
1827
+ auxiliarySourcesByFile.delete(physical);
1581
1828
  };
1582
1829
  const outputFinalizerTag = (value) => {
1583
1830
  if (!value || typeof value !== "object") return void 0;
@@ -1694,6 +1941,7 @@ const bamboocss = (options = {}) => {
1694
1941
  },
1695
1942
  configResolved(config) {
1696
1943
  command = config.command;
1944
+ host.setCommand(config.command);
1697
1945
  defaultEmitAssets = config.build?.emitAssets ?? (!config.build?.ssr || config.build?.ssrEmitAssets === true);
1698
1946
  const plugins = config.plugins;
1699
1947
  if (plugins) {
@@ -1743,14 +1991,15 @@ const bamboocss = (options = {}) => {
1743
1991
  state.unchangedFolds.clear();
1744
1992
  state.changedRun = 0;
1745
1993
  }
1746
- if (!ctx) return;
1747
- if (!shouldTransform(id)) return;
1748
1994
  const [filePath] = id.split("?");
1749
1995
  if (!filePath) return;
1996
+ if (change.event === "update") host.noteSourceChange(filePath, change.event);
1997
+ if (!ctx) return;
1998
+ if (!shouldTransform(id)) return;
1750
1999
  for (const state of transformStateByEnvironment.values()) state.recipeConfigCache.clear();
1751
- if (SFC_EXTENSIONS.test(filePath)) return;
1752
2000
  if (change.event === "delete") {
1753
- ctx.project.removeSourceFile(filePath);
2001
+ host.removeSource(filePath);
2002
+ releaseCompilerSources(filePath);
1754
2003
  const deleted = normalizeFsPath(filePath);
1755
2004
  for (const state of transformStateByEnvironment.values()) for (const [moduleId, moduleFile] of [...state.filesByModule]) {
1756
2005
  if (normalizeFsPath(moduleFile) !== deleted) continue;
@@ -1762,7 +2011,8 @@ const bamboocss = (options = {}) => {
1762
2011
  }
1763
2012
  return;
1764
2013
  }
1765
- ctx.project.reloadSourceFile(filePath);
2014
+ if (SFC_EXTENSIONS.test(filePath)) return;
2015
+ host.reloadSource(filePath);
1766
2016
  /**
1767
2017
  * Fold the edited file before the browser asks for it.
1768
2018
  *
@@ -1781,12 +2031,14 @@ const bamboocss = (options = {}) => {
1781
2031
  */
1782
2032
  if (command === "serve") setImmediate(() => {
1783
2033
  if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return;
2034
+ if (host.isCssPassActive() || !compilerStateIsCurrent()) return;
1784
2035
  try {
1785
2036
  const code = (0, node_fs.readFileSync)(filePath, "utf8");
1786
2037
  const memoKey = foldMemoKey(filePath, digest(code));
1787
2038
  if (foldMemoByContent.has(memoKey)) return;
1788
- const sourceFile = ctx.project.addSourceFile(filePath, code);
1789
- const parserResult = ctx.project.parseSourceFile(filePath);
2039
+ const sourceFile = addCompilerSource(filePath, filePath, code);
2040
+ if (!sourceFile) return;
2041
+ const parserResult = parseForCompiler(filePath);
1790
2042
  if (!parserResult) return;
1791
2043
  const folded = foldSourceImpl({
1792
2044
  ctx,
@@ -1796,7 +2048,7 @@ const bamboocss = (options = {}) => {
1796
2048
  runtimeCss,
1797
2049
  styleCompiler,
1798
2050
  maxRecipeStates,
1799
- parseModule: (path) => ctx?.project.parseSourceFile(path),
2051
+ parseModule: parseForCompiler,
1800
2052
  recipeConfigCache: transformStateByEnvironment.get("client")?.recipeConfigCache ?? /* @__PURE__ */ new Map(),
1801
2053
  reportSurvivors: true,
1802
2054
  sourceFile
@@ -1924,8 +2176,8 @@ const bamboocss = (options = {}) => {
1924
2176
  if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return null;
1925
2177
  const [filePath] = id.split("?");
1926
2178
  if (isGeneratedOutput(filePath, ctx)) return null;
1927
- const parsePath = compilerParsePath(id, code);
1928
- if (parsePath === null) return null;
2179
+ const requestedParsePath = compilerParsePath(id, code);
2180
+ if (requestedParsePath === null) return null;
1929
2181
  const state = environmentState(this);
1930
2182
  state.transformedModulesThisRun.add(id);
1931
2183
  let inputDigest;
@@ -1933,46 +2185,62 @@ const bamboocss = (options = {}) => {
1933
2185
  const previousDependencies = previousSignature && previousSignature.input === (inputDigest ??= digest(code)) ? state.dependenciesByModule.get(id) : void 0;
1934
2186
  let result;
1935
2187
  try {
1936
- const memoKey = command === "serve" ? foldMemoKey(parsePath, inputDigest ??= digest(code)) : void 0;
1937
- const memoized = memoKey ? foldMemoByContent.get(memoKey) : void 0;
1938
- let valueReads = [];
1939
- if (memoized?.reportedSurvivors) {
1940
- valueReads = memoized.valueReads;
1941
- result = withResolutionClosure(parsePath, memoized.result, memoized.parserDependencies, previousDependencies);
1942
- } else {
1943
- const sourceFile = ctx.project.addSourceFile(parsePath, code);
1944
- const parserResult = ctx.project.parseSourceFile(parsePath);
1945
- if (!parserResult) {
1946
- state.transformArtifactsByModule.delete(id);
1947
- recordFoldDependencies(state, id, filePath, []);
1948
- state.foldSignatures.delete(id);
1949
- state.foldInputsByModule.delete(id);
1950
- return null;
1951
- }
2188
+ /**
2189
+ * One serialized region, holding every read and every mutation of the shared AST.
2190
+ *
2191
+ * Synchronous throughout, which is what makes waiting for the stylesheet pass once at
2192
+ * the top sufficient: nothing can open a pass between the wait and the work, because
2193
+ * nothing else runs. The fold is CPU-bound anyway, so there is no await to give up.
2194
+ */
2195
+ const compiled = await host.runCompilerWork(() => {
2196
+ if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return null;
2197
+ const path = compilerSourcePath(filePath, requestedParsePath, code);
2198
+ const memoKey = command === "serve" ? foldMemoKey(path, inputDigest ??= digest(code)) : void 0;
2199
+ const memoized = memoKey ? foldMemoByContent.get(memoKey) : void 0;
2200
+ if (memoized?.reportedSurvivors) return {
2201
+ valueReads: memoized.valueReads,
2202
+ result: withResolutionClosure(path, memoized.result, memoized.parserDependencies, previousDependencies)
2203
+ };
2204
+ const sourceFile = addCompilerSource(filePath, path, code);
2205
+ if (!sourceFile) return null;
2206
+ const parserResult = parseForCompiler(path, requestedParsePath === filePath ? filePath : path);
2207
+ if (!parserResult) return { unparsed: true };
1952
2208
  const folded = foldSourceImpl({
1953
2209
  ctx,
1954
2210
  code,
1955
2211
  parserResult,
1956
- filePath: parsePath,
2212
+ filePath: path,
1957
2213
  runtimeCss,
1958
2214
  styleCompiler,
1959
2215
  maxRecipeStates,
1960
- parseModule: (path) => ctx?.project.parseSourceFile(path),
2216
+ parseModule: parseForCompiler,
1961
2217
  recipeConfigCache: state.recipeConfigCache,
1962
2218
  reportSurvivors: true,
1963
2219
  sourceFile
1964
2220
  });
1965
2221
  const parserDependencies = parserResult.getDependencies();
1966
- valueReads = parserResult.getExportReads?.() ?? [];
2222
+ const valueReads = parserResult.getExportReads?.() ?? [];
1967
2223
  if (memoKey) foldMemoByContent.set(memoKey, {
1968
2224
  result: folded,
1969
2225
  parserDependencies,
1970
2226
  valueReads,
1971
2227
  reportedSurvivors: true
1972
2228
  });
1973
- result = withResolutionClosure(parsePath, folded, parserDependencies, previousDependencies);
2229
+ return {
2230
+ valueReads,
2231
+ result: withResolutionClosure(path, folded, parserDependencies, previousDependencies)
2232
+ };
2233
+ });
2234
+ if (!compiled) return null;
2235
+ if ("unparsed" in compiled) {
2236
+ state.transformArtifactsByModule.delete(id);
2237
+ recordFoldDependencies(state, id, filePath, []);
2238
+ state.foldSignatures.delete(id);
2239
+ state.foldInputsByModule.delete(id);
2240
+ return null;
1974
2241
  }
1975
- state.exportReadsByModule.set(id, [...valueReads.map((read) => ({
2242
+ result = compiled.result;
2243
+ state.exportReadsByModule.set(id, [...compiled.valueReads.map((read) => ({
1976
2244
  kind: "value",
1977
2245
  ...read
1978
2246
  })), ...result.exportReads]);
@@ -2028,10 +2296,10 @@ const bamboocss = (options = {}) => {
2028
2296
  } } : {}
2029
2297
  });
2030
2298
  applyTransformArtifact(state, artifact, id, environmentName(this));
2031
- if (artifact.signature && (command === "serve" || parsePath !== filePath)) state.foldInputsByModule.set(id, {
2299
+ if (artifact.signature && (command === "serve" || requestedParsePath !== filePath)) state.foldInputsByModule.set(id, {
2032
2300
  code,
2033
2301
  input: artifact.signature.input,
2034
- parsePath
2302
+ parsePath: requestedParsePath
2035
2303
  });
2036
2304
  else state.foldInputsByModule.delete(id);
2037
2305
  if (reportSkipped && result.skipped.length) _bamboocss_logger.logger.info("vite:transform", formatSkipped(filePath, result.skipped));
@@ -2093,6 +2361,7 @@ const bamboocss = (options = {}) => {
2093
2361
  bamboocssCss({
2094
2362
  configPath,
2095
2363
  cwd,
2364
+ host,
2096
2365
  session: staticSession,
2097
2366
  pruneCss
2098
2367
  }),