@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.mjs CHANGED
@@ -43,24 +43,156 @@ const loadCssOutputModule = createLazyCssOutputModule();
43
43
  const createLazyFoldModule = (loadFold = () => import("./fold-module.mjs")) => createRetryableLazy(loadFold);
44
44
  /** One process-wide fold-module load shared by every plugin instance and Vite environment. */
45
45
  const loadFoldModule = createLazyFoldModule();
46
- /** One Builder per CSS plugin instance, created only when a hook first needs it. */
46
+ /**
47
+ * The one Builder a run compiles against, created only when a hook first needs it.
48
+ *
49
+ * Per host rather than per plugin instance: the compiler and the stylesheet share it now.
50
+ * @see `createCompilationHost`
51
+ */
47
52
  const createLazyBuilder = (loadNode = loadNodeModule) => createRetryableLazy(async () => {
48
53
  const { Builder } = await loadNode();
49
54
  return new Builder();
50
55
  });
51
- /** Build and publish one complete fold state; no caller can observe partial initialization. */
52
- const createLazyCompilerState = (loadContext, loadFold) => createRetryableLazy(async () => {
53
- const [context, fold] = await Promise.all([loadContext(), loadFold()]);
54
- const runtimeCss = fold.createRuntimeCss(context);
55
- const styleCompiler = fold.createStaticStyleSetCompiler(context, runtimeCss);
56
+ //#endregion
57
+ //#region src/compilation-host.ts
58
+ const createCompilationHost = (options = {}) => {
59
+ const { configPath, cwd } = options;
60
+ const loadBuilder = options.loadBuilder ?? createLazyBuilder();
61
+ let command = "build";
62
+ let builder;
63
+ let generation;
64
+ let nextGenerationId = 0;
65
+ /**
66
+ * The setup covering the pass currently open.
67
+ *
68
+ * A cold start reaches this twice — the compiler's `pre` `buildStart`, then the CSS
69
+ * plugin's — for one instant in which nothing can have changed on disk. Sharing one attempt
70
+ * across both is what keeps a project from loading and evaluating its config twice per
71
+ * build. Cleared once a stylesheet pass consumes it, and on any source mutation, so no
72
+ * later pass can be answered by a setup taken before an edit.
73
+ */
74
+ let openSetup;
75
+ let openSetupStale = false;
76
+ let cssPass;
77
+ let changedSourceFiles = /* @__PURE__ */ new Set();
78
+ let needsInventoryScan = false;
79
+ let needsConfigReload = false;
80
+ const recordSourceChange = (filePath, event, options) => {
81
+ changedSourceFiles.add(filePath);
82
+ if (event !== "update") needsInventoryScan = true;
83
+ needsConfigReload ||= options?.needsConfigReload === true;
84
+ openSetupStale = true;
85
+ };
86
+ const takeSourceChanges = () => {
87
+ const changes = {
88
+ files: [...changedSourceFiles].sort(),
89
+ needsInventoryScan,
90
+ ...needsConfigReload ? { needsConfigReload: true } : {}
91
+ };
92
+ changedSourceFiles = /* @__PURE__ */ new Set();
93
+ needsInventoryScan = false;
94
+ needsConfigReload = false;
95
+ return changes;
96
+ };
97
+ const restoreSourceChanges = (changes) => {
98
+ for (const file of changes.files) changedSourceFiles.add(file);
99
+ needsInventoryScan ||= changes.needsInventoryScan === true;
100
+ needsConfigReload ||= changes.needsConfigReload === true;
101
+ };
102
+ const settled = async (attempt) => {
103
+ try {
104
+ await attempt;
105
+ } catch {}
106
+ };
107
+ const publish = () => {
108
+ const context = builder.getContextOrThrow();
109
+ if (generation?.context !== context) generation = {
110
+ id: ++nextGenerationId,
111
+ context,
112
+ encoder: context.encoder.clone()
113
+ };
114
+ return generation;
115
+ };
116
+ const runSetup = async () => {
117
+ builder ??= await loadBuilder();
118
+ const sourceChanges = takeSourceChanges();
119
+ try {
120
+ await builder.setup({
121
+ configPath,
122
+ cwd,
123
+ dev: command === "serve",
124
+ ...command === "serve" ? { sourceChanges } : {}
125
+ });
126
+ return publish();
127
+ } catch (error) {
128
+ if (command === "serve") restoreSourceChanges(sourceChanges);
129
+ throw error;
130
+ }
131
+ };
132
+ /**
133
+ * The setup covering the pass currently open, started at most once.
134
+ *
135
+ * Started through a resolved promise, so a synchronous throw becomes the same
136
+ * rejected-attempt contract a failed module load has and a later hook can retry it. A
137
+ * source mutation observed while one is in flight does not cancel it — two overlapping
138
+ * `Builder.setup` calls would interleave their change detection — it queues a fresh one
139
+ * behind it.
140
+ */
141
+ const setupOnce = () => {
142
+ const previous = openSetup;
143
+ if (previous && !openSetupStale) return previous;
144
+ openSetupStale = false;
145
+ const attempt = previous ? settled(previous).then(runSetup) : Promise.resolve().then(runSetup);
146
+ openSetup = attempt;
147
+ attempt.catch(() => {
148
+ if (openSetup === attempt) openSetup = void 0;
149
+ });
150
+ return attempt;
151
+ };
56
152
  return {
57
- context,
58
- foldSource: fold.foldSource,
59
- verifyExportReads: fold.verifyExportReads,
60
- runtimeCss,
61
- styleCompiler
153
+ setCommand(next) {
154
+ command = next;
155
+ },
156
+ current: () => generation,
157
+ async ensureGeneration() {
158
+ if (cssPass) await settled(cssPass);
159
+ if (generation) return Promise.resolve(generation);
160
+ return setupOnce();
161
+ },
162
+ isCssPassActive: () => cssPass !== void 0,
163
+ async runCssPass(run) {
164
+ while (cssPass) await settled(cssPass);
165
+ let release;
166
+ cssPass = new Promise((resolve) => {
167
+ release = resolve;
168
+ });
169
+ try {
170
+ const passGeneration = await setupOnce();
171
+ return await run(builder, passGeneration);
172
+ } finally {
173
+ openSetup = void 0;
174
+ openSetupStale = false;
175
+ cssPass = void 0;
176
+ release();
177
+ }
178
+ },
179
+ async runCompilerWork(run) {
180
+ while (cssPass) await settled(cssPass);
181
+ return run();
182
+ },
183
+ noteSourceChange(filePath, event, options) {
184
+ if (command === "serve") recordSourceChange(filePath, event, options);
185
+ },
186
+ reloadSource(filePath) {
187
+ recordSourceChange(filePath, "update");
188
+ builder?.reloadSource(filePath);
189
+ },
190
+ removeSource(filePath) {
191
+ recordSourceChange(filePath, "update");
192
+ builder?.removeSource(filePath);
193
+ }
62
194
  };
63
- });
195
+ };
64
196
  //#endregion
