@bamboocss/vite 1.37.12 → 1.37.13

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.
Files changed (3) hide show
  1. package/dist/index.cjs +122 -39
  2. package/dist/index.mjs +122 -39
  3. package/package.json +9 -9
package/dist/index.cjs CHANGED
@@ -94,6 +94,7 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
94
94
  if (!className || !prunable.has(className) || used.has(className)) return;
95
95
  candidate.remove();
96
96
  removedAny = true;
97
+ session.prunedClasses.add(className);
97
98
  });
98
99
  }).processSync(rule.selector);
99
100
  } catch {
@@ -154,6 +155,56 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
154
155
  return root.toString();
155
156
  };
156
157
  //#endregion
158
+ //#region src/static-session.ts
159
+ const createStaticCompilationSession = () => {
160
+ const session = {
161
+ utilityLayer: "utilities",
162
+ sourcemap: false,
163
+ cssLoaded: false,
164
+ transformedFiles: /* @__PURE__ */ new Set(),
165
+ extractedFiles: /* @__PURE__ */ new Set(),
166
+ prunableClasses: /* @__PURE__ */ new Set(),
167
+ viewTransitionClasses: /* @__PURE__ */ new Set(),
168
+ usedClasses: /* @__PURE__ */ new Set(),
169
+ expectedEnvironments: void 0,
170
+ startedEnvironments: /* @__PURE__ */ new Set(),
171
+ prunedClasses: /* @__PURE__ */ new Set(),
172
+ markClassUsed(className) {
173
+ for (const token of className.split(" ")) {
174
+ if (!token) continue;
175
+ session.usedClasses.add(token.includes("\\") ? token : (0, _bamboocss_shared.esc)(token));
176
+ }
177
+ }
178
+ };
179
+ return session;
180
+ };
181
+ /**
182
+ * Environments this run intends to build that have not been compiled yet.
183
+ *
184
+ * Empty means everything the run will contribute has been contributed, which is the condition
185
+ * every whole-run judgement here waits for: pruning the stylesheet against reachability, and
186
+ * the two guards that ask whether the compiled modules and the extraction graph agree. Each of
187
+ * those is false about a build in progress and true only about a finished one.
188
+ *
189
+ * Empty is also the answer for a single-environment build, where nothing announced an
190
+ * environment list because there is only ever one — so that path is unchanged.
191
+ *
192
+ * An environment a run declares and then never builds leaves this permanently non-empty, and
193
+ * those judgements are skipped for the run. Every one of them errs towards shipping more CSS
194
+ * or asserting less, so that is the safe direction to be wrong in.
195
+ */
196
+ const remainingEnvironments = (session) => [...session.expectedEnvironments ?? []].filter((name) => !session.startedEnvironments.has(name));
197
+ const resetStaticCompilationSession = (session) => {
198
+ session.cssLoaded = false;
199
+ session.transformedFiles.clear();
200
+ session.extractedFiles.clear();
201
+ session.prunableClasses.clear();
202
+ session.viewTransitionClasses.clear();
203
+ session.usedClasses.clear();
204
+ session.startedEnvironments.clear();
205
+ session.prunedClasses.clear();
206
+ };
207
+ //#endregion
157
208
  //#region src/css.ts
158
209
  /**
159
210
  * What a project imports to get the stylesheet.
@@ -262,11 +313,15 @@ const carriesGeneratedCss = (output) => output.type === "asset" && output.fileNa
262
313
  * hash is not cosmetic: it makes late graph reachability cache-safe.
263
314
  */
264
315
  const optimizeStaticCssAssets = (bundle, session, options = {}) => {
265
- const { rename = true } = options;
316
+ const { rename = true, prune = true, sourcemap = session.sourcemap } = options;
317
+ /** Assets in this bundle that carry the generated stylesheet, pruned or not. */
318
+ let sheets = 0;
266
319
  for (const output of Object.values(bundle)) {
267
320
  if (!carriesGeneratedCss(output)) continue;
268
321
  const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString();
269
322
  if (!source.includes("--made-with-bamboo")) continue;
323
+ sheets++;
324
+ if (!prune) continue;
270
325
  const optimized = pruneStaticCss(source, session);
271
326
  output.source = optimized;
272
327
  if (optimized === source) continue;
@@ -279,8 +334,9 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
279
334
  if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
280
335
  const previous = output.fileName;
281
336
  output.fileName = nextName;
282
- replaceAssetReferences(bundle, previous, nextName, session.sourcemap);
337
+ replaceAssetReferences(bundle, previous, nextName, sourcemap);
283
338
  }
339
+ return { sheets };
284
340
  };
