@bamboocss/vite 1.39.1 → 1.40.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
@@ -383,7 +383,6 @@ const bamboocssCss = (options) => {
383
383
  builder.extract();
384
384
  if (builder.context?.config.polyfill) throw new Error("bamboocss: the cascade-layer polyfill is incompatible with compiled atomic styles. The polyfill removes the utility-layer boundary required for safe atom reachability and renaming.");
385
385
  if (builder.context) {
386
- session.cssLoaded = true;
387
386
  session.utilityLayer = builder.context.config.layers?.utilities ?? "utilities";
388
387
  session.extractedFiles.clear();
389
388
  for (const file of builder.context.getFiles()) session.extractedFiles.add(builder.context.runtime.path.abs(builder.context.config.cwd, file));
@@ -413,6 +412,31 @@ const bamboocssCss = (options) => {
413
412
  pending = Promise.resolve(pending).catch(() => void 0).then(build);
414
413
  return pending;
415
414
  };
415
+ /**
416
+ * The pass that runs before Rollup resolves anything, and the sheet it produced.
417
+ *
418
+ * `emit` writes `styled-system/`, and reaching it through `load` is too late to be what a
419
+ * fresh clone needs: `load` runs when the *virtual module* is requested, and a module's
420
+ * imports are all resolved before any of them is loaded. So `app/root.tsx` importing both
421
+ * `styled-system/css` and `virtual:bamboo.css` has the first resolved while the directory is
422
+ * still absent — the client build carried on and externalised it, the ssr build failed with
423
+ * "Rolldown failed to resolve import", and `vite dev` served an error overlay. `buildStart` is
424
+ * the first hook Rollup calls, and it precedes all of that.
425
+ *
426
+ * Memoised, and consumed by the first `load` rather than regenerated for it. `buildStart` runs
427
+ * once per environment against this one shared instance, and nothing can have invalidated the
428
+ * result in between — the modules that would invalidate it have not been transformed yet.
429
+ * Regenerating would mean a second full extraction on every cold start, to produce the same
430
+ * bytes.
431
+ */
432
+ let prebuilt;
433
+ let prebuildStarted = false;
434
+ const prebuild = async () => {
435
+ if (prebuildStarted) return;
436
+ prebuildStarted = true;
437
+ prebuilt = generate();
438
+ await prebuilt;
439
+ };
416
440
  return {
417
441
  name: "bamboocss:css",
418
442
  /**
@@ -479,6 +503,20 @@ const bamboocssCss = (options) => {
479
503
  session.expectedEnvironments = new Set(Object.keys(builder.environments));
480
504
  }
481
505
  },
506
+ /**
507
+ * Put `styled-system/` on disk before anything resolves an import of it.
508
+ *
509
+ * Normalized rather than rethrown as caught, for the reason the compiler's `buildStart`
510
+ * gives: this evaluates the user's config and its hooks, and in dev anything that is not an
511
+ * object crashes Vite's error middleware instead of being reported.
512
+ */
513
+ async buildStart() {
514
+ try {
515
+ await prebuild();
516
+ } catch (error) {
517
+ throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
518
+ }
519
+ },
482
520
  resolveId(id) {
483
521
  const query = queryOf(id);
484
522
  const base = id.slice(0, id.length - query.length);
@@ -489,9 +527,12 @@ const bamboocssCss = (options) => {
489
527
  const query = queryOf(id);
490
528
  if (id.slice(0, id.length - query.length) !== RESOLVED_ID) return null;
491
529
  servedEnvironments.add(this.environment?.name ?? "default");
530
+ session.cssLoaded = true;
492
531
  let css;
493
532
  try {
494
- css = await generate();
533
+ const first = prebuilt;
534
+ prebuilt = void 0;
535
+ css = await (first ?? generate());
495
536
  } catch (error) {
496
537
  throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
497
538
  }
@@ -500,12 +541,28 @@ const bamboocssCss = (options) => {
500
541
  },
501
542
  configureServer(devServer) {
502
543
  server = devServer;
544
+ /**
545
+ * The graph the stylesheet's own module lives in, which is the one that has to reach it.
546
+ *
547
+ * `load` registers every extracted file with `addWatchFile`, and `vite:css-analysis`
548
+ * turns those into real importer edges — the virtual module ends up a direct importer of
549
+ * each file the extractor read. So an edit to any of them propagates to the stylesheet on
550
+ * Vite's own pass, in whichever environment holds that edge.
551
+ *
552
+ * The client one, because CSS is a client concern: an ssr environment never applies a
553
+ * stylesheet update, and asking whether *any* environment matched would skip the forced
554
+ * reload below for a server-only module whose styles the client still has to be told
555
+ * about. Vite 5 has one graph and no `environments`, where the question is exact.
556
+ */
557
+ const clientGraph = devServer.environments?.client?.moduleGraph ?? devServer.moduleGraph;
503
558
  const invalidate = (file) => {
504
559
  const ctx = builder.context;
505
560
  if (!ctx) return;
506
561
  if (!ctx.getFiles().some((f) => ctx.runtime.path.abs(ctx.config.cwd, f) === file)) return;
562
+ prebuilt = void 0;
507
563
  const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
508
564
  if (!mod) return;
565
+ if (clientGraph.getModulesByFile(file)?.size) return;
509
566
  server?.moduleGraph.invalidateModule(mod);
510
567
  server?.reloadModule(mod);
511
568
  _bamboocss_logger.logger.debug("vite", `styles invalidated by ${file}`);
@@ -3236,21 +3293,33 @@ const bamboocss = (options = {}) => {
3236
3293
  /**
3237
3294
  * Modules to re-transform because `file` changed, hard-invalidated on the way out.
3238
3295
  *
3239
- * Both halves are load-bearing. Invalidating drops the stale compiled result, which is the
3240
- * defect itself; returning the modules is what makes Vite propagate an update for them, so
3241
- * the client applies one rather than waiting for whatever request happens next.
3296
+ * Invalidating is the fix. It drops the stale compiled result, which is the defect itself,
3297
+ * and it has to happen here rather than being left to `updateModules`: Vite *soft*-
3298
+ * invalidates an importer that statically imports the changed module, and a soft invalidation
3299
+ * keeps the cached transform — the very place the compiled class string lives.
3300
+ *
3301
+ * Doing it here also survives another plugin filtering the list afterwards — a framework's
3302
+ * own `hotUpdate` decides what its route modules do, and the stale bytes have to go either
3303
+ * way.
3304
+ *
3305
+ * *Naming* those modules back to Vite is a different question, and the answer is almost
3306
+ * always no. `addWatchFile` makes every consumer a direct importer of the dependency in the
3307
+ * module graph, so `propagateUpdate` walks to all of them from the changed file by itself and
3308
+ * sends exactly the same update. Returning them as well does not merge into that pass: a
3309
+ * framework plugin downstream reads the list and re-drives HMR per entry — react-router's
3310
+ * `react-router-server-change-trigger-client-hmr` calls `reloadModule` once per module, in
3311
+ * both its client and its ssr pass — and each of those is a separate `updateModules`, a
3312
+ * separate `hmr update`, and a separate refetch of the whole module by the browser.
3242
3313
  *
3243
- * Invalidating here rather than leaving it to `updateModules` also survives another plugin
3244
- * filtering the list afterwards a framework's own `hotUpdate` decides what its route
3245
- * modules do, and the stale bytes have to go either way.
3314
+ * Measured on a five-route react-router app, one `css()` edit in a shared `ui.ts`: eight
3315
+ * `hmr update` messages, `root.tsx` and `dashboard.tsx` fetched five times each, 554 kB over
3316
+ * the socket for a one-line edit. Dropping the redundant half takes it to four messages and
3317
+ * 367 kB with the same modules re-transformed and the same edit applied.
3246
3318
  *
3247
- * A consumer Vite already reached costs nothing: `propagateUpdate` skips a module it has
3248
- * traversed, and every consumer of a *resolvable* dependency is one, since `addWatchFile`
3249
- * makes it an importer whether or not the import survived the fold. What is left is the
3250
- * consumer of a dependency with no module of its own, where Vite matched nothing and would
3251
- * have done nothing at all. That one can end in a page reload, which is the honest outcome:
3252
- * its compiled classes really did change, and a reload is what Vite does with any update
3253
- * nothing accepts.
3319
+ * So the list is returned only when Vite has matched nothing for the file and would otherwise
3320
+ * do nothing at all a dependency the fold read that never became a module of its own. That
3321
+ * one can end in a page reload, which is the honest outcome: its compiled classes really did
3322
+ * change, and a reload is what Vite does with any update nothing accepts.
3254
3323
  */
3255
3324
  const foldDependentModules = (file, modules, graph) => {
3256
3325
  const dependents = dependentsByDependency.get(normalizeFsPath(file));
@@ -3262,6 +3331,7 @@ const bamboocss = (options = {}) => {
3262
3331
  added.push(module);
3263
3332
  }
3264
3333
  if (!added.length) return;
3334
+ if (modules.length) return;
3265
3335
  return [...modules, ...added];
3266
3336
  };
3267
3337
  let ctx;
package/dist/index.mjs CHANGED
@@ -353,7 +353,6 @@ const bamboocssCss = (options) => {
353
353
  builder.extract();
354
354
  if (builder.context?.config.polyfill) throw new Error("bamboocss: the cascade-layer polyfill is incompatible with compiled atomic styles. The polyfill removes the utility-layer boundary required for safe atom reachability and renaming.");
355
355
  if (builder.context) {
356
- session.cssLoaded = true;
357
356
  session.utilityLayer = builder.context.config.layers?.utilities ?? "utilities";
358
357
  session.extractedFiles.clear();
359
358
  for (const file of builder.context.getFiles()) session.extractedFiles.add(builder.context.runtime.path.abs(builder.context.config.cwd, file));
@@ -383,6 +382,31 @@ const bamboocssCss = (options) => {
383
382
  pending = Promise.resolve(pending).catch(() => void 0).then(build);
384
383
  return pending;
385
384
  };
385
+ /**
386
+ * The pass that runs before Rollup resolves anything, and the sheet it produced.
387
+ *
388
+ * `emit` writes `styled-system/`, and reaching it through `load` is too late to be what a
389
+ * fresh clone needs: `load` runs when the *virtual module* is requested, and a module's
390
+ * imports are all resolved before any of them is loaded. So `app/root.tsx` importing both
391
+ * `styled-system/css` and `virtual:bamboo.css` has the first resolved while the directory is
392
+ * still absent — the client build carried on and externalised it, the ssr build failed with
393
+ * "Rolldown failed to resolve import", and `vite dev` served an error overlay. `buildStart` is
394
+ * the first hook Rollup calls, and it precedes all of that.
395
+ *
396
+ * Memoised, and consumed by the first `load` rather than regenerated for it. `buildStart` runs
397
+ * once per environment against this one shared instance, and nothing can have invalidated the
398
+ * result in between — the modules that would invalidate it have not been transformed yet.
399
+ * Regenerating would mean a second full extraction on every cold start, to produce the same
400
+ * bytes.
401
+ */
402
+ let prebuilt;
403
+ let prebuildStarted = false;
404
+ const prebuild = async () => {
405
+ if (prebuildStarted) return;
406
+ prebuildStarted = true;
407
+ prebuilt = generate();
408
+ await prebuilt;
409
+ };
386
410
  return {
387
411
  name: "bamboocss:css",
388
412
  /**
@@ -449,6 +473,20 @@ const bamboocssCss = (options) => {
449
473
  session.expectedEnvironments = new Set(Object.keys(builder.environments));
450
474
  }
451
475
  },
476
+ /**
477
+ * Put `styled-system/` on disk before anything resolves an import of it.
478
+ *
479
+ * Normalized rather than rethrown as caught, for the reason the compiler's `buildStart`
480
+ * gives: this evaluates the user's config and its hooks, and in dev anything that is not an
481
+ * object crashes Vite's error middleware instead of being reported.
482
+ */
483
+ async buildStart() {
484
+ try {
485
+ await prebuild();
486
+ } catch (error) {
487
+ throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
488
+ }
489
+ },
452
490
  resolveId(id) {
453
491
  const query = queryOf(id);
454
492
  const base = id.slice(0, id.length - query.length);
@@ -459,9 +497,12 @@ const bamboocssCss = (options) => {
459
497
  const query = queryOf(id);
460
498
  if (id.slice(0, id.length - query.length) !== RESOLVED_ID) return null;
461
499
  servedEnvironments.add(this.environment?.name ?? "default");
500
+ session.cssLoaded = true;
462
501
  let css;
463
502
  try {
464
- css = await generate();
503
+ const first = prebuilt;
504
+ prebuilt = void 0;
505
+ css = await (first ?? generate());
465
506
  } catch (error) {
466
507
  throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
467
508
  }
@@ -470,12 +511,28 @@ const bamboocssCss = (options) => {
470
511
  },
471
512
  configureServer(devServer) {
472
513
  server = devServer;
514
+ /**
515
+ * The graph the stylesheet's own module lives in, which is the one that has to reach it.
516
+ *
517
+ * `load` registers every extracted file with `addWatchFile`, and `vite:css-analysis`
518
+ * turns those into real importer edges — the virtual module ends up a direct importer of
519
+ * each file the extractor read. So an edit to any of them propagates to the stylesheet on
520
+ * Vite's own pass, in whichever environment holds that edge.
521
+ *
522
+ * The client one, because CSS is a client concern: an ssr environment never applies a
523
+ * stylesheet update, and asking whether *any* environment matched would skip the forced
524
+ * reload below for a server-only module whose styles the client still has to be told
525
+ * about. Vite 5 has one graph and no `environments`, where the question is exact.
526
+ */
527
+ const clientGraph = devServer.environments?.client?.moduleGraph ?? devServer.moduleGraph;
473
528
  const invalidate = (file) => {
474
529
  const ctx = builder.context;
475
530
  if (!ctx) return;
476
531
  if (!ctx.getFiles().some((f) => ctx.runtime.path.abs(ctx.config.cwd, f) === file)) return;
532
+ prebuilt = void 0;
477
533
  const mod = server?.moduleGraph.getModuleById(RESOLVED_ID);
478
534
  if (!mod) return;
535
+ if (clientGraph.getModulesByFile(file)?.size) return;
479
536
  server?.moduleGraph.invalidateModule(mod);
480
537
  server?.reloadModule(mod);
481
538
  logger.debug("vite", `styles invalidated by ${file}`);
@@ -3206,21 +3263,33 @@ const bamboocss = (options = {}) => {
3206
3263
  /**
3207
3264
  * Modules to re-transform because `file` changed, hard-invalidated on the way out.
3208
3265
  *
3209
- * Both halves are load-bearing. Invalidating drops the stale compiled result, which is the
3210
- * defect itself; returning the modules is what makes Vite propagate an update for them, so
3211
- * the client applies one rather than waiting for whatever request happens next.
3266
+ * Invalidating is the fix. It drops the stale compiled result, which is the defect itself,
3267
+ * and it has to happen here rather than being left to `updateModules`: Vite *soft*-
3268
+ * invalidates an importer that statically imports the changed module, and a soft invalidation
3269
+ * keeps the cached transform — the very place the compiled class string lives.
3270
+ *
3271
+ * Doing it here also survives another plugin filtering the list afterwards — a framework's
3272
+ * own `hotUpdate` decides what its route modules do, and the stale bytes have to go either
3273
+ * way.
3274
+ *
3275
+ * *Naming* those modules back to Vite is a different question, and the answer is almost
3276
+ * always no. `addWatchFile` makes every consumer a direct importer of the dependency in the
3277
+ * module graph, so `propagateUpdate` walks to all of them from the changed file by itself and
3278
+ * sends exactly the same update. Returning them as well does not merge into that pass: a
3279
+ * framework plugin downstream reads the list and re-drives HMR per entry — react-router's
3280
+ * `react-router-server-change-trigger-client-hmr` calls `reloadModule` once per module, in
3281
+ * both its client and its ssr pass — and each of those is a separate `updateModules`, a
3282
+ * separate `hmr update`, and a separate refetch of the whole module by the browser.
3212
3283
  *
3213
- * Invalidating here rather than leaving it to `updateModules` also survives another plugin
3214
- * filtering the list afterwards a framework's own `hotUpdate` decides what its route
3215
- * modules do, and the stale bytes have to go either way.
3284
+ * Measured on a five-route react-router app, one `css()` edit in a shared `ui.ts`: eight
3285
+ * `hmr update` messages, `root.tsx` and `dashboard.tsx` fetched five times each, 554 kB over
3286
+ * the socket for a one-line edit. Dropping the redundant half takes it to four messages and
3287
+ * 367 kB with the same modules re-transformed and the same edit applied.
3216
3288
  *
3217
- * A consumer Vite already reached costs nothing: `propagateUpdate` skips a module it has
3218
- * traversed, and every consumer of a *resolvable* dependency is one, since `addWatchFile`
3219
- * makes it an importer whether or not the import survived the fold. What is left is the
3220
- * consumer of a dependency with no module of its own, where Vite matched nothing and would
3221
- * have done nothing at all. That one can end in a page reload, which is the honest outcome:
3222
- * its compiled classes really did change, and a reload is what Vite does with any update
3223
- * nothing accepts.
3289
+ * So the list is returned only when Vite has matched nothing for the file and would otherwise
3290
+ * do nothing at all a dependency the fold read that never became a module of its own. That
3291
+ * one can end in a page reload, which is the honest outcome: its compiled classes really did
3292
+ * change, and a reload is what Vite does with any update nothing accepts.
3224
3293
  */
3225
3294
  const foldDependentModules = (file, modules, graph) => {
3226
3295
  const dependents = dependentsByDependency.get(normalizeFsPath(file));
@@ -3232,6 +3301,7 @@ const bamboocss = (options = {}) => {
3232
3301
  added.push(module);
3233
3302
  }
3234
3303
  if (!added.length) return;
3304
+ if (modules.length) return;
3235
3305
  return [...modules, ...added];
3236
3306
  };
3237
3307
  let ctx;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.39.1",
3
+ "version": "1.40.1",
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.39.1",
44
- "@bamboocss/extractor": "1.39.1",
45
- "@bamboocss/core": "1.39.1",
46
- "@bamboocss/logger": "1.39.1",
47
- "@bamboocss/node": "1.39.1",
48
- "@bamboocss/shared": "1.39.1",
49
- "@bamboocss/types": "1.39.1"
43
+ "@bamboocss/config": "1.40.1",
44
+ "@bamboocss/extractor": "1.40.1",
45
+ "@bamboocss/logger": "1.40.1",
46
+ "@bamboocss/core": "1.40.1",
47
+ "@bamboocss/node": "1.40.1",
48
+ "@bamboocss/shared": "1.40.1",
49
+ "@bamboocss/types": "1.40.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@jridgewell/trace-mapping": "^0.3.31",
53
53
  "vite": "7.2.6",
54
- "@bamboocss/fixture": "1.39.1"
54
+ "@bamboocss/fixture": "1.40.1"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": ">=5"