65
197
  //#region src/static-session.ts
66
198
  const createStaticCompilationSession = () => {
@@ -179,24 +311,26 @@ const asError = (error, context) => error instanceof Error ? error : new Error(`
179
311
  * process just wrote, which is a race on any watch rebuild.
180
312
  */
181
313
  const bamboocssCss = (options) => {
182
- const { configPath, cwd, loadCssOutput = loadCssOutputModule, session, pruneCss = true } = options;
314
+ const { configPath, cwd, loadCssOutput = loadCssOutputModule, session, host = createCompilationHost({
315
+ configPath,
316
+ cwd
317
+ }), pruneCss = true } = options;
183
318
  let builder;
184
- const loadBuilder = createLazyBuilder();
185
- const ensureBuilder = async () => {
186
- const loaded = await loadBuilder();
187
- builder = loaded;
188
- return loaded;
189
- };
190
319
  let server;
191
320
  let command = "build";
192
321
  /** The run's own `build` options, for a bundler with no per-environment config. */
193
322
  let ssrBuildOptions;
194
- /** Included owners plus local modules their extraction actually resolved and read. */
323
+ /** Every source, resolver input and expanded config dependency which can change the sheet. */
195
324
  const extractedSourceFiles = () => {
196
325
  const activeBuilder = builder;
197
326
  const context = activeBuilder?.context;
198
327
  if (!context) return [];
199
- return [...new Set([...context.getFiles(), ...activeBuilder.getResolutionReadFiles()].map((file) => context.runtime.path.abs(context.config.cwd, file)))];
328
+ return [...new Set([
329
+ ...activeBuilder.getSourceFiles(),
330
+ ...activeBuilder.getResolutionReadFiles(),
331
+ ...activeBuilder.getResolutionConfigurationFiles(),
332
+ ...context.explicitDeps
333
+ ].map((file) => context.runtime.path.abs(context.config.cwd, file)))];
200
334
  };
201
335
  /**
202
336
  * Serialised, because both `load` and the watcher can reach it and `Builder` keeps one
@@ -221,34 +355,36 @@ const bamboocssCss = (options) => {
221
355
  let changeGeneration = 0;
222
356
  let pendingGeneration = -1;
223
357
  let servedCss;
224
- const build = async () => {
225
- const builder = await ensureBuilder();
226
- await builder.setup({
227
- configPath,
228
- cwd,
229
- dev: command === "serve"
230
- });
231
- await builder.emit();
232
- builder.extract();
358
+ /**
359
+ * Held by the host for its whole length, rather than only around each mutation.
360
+ *
361
+ * Extraction fills the encoder this sheet is emitted from and `toCss` reads it back, with a
362
+ * deliberate macrotask between them. The compiler shares the AST both halves run against,
363
+ * so a transform folding a module in that window would re-prepare a source the extraction
364
+ * pass has already read and `toCss` has not finished reporting on. The host makes compiler
365
+ * work wait instead; a fold is a few milliseconds and this is the one place correctness
366
+ * depends on it.
367
+ */
368
+ const build = () => host.runCssPass(async (activeBuilder) => {
369
+ builder = activeBuilder;
370
+ await activeBuilder.emit();
371
+ activeBuilder.extract();
233
372
  await new Promise((settle) => setImmediate(settle));
234
- if (builder.context) {
235
- session.utilityLayer = builder.context.config.layers?.utilities ?? "utilities";
373
+ if (activeBuilder.context) {
374
+ session.utilityLayer = activeBuilder.context.config.layers?.utilities ?? "utilities";
236
375
  session.extractedFiles.clear();
237
376
  for (const file of extractedSourceFiles()) session.extractedFiles.add(file);
238
377
  }
239
378
  let graphAtomHashes;
240
- if (builder.context) {
241
- builder.context.encoder.atomizeObservedRecipes();
242
- graphAtomHashes = new Set(builder.context.encoder.atomic);
379
+ if (activeBuilder.context) {
380
+ activeBuilder.context.encoder.atomizeObservedRecipes();
381
+ graphAtomHashes = new Set(activeBuilder.context.encoder.atomic);
243
382
  }
244
- const css = builder.toCss({
245
- layerParams: true,
246
- includeRecipes: false
247
- });
383
+ const css = activeBuilder.toCss({ layerParams: true });
248
384
  session.prunableClasses.clear();
249
385
  session.viewTransitionClasses.clear();
250
- if (graphAtomHashes && builder.context) {
251
- const decoder = builder.context.decoder.collect(builder.context.encoder);
386
+ if (graphAtomHashes && activeBuilder.context) {
387
+ const decoder = activeBuilder.context.decoder.collect(activeBuilder.context.encoder);
252
388
  for (const atom of decoder.atomic) if (graphAtomHashes.has(atom.hash)) session.prunableClasses.add(atom.className);
253
389
  for (const transition of decoder.view_transitions) {
254
390
  session.viewTransitionClasses.add(transition.className);
@@ -256,7 +392,7 @@ const bamboocssCss = (options) => {
256
392
  }
257
393
  }
258
394
  return css;
259
- };
395
+ });
260
396
  const generate = () => {
261
397
  if (command === "serve" && pending && pendingGeneration === changeGeneration) return pending;
262
398
  pendingGeneration = changeGeneration;
@@ -336,6 +472,7 @@ const bamboocssCss = (options) => {
336
472
  sharedDuringBuild: true,
337
473
  async configResolved(config) {
338
474
  command = config.command;
475
+ host.setCommand(config.command);
339
476
  session.sourcemap = config.build.sourcemap;
340
477
  ssrBuildOptions = {
341
478
  ssr: config.build.ssr,
@@ -349,11 +486,12 @@ const bamboocssCss = (options) => {
349
486
  * edited all afternoon. Nothing watched it: `watch` is the CLI's own watcher, and a
350
487
  * project running `vite dev` never reaches it.
351
488
  *
352
- * A restart rather than re-emitting the stylesheet, because this plugin and the compiler
353
- * hold *separate* contexts and only this one reloads its config. A token *value* edit
354
- * came out right on the next source change, and an edit that changes what compiles
355
- * adding a token, a condition, a utility left the compiler naming classes from the old
356
- * config against a sheet emitted from the new one. Half-updated is worse than stale.
489
+ * A restart rather than re-emitting the stylesheet. The two plugins share one context now,
490
+ * and the compiler re-derives everything it holds when `Builder.setup` replaces it so the
491
+ * half-updated state this used to prevent, with the compiler naming classes from the old
492
+ * config against a sheet emitted from the new one, can no longer happen. What a restart
493
+ * still buys is the rest of the server: a changed `outdir`, a preset that adds an entry
494
+ * point, and every module Vite has already transformed against the previous config.
357
495
  *
358
496
  * Through Vite's own list rather than a watcher of ours. Vite adds these paths to the
359
497
  * files it watches, which is what reaches a config *outside* `root` — a monorepo with one
@@ -446,23 +584,27 @@ const bamboocssCss = (options) => {
446
584
  * about. Vite 5 has one graph and no `environments`, where the question is exact.
447
585
  */
448
586
  const clientGraph = devServer.environments?.client?.moduleGraph ?? devServer.moduleGraph;
449
- const invalidate = (file) => {
450
- const ctx = builder?.context;
587
+ const invalidate = (file, event) => {
588
+ const activeBuilder = builder;
589
+ const ctx = activeBuilder?.context;
451
590
  if (!ctx) return;
452
591
  const absoluteFile = ctx.runtime.path.abs(ctx.config.cwd, file);
453
- if (!session.extractedFiles.has(absoluteFile)) return;
592
+ const wasExtracted = session.extractedFiles.has(absoluteFile);
593
+ const changesConfigMembership = event !== "update" && activeBuilder.isPotentialConfigDependency(absoluteFile);
594
+ if (!wasExtracted && (event !== "create" || !activeBuilder.isPotentialSourceFile(absoluteFile) && !changesConfigMembership)) return;
595
+ host.noteSourceChange(absoluteFile, event, { needsConfigReload: changesConfigMembership });
454
596
  changeGeneration++;
455
597
  prebuilt = void 0;
456
598
  const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
457
599
  if (!mod) return;
458
- if (clientGraph.getModulesByFile(absoluteFile)?.size) return;
600
+ if (wasExtracted && clientGraph.getModulesByFile(absoluteFile)?.size) return;
459
601
  server?.moduleGraph.invalidateModule(mod);
460
602
  server?.reloadModule(mod);
461
603
  logger.debug("vite", `styles invalidated by ${absoluteFile}`);
462
604
  };
463
- devServer.watcher.on("change", invalidate);
464
- devServer.watcher.on("add", invalidate);
465
- devServer.watcher.on("unlink", invalidate);
605
+ devServer.watcher.on("change", (file) => invalidate(file, "update"));
606
+ devServer.watcher.on("add", (file) => invalidate(file, "create"));
607
+ devServer.watcher.on("unlink", (file) => invalidate(file, "delete"));
466
608
  },
467
609
  generateBundle: {
468
610
  order: "post",
@@ -623,6 +765,22 @@ const compilerParsePath = (id, code) => {
623
765
  return `${filePath}.__bamboo__.ts`;
624
766
  };
625
767
  /**
768
+ * Where to park a transform's text when it is not what the shared Project holds for the file.
769
+ *
770
+ * The compiler folds the bundler's view of a module — after every `enforce: 'pre'` plugin
771
+ * before it, and after Vite's own load. The stylesheet pass reads the same file off disk
772
+ * through the same ts-morph Project. When the two texts differ and the compiler writes its
773
+ * own under the file's path, that transform silently becomes the canonical source for the
774
+ * next extraction pass: the CSS would then be generated from a bundler artifact rather than
775
+ * from the checkout. Under a sibling path both readings exist and neither overwrites the
776
+ * other, which is the same reason `compilerParsePath` already does this for SFC submodules.
777
+ *
778
+ * The extension carries JSX-ness across, since it is what ts-morph keys its script kind on:
779
+ * anything but an unambiguously non-JSX `.ts`/`.mts`/`.cts` is parsed as `.tsx`, so a `<div>`
780
+ * in a `.js` file still parses and a `<T>value` assertion in a `.ts` file still means a cast.
781
+ */
782
+ const auxiliaryParsePath = (filePath) => `${filePath}.__bamboo__.${/\.[cm]?ts$/i.test(filePath) ? "ts" : "tsx"}`;
783
+ /**
626
784
  * Is this file part of the generated `styled-system` rather than the user's source?
627
785
  *
628
786
  * Resolved to a path and compared as a prefix, rather than by looking for the outdir's
@@ -677,6 +835,17 @@ const bamboocss = (options = {}) => {
677
835
  if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
678
836
  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.");
679
837
  const staticSession = createStaticCompilationSession();
838
+ /**
839
+ * One Builder, one resolved config, one context and one ts-morph project for the run.
840
+ *
841
+ * Created here rather than by either plugin because both need it and neither may own it:
842
+ * the compiler used to load a second config of its own, which is why a token edit could
843
+ * leave it naming classes from the old one against a sheet emitted from the new one.
844
+ */
845
+ const host = createCompilationHost({
846
+ configPath,
847
+ cwd
848
+ });
680
849
  const transformArtifactIntegrityKey = randomBytes(32);
681
850
  const serializeTransformArtifact = (environment, artifact) => JSON.stringify([
682
851
  TRANSFORM_META_KEY,
@@ -1321,6 +1490,8 @@ const bamboocss = (options = {}) => {
1321
1490
  const foldOutputUnchanged = (state, dependent, changedFile) => {
1322
1491
  const memoized = state.unchangedFolds.get(dependent);
1323
1492
  if (memoized !== void 0) return memoized;
1493
+ if (host.isCssPassActive()) return false;
1494
+ if (!compilerStateIsCurrent()) return false;
1324
1495
  const unchanged = state.changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(state, dependent, changedFile);
1325
1496
  state.changedRun = unchanged ? 0 : state.changedRun + 1;
1326
1497
  state.unchangedFolds.set(dependent, unchanged);
@@ -1332,7 +1503,8 @@ const bamboocss = (options = {}) => {
1332
1503
  try {
1333
1504
  const retained = state.foldInputsByModule.get(dependent);
1334
1505
  const code = retained?.input === signature.input ? retained.code : readFileSync(signature.path, "utf8");
1335
- const parsePath = retained?.input === signature.input ? retained.parsePath : signature.path;
1506
+ const requestedParsePath = retained?.input === signature.input ? retained.parsePath : signature.path;
1507
+ const parsePath = compilerSourcePath(signature.path, requestedParsePath, code);
1336
1508
  const inputDigest = digest(code);
1337
1509
  if (inputDigest !== signature.input) return false;
1338
1510
  /**
@@ -1349,7 +1521,7 @@ const bamboocss = (options = {}) => {
1349
1521
  */
1350
1522
  const reads = state.exportReadsByModule.get(dependent);
1351
1523
  if (reads?.length && verifyExportReadsImpl) {
1352
- const { verdict, crossings } = verifyExportReadsImpl(ctx, (path) => ctx?.project.parseSourceFile(path), reads, normalizeFsPath(changedFile), verifyDigestMemo);
1524
+ const { verdict, crossings } = verifyExportReadsImpl(ctx, parseForCompiler, reads, normalizeFsPath(changedFile), verifyDigestMemo);
1353
1525
  if (verdict === "unchanged") {
1354
1526
  recordFoldDependencies(state, dependent, signature.path, [...state.dependenciesByModule.get(dependent) ?? [], ...crossings]);
1355
1527
  return true;
@@ -1364,8 +1536,9 @@ const bamboocss = (options = {}) => {
1364
1536
  raw = memoized.result;
1365
1537
  parserDependencies = memoized.parserDependencies;
1366
1538
  } else {
1367
- const sourceFile = ctx.project.addSourceFile(parsePath, code);
1368
- const parserResult = ctx.project.parseSourceFile(parsePath);
1539
+ const sourceFile = addCompilerSource(signature.path, parsePath, code);
1540
+ if (!sourceFile) return false;
1541
+ const parserResult = parseForCompiler(parsePath, requestedParsePath === signature.path ? signature.path : parsePath);
1369
1542
  if (!parserResult) return false;
1370
1543
  raw = foldSourceImpl({
1371
1544
  ctx,
@@ -1375,7 +1548,7 @@ const bamboocss = (options = {}) => {
1375
1548
  runtimeCss,
1376
1549
  styleCompiler,
1377
1550
  maxRecipeStates,
1378
- parseModule: (path) => ctx?.project.parseSourceFile(path),
1551
+ parseModule: parseForCompiler,
1379
1552
  recipeConfigCache: state.recipeConfigCache,
1380
1553
  reportSurvivors: false,
1381
1554
  sourceFile
@@ -1515,6 +1688,8 @@ const bamboocss = (options = {}) => {
1515
1688
  return [...modules, ...added];
1516
1689
  };
1517
1690
  let ctx;
1691
+ /** The compiler's private parse sink for `ctx`. @see `CompilationGeneration.encoder` */
1692
+ let parseEncoder;
1518
1693
  let foldSourceImpl;
1519
1694
  let verifyExportReadsImpl;
1520
1695
  let runtimeCss;
@@ -1554,25 +1729,97 @@ const bamboocss = (options = {}) => {
1554
1729
  dependencies: expanded
1555
1730
  };
1556
1731
  };
1557
- const loadContext = createRetryableLazy(async () => {
1558
- const { loadConfigAndCreateContext } = await loadNodeModule();
1559
- return loadConfigAndCreateContext({
1560
- configPath,
1561
- cwd,
1562
- dev: command === "serve"
1563
- });
1564
- });
1565
- const loadCompilerState = createLazyCompilerState(loadContext, loadFoldModule);
1732
+ /** Which context the published derivations below were built from. */
1733
+ let derivedGeneration = -1;
1734
+ /** Compiler-only sibling ASTs retained for each physical module. */
1735
+ const auxiliarySourcesByFile = /* @__PURE__ */ new Map();
1736
+ /**
1737
+ * Whether the compiler state below still describes the context the host is on.
1738
+ *
1739
+ * Only `ensureCompilerState` re-derives, and only an awaited hook may call it — so the two
1740
+ * synchronous entry points, the speculative prefold and the unchanged-dependent check, can
1741
+ * be reached after a stylesheet pass has published a config reload they have not seen. Both
1742
+ * decline rather than fold against a runtime `css` from the previous config.
1743
+ */
1744
+ const compilerStateIsCurrent = () => {
1745
+ const current = host.current();
1746
+ return current !== void 0 && current.id === derivedGeneration;
1747
+ };
1566
1748
  const ensureContext = async () => {
1567
- ctx = await loadContext();
1749
+ ctx = (await host.ensureGeneration()).context;
1568
1750
  };
1751
+ /**
1752
+ * Load the fold chunk and derive everything that depends on the resolved context.
1753
+ *
1754
+ * Keyed on context *identity* rather than derived once. `Builder.setup` replaces its context
1755
+ * on a config reload, and the runtime `css`, the style-set compiler and the parse sink are
1756
+ * all closures over the previous one — a stale `runtimeCss` names classes from the old
1757
+ * config while the stylesheet is emitted from the new one, and nothing downstream can see
1758
+ * the difference. Re-derivation is cheap; both factories are a handful of bound methods.
1759
+ *
1760
+ * Published as a set, and only once every part of the attempt has succeeded, so a failed
1761
+ * chunk load leaves no half-compiler visible to HMR.
1762
+ */
1569
1763
  const ensureCompilerState = async () => {
1570
- const loaded = await loadCompilerState();
1571
- ctx = loaded.context;
1572
- foldSourceImpl = loaded.foldSource;
1573
- verifyExportReadsImpl = loaded.verifyExportReads;
1574
- runtimeCss = loaded.runtimeCss;
1575
- styleCompiler = loaded.styleCompiler;
1764
+ const [initialGeneration, fold] = await Promise.all([host.ensureGeneration(), loadFoldModule()]);
1765
+ const currentGeneration = await host.ensureGeneration();
1766
+ const generation = currentGeneration.id === initialGeneration.id ? initialGeneration : currentGeneration;
1767
+ if (derivedGeneration === generation.id && foldSourceImpl) {
1768
+ ctx = generation.context;
1769
+ return;
1770
+ }
1771
+ const derivedRuntimeCss = fold.createRuntimeCss(generation.context);
1772
+ const derivedStyleCompiler = fold.createStaticStyleSetCompiler(generation.context, derivedRuntimeCss);
1773
+ ctx = generation.context;
1774
+ parseEncoder = generation.encoder;
1775
+ foldSourceImpl = fold.foldSource;
1776
+ verifyExportReadsImpl = fold.verifyExportReads;
1777
+ runtimeCss = derivedRuntimeCss;
1778
+ styleCompiler = derivedStyleCompiler;
1779
+ derivedGeneration = generation.id;
1780
+ auxiliarySourcesByFile.clear();
1781
+ };
1782
+ /**
1783
+ * Parse a module for the compiler, never for the stylesheet.
1784
+ *
1785
+ * Every compiler parse goes through here so the private encoder cannot be forgotten at one
1786
+ * call site. Forgetting it at any of them puts that module's reading into the encoder the
1787
+ * sheet is emitted from, under a `parse` owner nothing retracts.
1788
+ */
1789
+ const parseForCompiler = (filePath, hookFilePath = filePath) => ctx?.project.parseSourceFile(filePath, parseEncoder, { hookFilePath });
1790
+ /**
1791
+ * Where the compiler may hold `code` for `filePath` without displacing the checkout.
1792
+ *
1793
+ * The file's own path exactly when the shared Project already holds these bytes — then
1794
+ * `addSourceFile` is a lookup and there is nothing to displace. @see `auxiliaryParsePath`
1795
+ */
1796
+ const compilerSourcePath = (filePath, requested, code) => {
1797
+ if (requested !== filePath) return requested;
1798
+ return ctx?.project.getSourceFile(filePath)?.getFullText() === code ? filePath : auxiliaryParsePath(filePath);
1799
+ };
1800
+ /** Add one compiler-owned source without letting it displace or outlive its physical file. */
1801
+ const addCompilerSource = (filePath, parsePath, code) => {
1802
+ if (!ctx) return;
1803
+ const auxiliary = parsePath !== filePath;
1804
+ const sourceFile = ctx.project.addSourceFile(parsePath, code, { auxiliary });
1805
+ if (auxiliary) {
1806
+ const physical = normalizeFsPath(filePath);
1807
+ const paths = auxiliarySourcesByFile.get(physical) ?? /* @__PURE__ */ new Set();
1808
+ paths.add(parsePath);
1809
+ auxiliarySourcesByFile.set(physical, paths);
1810
+ }
1811
+ return sourceFile;
1812
+ };
1813
+ /** Release compiler encoder owners and sibling ASTs when their physical module disappears. */
1814
+ const releaseCompilerSources = (filePath) => {
1815
+ if (!ctx) return;
1816
+ parseEncoder?.releaseFile(filePath);
1817
+ const physical = normalizeFsPath(filePath);
1818
+ for (const auxiliary of auxiliarySourcesByFile.get(physical) ?? []) {
1819
+ parseEncoder?.releaseFile(auxiliary);
1820
+ ctx.project.removeSourceFile(auxiliary);
1821
+ }
1822
+ auxiliarySourcesByFile.delete(physical);
1576
1823
  };
1577
1824
  const outputFinalizerTag = (value) => {
1578
1825
  if (!value || typeof value !== "object") return void 0;
@@ -1689,6 +1936,7 @@ const bamboocss = (options = {}) => {
1689
1936
  },
1690
1937
  configResolved(config) {
1691
1938
  command = config.command;
1939
+ host.setCommand(config.command);
1692
1940
  defaultEmitAssets = config.build?.emitAssets ?? (!config.build?.ssr || config.build?.ssrEmitAssets === true);
1693
1941
  const plugins = config.plugins;
1694
1942
  if (plugins) {
@@ -1738,14 +1986,15 @@ const bamboocss = (options = {}) => {
1738
1986
  state.unchangedFolds.clear();
1739
1987
  state.changedRun = 0;
1740
1988
  }
1741
- if (!ctx) return;
1742
- if (!shouldTransform(id)) return;
1743
1989
  const [filePath] = id.split("?");
1744
1990
  if (!filePath) return;
1991
+ if (change.event === "update") host.noteSourceChange(filePath, change.event);
1992
+ if (!ctx) return;
1993
+ if (!shouldTransform(id)) return;
1745
1994
  for (const state of transformStateByEnvironment.values()) state.recipeConfigCache.clear();
1746
- if (SFC_EXTENSIONS.test(filePath)) return;
1747
1995
  if (change.event === "delete") {
1748
- ctx.project.removeSourceFile(filePath);
1996
+ host.removeSource(filePath);
1997
+ releaseCompilerSources(filePath);
1749
1998
  const deleted = normalizeFsPath(filePath);
1750
1999
  for (const state of transformStateByEnvironment.values()) for (const [moduleId, moduleFile] of [...state.filesByModule]) {
1751
2000
  if (normalizeFsPath(moduleFile) !== deleted) continue;
@@ -1757,7 +2006,8 @@ const bamboocss = (options = {}) => {
1757
2006
  }
1758
2007
  return;
1759
2008
  }
1760
- ctx.project.reloadSourceFile(filePath);
2009
+ if (SFC_EXTENSIONS.test(filePath)) return;
2010
+ host.reloadSource(filePath);
1761
2011
  /**
1762
2012
  * Fold the edited file before the browser asks for it.
1763
2013
  *
@@ -1776,12 +2026,14 @@ const bamboocss = (options = {}) => {
1776
2026
  */
1777
2027
  if (command === "serve") setImmediate(() => {
1778
2028
  if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return;
2029
+ if (host.isCssPassActive() || !compilerStateIsCurrent()) return;
1779
2030
  try {
1780
2031
  const code = readFileSync(filePath, "utf8");
1781
2032
  const memoKey = foldMemoKey(filePath, digest(code));
1782
2033
  if (foldMemoByContent.has(memoKey)) return;
1783
- const sourceFile = ctx.project.addSourceFile(filePath, code);
1784
- const parserResult = ctx.project.parseSourceFile(filePath);
2034
+ const sourceFile = addCompilerSource(filePath, filePath, code);
2035
+ if (!sourceFile) return;
2036
+ const parserResult = parseForCompiler(filePath);
1785
2037
  if (!parserResult) return;
1786
2038
  const folded = foldSourceImpl({
1787
2039
  ctx,
@@ -1791,7 +2043,7 @@ const bamboocss = (options = {}) => {
1791
2043
  runtimeCss,
1792
2044
  styleCompiler,
1793
2045
  maxRecipeStates,
1794
- parseModule: (path) => ctx?.project.parseSourceFile(path),
2046
+ parseModule: parseForCompiler,
1795
2047
  recipeConfigCache: transformStateByEnvironment.get("client")?.recipeConfigCache ?? /* @__PURE__ */ new Map(),
1796
2048
  reportSurvivors: true,
1797
2049
  sourceFile
@@ -1919,8 +2171,8 @@ const bamboocss = (options = {}) => {
1919
2171
  if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return null;
1920
2172
  const [filePath] = id.split("?");
1921
2173
  if (isGeneratedOutput(filePath, ctx)) return null;
1922
- const parsePath = compilerParsePath(id, code);
1923
- if (parsePath === null) return null;
2174
+ const requestedParsePath = compilerParsePath(id, code);
2175
+ if (requestedParsePath === null) return null;
1924
2176
  const state = environmentState(this);
1925
2177
  state.transformedModulesThisRun.add(id);
1926
2178
  let inputDigest;
@@ -1928,46 +2180,62 @@ const bamboocss = (options = {}) => {
1928
2180
  const previousDependencies = previousSignature && previousSignature.input === (inputDigest ??= digest(code)) ? state.dependenciesByModule.get(id) : void 0;
1929
2181
  let result;
1930
2182
  try {
1931
- const memoKey = command === "serve" ? foldMemoKey(parsePath, inputDigest ??= digest(code)) : void 0;
1932
- const memoized = memoKey ? foldMemoByContent.get(memoKey) : void 0;
1933
- let valueReads = [];
1934
- if (memoized?.reportedSurvivors) {
1935
- valueReads = memoized.valueReads;
1936
- result = withResolutionClosure(parsePath, memoized.result, memoized.parserDependencies, previousDependencies);
1937
- } else {
1938
- const sourceFile = ctx.project.addSourceFile(parsePath, code);
1939
- const parserResult = ctx.project.parseSourceFile(parsePath);
1940
- if (!parserResult) {
1941
- state.transformArtifactsByModule.delete(id);
1942
- recordFoldDependencies(state, id, filePath, []);
1943
- state.foldSignatures.delete(id);
1944
- state.foldInputsByModule.delete(id);
1945
- return null;
1946
- }
2183
+ /**
2184
+ * One serialized region, holding every read and every mutation of the shared AST.
2185
+ *
2186
+ * Synchronous throughout, which is what makes waiting for the stylesheet pass once at
2187
+ * the top sufficient: nothing can open a pass between the wait and the work, because
2188
+ * nothing else runs. The fold is CPU-bound anyway, so there is no await to give up.
2189
+ */
2190
+ const compiled = await host.runCompilerWork(() => {
2191
+ if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return null;
2192
+ const path = compilerSourcePath(filePath, requestedParsePath, code);
2193
+ const memoKey = command === "serve" ? foldMemoKey(path, inputDigest ??= digest(code)) : void 0;
2194
+ const memoized = memoKey ? foldMemoByContent.get(memoKey) : void 0;
2195
+ if (memoized?.reportedSurvivors) return {
2196
+ valueReads: memoized.valueReads,
2197
+ result: withResolutionClosure(path, memoized.result, memoized.parserDependencies, previousDependencies)
2198
+ };
2199
+ const sourceFile = addCompilerSource(filePath, path, code);
2200
+ if (!sourceFile) return null;
2201
+ const parserResult = parseForCompiler(path, requestedParsePath === filePath ? filePath : path);
2202
+ if (!parserResult) return { unparsed: true };
1947
2203
  const folded = foldSourceImpl({
1948
2204
  ctx,
1949
2205
  code,
1950
2206
  parserResult,
1951
- filePath: parsePath,
2207
+ filePath: path,
1952
2208
  runtimeCss,
1953
2209
  styleCompiler,
1954
2210
  maxRecipeStates,
1955
- parseModule: (path) => ctx?.project.parseSourceFile(path),
2211
+ parseModule: parseForCompiler,
1956
2212
  recipeConfigCache: state.recipeConfigCache,
1957
2213
  reportSurvivors: true,
1958
2214
  sourceFile
1959
2215
  });
1960
2216
  const parserDependencies = parserResult.getDependencies();
1961
- valueReads = parserResult.getExportReads?.() ?? [];
2217
+ const valueReads = parserResult.getExportReads?.() ?? [];
1962
2218
  if (memoKey) foldMemoByContent.set(memoKey, {
1963
2219
  result: folded,
1964
2220
  parserDependencies,
1965
2221
  valueReads,
1966
2222
  reportedSurvivors: true
1967
2223
  });
1968
- result = withResolutionClosure(parsePath, folded, parserDependencies, previousDependencies);
2224
+ return {
2225
+ valueReads,
2226
+ result: withResolutionClosure(path, folded, parserDependencies, previousDependencies)
2227
+ };
2228
+ });
2229
+ if (!compiled) return null;
2230
+ if ("unparsed" in compiled) {
2231
+ state.transformArtifactsByModule.delete(id);
2232
+ recordFoldDependencies(state, id, filePath, []);
2233
+ state.foldSignatures.delete(id);
2234
+ state.foldInputsByModule.delete(id);
2235
+ return null;
1969
2236
  }
1970
- state.exportReadsByModule.set(id, [...valueReads.map((read) => ({
2237
+ result = compiled.result;
2238
+ state.exportReadsByModule.set(id, [...compiled.valueReads.map((read) => ({
1971
2239
  kind: "value",
1972
2240
  ...read
1973
2241
  })), ...result.exportReads]);
@@ -2023,10 +2291,10 @@ const bamboocss = (options = {}) => {
2023
2291
  } } : {}
2024
2292
  });
2025
2293
  applyTransformArtifact(state, artifact, id, environmentName(this));
2026
- if (artifact.signature && (command === "serve" || parsePath !== filePath)) state.foldInputsByModule.set(id, {
2294
+ if (artifact.signature && (command === "serve" || requestedParsePath !== filePath)) state.foldInputsByModule.set(id, {
2027
2295
  code,
2028
2296
  input: artifact.signature.input,
2029
- parsePath
2297
+ parsePath: requestedParsePath
2030
2298
  });
2031
2299
  else state.foldInputsByModule.delete(id);
2032
2300
  if (reportSkipped && result.skipped.length) logger.info("vite:transform", formatSkipped(filePath, result.skipped));
@@ -2088,6 +2356,7 @@ const bamboocss = (options = {}) => {
2088
2356
  bamboocssCss({
2089
2357
  configPath,
2090
2358
  cwd,
2359
+ host,
2091
2360
  session: staticSession,
2092
2361
  pruneCss
2093
2362
  }),