285
341
  /**
286
342
  * Serve bamboo's stylesheet as a virtual module, in dev and in build.
@@ -356,9 +412,32 @@ const bamboocssCss = (options) => {
356
412
  };
357
413
  return {
358
414
  name: "bamboocss:css",
415
+ /**
416
+ * One instance for every environment of a build, rather than one per environment.
417
+ *
418
+ * Vite re-reads the config file once per environment, so a project that lists this plugin
419
+ * in `vite.config.ts` — every project — got a *fresh* instance per environment, each with
420
+ * its own compilation session, context and ts-morph project. Nothing an environment
421
+ * established could then be seen by the next one, which is the premise the reachability
422
+ * accounting below is built on, and it also meant the whole config load and extraction
423
+ * happened once per environment.
424
+ */
425
+ sharedDuringBuild: true,
359
426
  configResolved(config) {
360
427
  command = config.command;
361
428
  session.sourcemap = config.build.sourcemap;
429
+ if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
430
+ },
431
+ /**
432
+ * The definitive environment list, for a run that reaches `builder.buildApp()` without
433
+ * configuring `builder` — the shape `vite build` itself takes, where exactly one
434
+ * environment is set up and pruning is therefore safe.
435
+ */
436
+ buildApp: {
437
+ order: "pre",
438
+ async handler(builder) {
439
+ session.expectedEnvironments = new Set(Object.keys(builder.environments));
440
+ }
362
441
  },
363
442
  resolveId(id) {
364
443
  const query = queryOf(id);
@@ -398,8 +477,34 @@ const bamboocssCss = (options) => {
398
477
  generateBundle: {
399
478
  order: "post",
400
479
  handler(_, bundle) {
401
- optimizeStaticCssAssets(bundle, session, { rename: renameCssAsset });
402
- if (!servedEnvironments.has(this.environment?.name ?? "default")) return;
480
+ const environment = this.environment;
481
+ /**
482
+ * Every environment of this run has already had its modules compiled.
483
+ *
484
+ * Reachability is what pruning removes rules against, and it is only complete once
485
+ * nothing is left to contribute to it. The stylesheet is emitted and finalized by the
486
+ * environment that *imports* it, which in an SSR app is the client — and the client
487
+ * builds first, before the server environment has transformed a single module. Pruning
488
+ * there deletes every rule for a class only the server graph reaches, and the pages
489
+ * link the pruned copy: one project lost 39% of its atoms that way, presenting as
490
+ * rarely-used classes such as `md:{display:inline-block}` silently not applying.
491
+ *
492
+ * So the full extracted stylesheet ships instead, exactly as `renameCssAsset: false`
493
+ * already does. Being the last environment is not the common case — frameworks build
494
+ * the client first — but it is the only one where the answer is knowable, and a
495
+ * framework that builds its server bundle first does get pruned output.
496
+ */
497
+ const pending = remainingEnvironments(session);
498
+ const { sheets } = optimizeStaticCssAssets(bundle, session, {
499
+ rename: renameCssAsset,
500
+ prune: pending.length === 0,
501
+ sourcemap: environment?.config?.build?.sourcemap
502
+ });
503
+ if (sheets && pending.length) _bamboocss_logger.logger.info("vite", `Reachability pruning skipped: the stylesheet is emitted by the ${JSON.stringify(environment?.name ?? "default")} environment, and ${(0, _bamboocss_shared.truncateList)(pending, {
504
+ unit: "environment",
505
+ separator: ", "
506
+ })} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
507
+ if (!servedEnvironments.has(environment?.name ?? "default")) return;
403
508
  if (!session.transformedFiles.size) return;
404
509
  if (!Object.values(bundle).some((output) => {
405
510
  if (!carriesGeneratedCss(output)) return false;
@@ -2784,35 +2889,6 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
2784
2889
  };
2785
2890
  };
2786
2891
  //#endregion
2787
- //#region src/static-session.ts
2788
- const createStaticCompilationSession = () => {
2789
- const session = {
2790
- utilityLayer: "utilities",
2791
- sourcemap: false,
2792
- cssLoaded: false,
2793
- transformedFiles: /* @__PURE__ */ new Set(),
2794
- extractedFiles: /* @__PURE__ */ new Set(),
2795
- prunableClasses: /* @__PURE__ */ new Set(),
2796
- viewTransitionClasses: /* @__PURE__ */ new Set(),
2797
- usedClasses: /* @__PURE__ */ new Set(),
2798
- markClassUsed(className) {
2799
- for (const token of className.split(" ")) {
2800
- if (!token) continue;
2801
- session.usedClasses.add(token.includes("\\") ? token : (0, _bamboocss_shared.esc)(token));
2802
- }
2803
- }
2804
- };
2805
- return session;
2806
- };
2807
- const resetStaticCompilationSession = (session) => {
2808
- session.cssLoaded = false;
2809
- session.transformedFiles.clear();
2810
- session.extractedFiles.clear();
2811
- session.prunableClasses.clear();
2812
- session.viewTransitionClasses.clear();
2813
- session.usedClasses.clear();
2814
- };
2815
- //#endregion
2816
2892
  //#region src/plugin.ts
2817
2893
  const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
2818
2894
  const NODE_MODULES = /node_modules/;
@@ -2866,8 +2942,6 @@ const formatSkipped = (id, skipped) => {
2866
2942
  const bamboocss = (options = {}) => {
2867
2943
  const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, renameCssAsset = true } = options;
2868
2944
  if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
2869
- /** Environments whose `buildStart` has run in the build currently in progress. */
2870
- const seenEnvironments = /* @__PURE__ */ new Set();
2871
2945
  /** Totals across the build, for the summary. */
2872
2946
  const totals = {
2873
2947
  folded: 0,
@@ -2941,13 +3015,14 @@ const bamboocss = (options = {}) => {
2941
3015
  }), {
2942
3016
  name: "bamboocss:compiler",
2943
3017
  enforce: "pre",
3018
+ /** See the same declaration on the css plugin: one instance per build, not per environment. */
3019
+ sharedDuringBuild: true,
2944
3020
  configResolved(config) {
2945
3021
  command = config.command;
2946
3022
  },
2947
3023
  async buildStart() {
2948
3024
  const environment = this.environment?.name ?? "default";
2949
- if (seenEnvironments.has(environment)) {
2950
- seenEnvironments.clear();
3025
+ if (staticSession.startedEnvironments.has(environment)) {
2951
3026
  totals.folded = 0;
2952
3027
  totals.files = 0;
2953
3028
  totals.filesWithFolds = 0;
@@ -2956,7 +3031,7 @@ const bamboocss = (options = {}) => {
2956
3031
  recipeConfigCache.clear();
2957
3032
  resetStaticCompilationSession(staticSession);
2958
3033
  }
2959
- seenEnvironments.add(environment);
3034
+ staticSession.startedEnvironments.add(environment);
2960
3035
  try {
2961
3036
  await ensureContext();
2962
3037
  } catch (error) {
@@ -3067,7 +3142,15 @@ const bamboocss = (options = {}) => {
3067
3142
  buildEnd() {
3068
3143
  const survivors = allSurvivors();
3069
3144
  if (survivors.length) throw createSurvivorError(survivors);
3070
- if (typeof this.getModuleInfo === "function") {
3145
+ const lost = [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(bare(className)));
3146
+ if (lost.length) {
3147
+ const environment = this.environment?.name ?? "default";
3148
+ throw new Error(`bamboocss: ${lost.length} class(es) compiled in the ${JSON.stringify(environment)} environment were already pruned out of a stylesheet emitted by an earlier one. Elements carrying them would render unstyled.\n\n${(0, _bamboocss_shared.truncateList)(lost.map((className) => ` ${className}`), {
3149
+ unit: "class",
3150
+ separator: "\n"
3151
+ })}\n\nThe stylesheet is finalized by the environment that imports it, so pruning it is only safe once every environment has been compiled. This build did not say how many there would be: it called \`builder.build(environment)\` directly. Run it through \`vite build\`, call \`builder.buildApp()\`, or set \`builder: {}\` in the Vite config so the environments are known before the first one builds. \`bamboocss({ renameCssAsset: false })\` also turns pruning off entirely.`);
3152
+ }
3153
+ if (typeof this.getModuleInfo === "function" && !remainingEnvironments(staticSession).length) {
3071
3154
  if (!staticSession.cssLoaded) throw new Error(`bamboocss: compiled class values were produced, but ${JSON.stringify(VIRTUAL_CSS_ID)} was not imported. Add \`import ${JSON.stringify(VIRTUAL_CSS_ID)}\` once, from a JavaScript or TypeScript module in the application entry graph.\n\nIt has to be a JS import. \`@import\` from a stylesheet does not reach it: the id names a virtual module resolved by this plugin, and Vite resolves CSS \`@import\` before plugin resolution, so it fails as an unresolvable path. A project that ships one preloaded stylesheet imports this from its entry module instead, and lets Vite emit the CSS asset.`);
3072
3155
  const outsideExtraction = [...staticSession.transformedFiles].filter((file) => !staticSession.extractedFiles.has(file));
3073
3156
  if (outsideExtraction.length) throw new Error(`bamboocss: ${outsideExtraction.length} statically compiled module(s) are outside the CSS extraction graph:\n\n${(0, _bamboocss_shared.truncateList)(outsideExtraction.map((file) => ` ${file}`), {
package/dist/index.mjs CHANGED
@@ -64,6 +64,7 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
64
64
  if (!className || !prunable.has(className) || used.has(className)) return;
65
65
  candidate.remove();
66
66
  removedAny = true;
67
+ session.prunedClasses.add(className);
67
68
  });
68
69
  }).processSync(rule.selector);
69
70
  } catch {
@@ -124,6 +125,56 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
124
125
  return root.toString();
125
126
  };
126
127
  //#endregion
128
+ //#region src/static-session.ts
129
+ const createStaticCompilationSession = () => {
130
+ const session = {
131
+ utilityLayer: "utilities",
132
+ sourcemap: false,
133
+ cssLoaded: false,
134
+ transformedFiles: /* @__PURE__ */ new Set(),
135
+ extractedFiles: /* @__PURE__ */ new Set(),
136
+ prunableClasses: /* @__PURE__ */ new Set(),
137
+ viewTransitionClasses: /* @__PURE__ */ new Set(),
138
+ usedClasses: /* @__PURE__ */ new Set(),
139
+ expectedEnvironments: void 0,
140
+ startedEnvironments: /* @__PURE__ */ new Set(),
141
+ prunedClasses: /* @__PURE__ */ new Set(),
142
+ markClassUsed(className) {
143
+ for (const token of className.split(" ")) {
144
+ if (!token) continue;
145
+ session.usedClasses.add(token.includes("\\") ? token : esc(token));
146
+ }
147
+ }
148
+ };
149
+ return session;
150
+ };
151
+ /**
152
+ * Environments this run intends to build that have not been compiled yet.
153
+ *
154
+ * Empty means everything the run will contribute has been contributed, which is the condition
155
+ * every whole-run judgement here waits for: pruning the stylesheet against reachability, and
156
+ * the two guards that ask whether the compiled modules and the extraction graph agree. Each of
157
+ * those is false about a build in progress and true only about a finished one.
158
+ *
159
+ * Empty is also the answer for a single-environment build, where nothing announced an
160
+ * environment list because there is only ever one — so that path is unchanged.
161
+ *
162
+ * An environment a run declares and then never builds leaves this permanently non-empty, and
163
+ * those judgements are skipped for the run. Every one of them errs towards shipping more CSS
164
+ * or asserting less, so that is the safe direction to be wrong in.
165
+ */
166
+ const remainingEnvironments = (session) => [...session.expectedEnvironments ?? []].filter((name) => !session.startedEnvironments.has(name));
167
+ const resetStaticCompilationSession = (session) => {
168
+ session.cssLoaded = false;
169
+ session.transformedFiles.clear();
170
+ session.extractedFiles.clear();
171
+ session.prunableClasses.clear();
172
+ session.viewTransitionClasses.clear();
173
+ session.usedClasses.clear();
174
+ session.startedEnvironments.clear();
175
+ session.prunedClasses.clear();
176
+ };
177
+ //#endregion
127
178
  //#region src/css.ts
128
179
  /**
129
180
  * What a project imports to get the stylesheet.
@@ -232,11 +283,15 @@ const carriesGeneratedCss = (output) => output.type === "asset" && output.fileNa
232
283
  * hash is not cosmetic: it makes late graph reachability cache-safe.
233
284
  */
234
285
  const optimizeStaticCssAssets = (bundle, session, options = {}) => {
235
- const { rename = true } = options;
286
+ const { rename = true, prune = true, sourcemap = session.sourcemap } = options;
287
+ /** Assets in this bundle that carry the generated stylesheet, pruned or not. */
288
+ let sheets = 0;
236
289
  for (const output of Object.values(bundle)) {
237
290
  if (!carriesGeneratedCss(output)) continue;
238
291
  const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString();
239
292
  if (!source.includes("--made-with-bamboo")) continue;
293
+ sheets++;
294
+ if (!prune) continue;
240
295
  const optimized = pruneStaticCss(source, session);
241
296
  output.source = optimized;
242
297
  if (optimized === source) continue;
@@ -249,8 +304,9 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
249
304
  if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
250
305
  const previous = output.fileName;
251
306
  output.fileName = nextName;
252
- replaceAssetReferences(bundle, previous, nextName, session.sourcemap);
307
+ replaceAssetReferences(bundle, previous, nextName, sourcemap);
253
308
  }
309
+ return { sheets };
254
310
  };
255
311
  /**
256
312
  * Serve bamboo's stylesheet as a virtual module, in dev and in build.
@@ -326,9 +382,32 @@ const bamboocssCss = (options) => {
326
382
  };
327
383
  return {
328
384
  name: "bamboocss:css",
385
+ /**
386
+ * One instance for every environment of a build, rather than one per environment.
387
+ *
388
+ * Vite re-reads the config file once per environment, so a project that lists this plugin
389
+ * in `vite.config.ts` — every project — got a *fresh* instance per environment, each with
390
+ * its own compilation session, context and ts-morph project. Nothing an environment
391
+ * established could then be seen by the next one, which is the premise the reachability
392
+ * accounting below is built on, and it also meant the whole config load and extraction
393
+ * happened once per environment.
394
+ */
395
+ sharedDuringBuild: true,
329
396
  configResolved(config) {
330
397
  command = config.command;
331
398
  session.sourcemap = config.build.sourcemap;
399
+ if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
400
+ },
401
+ /**
402
+ * The definitive environment list, for a run that reaches `builder.buildApp()` without
403
+ * configuring `builder` — the shape `vite build` itself takes, where exactly one
404
+ * environment is set up and pruning is therefore safe.
405
+ */
406
+ buildApp: {
407
+ order: "pre",
408
+ async handler(builder) {
409
+ session.expectedEnvironments = new Set(Object.keys(builder.environments));
410
+ }
332
411
  },
333
412
  resolveId(id) {
334
413
  const query = queryOf(id);
@@ -368,8 +447,34 @@ const bamboocssCss = (options) => {
368
447
  generateBundle: {
369
448
  order: "post",
370
449
  handler(_, bundle) {
371
- optimizeStaticCssAssets(bundle, session, { rename: renameCssAsset });
372
- if (!servedEnvironments.has(this.environment?.name ?? "default")) return;
450
+ const environment = this.environment;
451
+ /**
452
+ * Every environment of this run has already had its modules compiled.
453
+ *
454
+ * Reachability is what pruning removes rules against, and it is only complete once
455
+ * nothing is left to contribute to it. The stylesheet is emitted and finalized by the
456
+ * environment that *imports* it, which in an SSR app is the client — and the client
457
+ * builds first, before the server environment has transformed a single module. Pruning
458
+ * there deletes every rule for a class only the server graph reaches, and the pages
459
+ * link the pruned copy: one project lost 39% of its atoms that way, presenting as
460
+ * rarely-used classes such as `md:{display:inline-block}` silently not applying.
461
+ *
462
+ * So the full extracted stylesheet ships instead, exactly as `renameCssAsset: false`
463
+ * already does. Being the last environment is not the common case — frameworks build
464
+ * the client first — but it is the only one where the answer is knowable, and a
465
+ * framework that builds its server bundle first does get pruned output.
466
+ */
467
+ const pending = remainingEnvironments(session);
468
+ const { sheets } = optimizeStaticCssAssets(bundle, session, {
469
+ rename: renameCssAsset,
470
+ prune: pending.length === 0,
471
+ sourcemap: environment?.config?.build?.sourcemap
472
+ });
473
+ if (sheets && pending.length) logger.info("vite", `Reachability pruning skipped: the stylesheet is emitted by the ${JSON.stringify(environment?.name ?? "default")} environment, and ${truncateList(pending, {
474
+ unit: "environment",
475
+ separator: ", "
476
+ })} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
477
+ if (!servedEnvironments.has(environment?.name ?? "default")) return;
373
478
  if (!session.transformedFiles.size) return;
374
479
  if (!Object.values(bundle).some((output) => {
375
480
  if (!carriesGeneratedCss(output)) return false;
@@ -2754,35 +2859,6 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
2754
2859
  };
2755
2860
  };
2756
2861
  //#endregion
2757
- //#region src/static-session.ts
2758
- const createStaticCompilationSession = () => {
2759
- const session = {
2760
- utilityLayer: "utilities",
2761
- sourcemap: false,
2762
- cssLoaded: false,
2763
- transformedFiles: /* @__PURE__ */ new Set(),
2764
- extractedFiles: /* @__PURE__ */ new Set(),
2765
- prunableClasses: /* @__PURE__ */ new Set(),
2766
- viewTransitionClasses: /* @__PURE__ */ new Set(),
2767
- usedClasses: /* @__PURE__ */ new Set(),
2768
- markClassUsed(className) {
2769
- for (const token of className.split(" ")) {
2770
- if (!token) continue;
2771
- session.usedClasses.add(token.includes("\\") ? token : esc(token));
2772
- }
2773
- }
2774
- };
2775
- return session;
2776
- };
2777
- const resetStaticCompilationSession = (session) => {
2778
- session.cssLoaded = false;
2779
- session.transformedFiles.clear();
2780
- session.extractedFiles.clear();
2781
- session.prunableClasses.clear();
2782
- session.viewTransitionClasses.clear();
2783
- session.usedClasses.clear();
2784
- };
2785
- //#endregion
2786
2862
  //#region src/plugin.ts
2787
2863
  const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
2788
2864
  const NODE_MODULES = /node_modules/;
@@ -2836,8 +2912,6 @@ const formatSkipped = (id, skipped) => {
2836
2912
  const bamboocss = (options = {}) => {
2837
2913
  const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, renameCssAsset = true } = options;
2838
2914
  if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
2839
- /** Environments whose `buildStart` has run in the build currently in progress. */
2840
- const seenEnvironments = /* @__PURE__ */ new Set();
2841
2915
  /** Totals across the build, for the summary. */
2842
2916
  const totals = {
2843
2917
  folded: 0,
@@ -2911,13 +2985,14 @@ const bamboocss = (options = {}) => {
2911
2985
  }), {
2912
2986
  name: "bamboocss:compiler",
2913
2987
  enforce: "pre",
2988
+ /** See the same declaration on the css plugin: one instance per build, not per environment. */
2989
+ sharedDuringBuild: true,
2914
2990
  configResolved(config) {
2915
2991
  command = config.command;
2916
2992
  },
2917
2993
  async buildStart() {
2918
2994
  const environment = this.environment?.name ?? "default";
2919
- if (seenEnvironments.has(environment)) {
2920
- seenEnvironments.clear();
2995
+ if (staticSession.startedEnvironments.has(environment)) {
2921
2996
  totals.folded = 0;
2922
2997
  totals.files = 0;
2923
2998
  totals.filesWithFolds = 0;
@@ -2926,7 +3001,7 @@ const bamboocss = (options = {}) => {
2926
3001
  recipeConfigCache.clear();
2927
3002
  resetStaticCompilationSession(staticSession);
2928
3003
  }
2929
- seenEnvironments.add(environment);
3004
+ staticSession.startedEnvironments.add(environment);
2930
3005
  try {
2931
3006
  await ensureContext();
2932
3007
  } catch (error) {
@@ -3037,7 +3112,15 @@ const bamboocss = (options = {}) => {
3037
3112
  buildEnd() {
3038
3113
  const survivors = allSurvivors();
3039
3114
  if (survivors.length) throw createSurvivorError(survivors);
3040
- if (typeof this.getModuleInfo === "function") {
3115
+ const lost = [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(bare(className)));
3116
+ if (lost.length) {
3117
+ const environment = this.environment?.name ?? "default";
3118
+ throw new Error(`bamboocss: ${lost.length} class(es) compiled in the ${JSON.stringify(environment)} environment were already pruned out of a stylesheet emitted by an earlier one. Elements carrying them would render unstyled.\n\n${truncateList(lost.map((className) => ` ${className}`), {
3119
+ unit: "class",
3120
+ separator: "\n"
3121
+ })}\n\nThe stylesheet is finalized by the environment that imports it, so pruning it is only safe once every environment has been compiled. This build did not say how many there would be: it called \`builder.build(environment)\` directly. Run it through \`vite build\`, call \`builder.buildApp()\`, or set \`builder: {}\` in the Vite config so the environments are known before the first one builds. \`bamboocss({ renameCssAsset: false })\` also turns pruning off entirely.`);
3122
+ }
3123
+ if (typeof this.getModuleInfo === "function" && !remainingEnvironments(staticSession).length) {
3041
3124
  if (!staticSession.cssLoaded) throw new Error(`bamboocss: compiled class values were produced, but ${JSON.stringify(VIRTUAL_CSS_ID)} was not imported. Add \`import ${JSON.stringify(VIRTUAL_CSS_ID)}\` once, from a JavaScript or TypeScript module in the application entry graph.\n\nIt has to be a JS import. \`@import\` from a stylesheet does not reach it: the id names a virtual module resolved by this plugin, and Vite resolves CSS \`@import\` before plugin resolution, so it fails as an unresolvable path. A project that ships one preloaded stylesheet imports this from its entry module instead, and lets Vite emit the CSS asset.`);
3042
3125
  const outsideExtraction = [...staticSession.transformedFiles].filter((file) => !staticSession.extractedFiles.has(file));
3043
3126
  if (outsideExtraction.length) throw new Error(`bamboocss: ${outsideExtraction.length} statically compiled module(s) are outside the CSS extraction graph:\n\n${truncateList(outsideExtraction.map((file) => ` ${file}`), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.37.12",
3
+ "version": "1.37.13",
4
4
  "description": "Vite integration for Bamboo CSS",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -40,18 +40,18 @@
40
40
  "postcss": "8.5.26",
41
41
  "postcss-selector-parser": "7.1.5",
42
42
  "ts-morph": "28.0.0",
43
- "@bamboocss/config": "1.37.12",
44
- "@bamboocss/core": "1.37.12",
45
- "@bamboocss/extractor": "1.37.12",
46
- "@bamboocss/logger": "1.37.12",
47
- "@bamboocss/node": "1.37.12",
48
- "@bamboocss/shared": "1.37.12",
49
- "@bamboocss/types": "1.37.12"
43
+ "@bamboocss/config": "1.37.13",
44
+ "@bamboocss/core": "1.37.13",
45
+ "@bamboocss/extractor": "1.37.13",
46
+ "@bamboocss/logger": "1.37.13",
47
+ "@bamboocss/node": "1.37.13",
48
+ "@bamboocss/types": "1.37.13",
49
+ "@bamboocss/shared": "1.37.13"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@jridgewell/trace-mapping": "^0.3.31",
53
53
  "vite": "7.2.6",
54
- "@bamboocss/fixture": "1.37.12"
54
+ "@bamboocss/fixture": "1.37.13"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": ">=5"