@bamboocss/vite 1.38.0 → 1.39.0

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
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
+ let _bamboocss_config = require("@bamboocss/config");
27
28
  let _bamboocss_node = require("@bamboocss/node");
28
29
  let _bamboocss_logger = require("@bamboocss/logger");
29
30
  let _bamboocss_shared = require("@bamboocss/shared");
@@ -426,6 +427,39 @@ const bamboocssCss = (options) => {
426
427
  configResolved(config) {
427
428
  command = config.command;
428
429
  session.sourcemap = config.build.sourcemap;
430
+ /**
431
+ * Tell Vite that `bamboo.config.ts` is a config file, so editing one restarts the server.
432
+ *
433
+ * Tokens live there, and they are what a designer iterates on most — "restart the dev
434
+ * server to see a colour change" is the wrong instruction for the file most likely to be
435
+ * edited all afternoon. Nothing watched it: `watch` is the CLI's own watcher, and a
436
+ * project running `vite dev` never reaches it.
437
+ *
438
+ * A restart rather than re-emitting the stylesheet, because this plugin and the compiler
439
+ * hold *separate* contexts and only this one reloads its config. A token *value* edit
440
+ * came out right on the next source change, and an edit that changes what compiles —
441
+ * adding a token, a condition, a utility — left the compiler naming classes from the old
442
+ * config against a sheet emitted from the new one. Half-updated is worse than stale.
443
+ *
444
+ * Through Vite's own list rather than a watcher of ours. Vite adds these paths to the
445
+ * files it watches, which is what reaches a config *outside* `root` — a monorepo with one
446
+ * config above `apps/web`, or a preset resolved into `node_modules`, neither of which the
447
+ * project watcher covers. It also means the restart is Vite's, with its own concurrency
448
+ * guard and its own error reporting, rather than a second implementation of both.
449
+ *
450
+ * The config's own import graph is resolved the way `Builder` resolves it, minus the
451
+ * tsconfig paths it has not loaded yet at this point. `dependencies` globs are not
452
+ * expanded here: they are declared as an escape hatch for a *config reload*, and turning
453
+ * every file matching one into a full server restart is not what a project asking for
454
+ * that meant.
455
+ */
456
+ if (config.command === "serve") try {
457
+ const { deps } = (0, _bamboocss_config.getConfigDependencies)((0, _bamboocss_config.findConfig)({
458
+ cwd: cwd ?? config.root,
459
+ file: configPath
460
+ }));
461
+ config.configFileDependencies.push(...deps);
462
+ } catch {}
429
463
  if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
430
464
  },
431
465
  /**
@@ -3033,6 +3067,18 @@ const isGeneratedOutput = (filePath, ctx) => {
3033
3067
  };
3034
3068
  /** 1-indexed line of a source offset, for an error a user can navigate to. */
3035
3069
  const lineAt = (code, offset) => code.slice(0, offset).split("\n").length;
3070
+ /**
3071
+ * One spelling for a path used as a map key against paths from somewhere else.
3072
+ *
3073
+ * The fold reports dependencies as ts-morph sees them and Vite reports a changed file as its
3074
+ * watcher saw it. On Windows those differ by separator, and can differ by the case of the
3075
+ * drive letter alone — chokidar reports what the OS handed it, `path.resolve` preserves
3076
+ * whatever the cwd had. Either would make every lookup below miss and restore the exact
3077
+ * staleness they exist to fix, silently, since a miss is indistinguishable from a module that
3078
+ * folded nothing. Only the drive letter is case-folded: the rest of the path is compared as
3079
+ * written, because elsewhere the filesystem may well be case-sensitive.
3080
+ */
3081
+ const normalizeFsPath = (file) => (0, node_path.resolve)(file).replaceAll("\\", "/").replace(/^[a-z]:\//, (drive) => drive.toUpperCase());
3036
3082
  const formatSkipped = (id, skipped) => {
3037
3083
  const counts = /* @__PURE__ */ new Map();
3038
3084
  for (const entry of skipped) counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1);
@@ -3053,6 +3099,7 @@ const formatSkipped = (id, skipped) => {
3053
3099
  */
3054
3100
  const bamboocss = (options = {}) => {
3055
3101
  const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
3102
+ (0, _bamboocss_node.markStaticCompilerActive)();
3056
3103
  if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
3057
3104
  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.");
3058
3105
  /**
@@ -3114,6 +3161,84 @@ const bamboocss = (options = {}) => {
3114
3161
  * would otherwise re-parse its module fifty times, which is the transform path.
3115
3162
  */
3116
3163
  const recipeConfigCache = /* @__PURE__ */ new Map();
3164
+ /**
3165
+ * Which modules folded a value read out of which other module, for dev invalidation.
3166
+ *
3167
+ * `addWatchFile` reports the same edges, and in a build that is enough — Rollup discards a
3168
+ * module whose watched file changed. Vite's dev server does not: a module that *statically
3169
+ * imports* the changed one is only **soft**-invalidated, which by design keeps its cached
3170
+ * transform result and rewrites nothing but the timestamps on its import specifiers. That
3171
+ * cached result is where the compiled class string lives, so the edit never reaches it.
3172
+ *
3173
+ * The recipe case is the one users meet, because it is the one where the class is compiled
3174
+ * into somebody else's module: an inline `cva` declaration is erased, and each *call site*
3175
+ * becomes a literal in the module that calls it. Editing the recipe then updates the class
3176
+ * in a module Vite has decided not to re-transform, so the browser and the SSR render keep
3177
+ * the old class — with no error, and with Vite and Bamboo both logging as if the edit landed.
3178
+ * A restart applies it, which is what makes it read as "recipes do not hot-reload".
3179
+ *
3180
+ * `css(sharedObject)` across modules fails identically; it is rarer only because a consumer
3181
+ * that folds *nothing but* recipe calls has its import erased, and an erased import is not a
3182
+ * static one, so Vite hard-invalidates it and the bug hides. Import one more value from the
3183
+ * same module — the shape any real `ui.ts` has — and the import survives, and so does the
3184
+ * stale class.
3185
+ *
3186
+ * Keyed by dependency, since "what changed" is the question asked, and tracked in the other
3187
+ * direction as well so a re-transform can retract edges the module no longer has.
3188
+ */
3189
+ const dependentsByDependency = /* @__PURE__ */ new Map();
3190
+ const dependenciesByFile = /* @__PURE__ */ new Map();
3191
+ const recordFoldDependencies = (file, dependencies) => {
3192
+ const next = new Set(dependencies.map(normalizeFsPath).filter((dependency) => dependency !== file));
3193
+ const previous = dependenciesByFile.get(file);
3194
+ for (const dependency of previous ?? []) {
3195
+ if (next.has(dependency)) continue;
3196
+ const dependents = dependentsByDependency.get(dependency);
3197
+ if (!dependents?.delete(file)) continue;
3198
+ if (!dependents.size) dependentsByDependency.delete(dependency);
3199
+ }
3200
+ if (!next.size) {
3201
+ dependenciesByFile.delete(file);
3202
+ return;
3203
+ }
3204
+ dependenciesByFile.set(file, next);
3205
+ for (const dependency of next) {
3206
+ const dependents = dependentsByDependency.get(dependency);
3207
+ if (dependents) dependents.add(file);
3208
+ else dependentsByDependency.set(dependency, new Set([file]));
3209
+ }
3210
+ };
3211
+ /**
3212
+ * Modules to re-transform because `file` changed, hard-invalidated on the way out.
3213
+ *
3214
+ * Both halves are load-bearing. Invalidating drops the stale compiled result, which is the
3215
+ * defect itself; returning the modules is what makes Vite propagate an update for them, so
3216
+ * the client applies one rather than waiting for whatever request happens next.
3217
+ *
3218
+ * Invalidating here rather than leaving it to `updateModules` also survives another plugin
3219
+ * filtering the list afterwards — a framework's own `hotUpdate` decides what its route
3220
+ * modules do, and the stale bytes have to go either way.
3221
+ *
3222
+ * A consumer Vite already reached costs nothing: `propagateUpdate` skips a module it has
3223
+ * traversed, and every consumer of a *resolvable* dependency is one, since `addWatchFile`
3224
+ * makes it an importer whether or not the import survived the fold. What is left is the
3225
+ * consumer of a dependency with no module of its own, where Vite matched nothing and would
3226
+ * have done nothing at all. That one can end in a page reload, which is the honest outcome:
3227
+ * its compiled classes really did change, and a reload is what Vite does with any update
3228
+ * nothing accepts.
3229
+ */
3230
+ const foldDependentModules = (file, modules, graph) => {
3231
+ const dependents = dependentsByDependency.get(normalizeFsPath(file));
3232
+ if (!dependents?.size) return;
3233
+ const added = [];
3234
+ for (const dependent of dependents) for (const module of graph.getModulesByFile(dependent) ?? []) {
3235
+ if (modules.includes(module) || added.includes(module)) continue;
3236
+ graph.invalidateModule(module);
3237
+ added.push(module);
3238
+ }
3239
+ if (!added.length) return;
3240
+ return [...modules, ...added];
3241
+ };
3117
3242
  let ctx;
3118
3243
  let runtimeCss;
3119
3244
  let styleCompiler;
@@ -3149,6 +3274,8 @@ const bamboocss = (options = {}) => {
3149
3274
  perFile.clear();
3150
3275
  survivorsByFile.clear();
3151
3276
  recipeConfigCache.clear();
3277
+ dependentsByDependency.clear();
3278
+ dependenciesByFile.clear();
3152
3279
  resetStaticCompilationSession(staticSession);
3153
3280
  }
3154
3281
  staticSession.startedEnvironments.add(environment);
@@ -3186,10 +3313,40 @@ const bamboocss = (options = {}) => {
3186
3313
  recipeConfigCache.clear();
3187
3314
  if (change.event === "delete") {
3188
3315
  ctx.project.removeSourceFile(filePath);
3316
+ recordFoldDependencies(normalizeFsPath(filePath), []);
3189
3317
  return;
3190
3318
  }
3191
3319
  ctx.project.reloadSourceFile(filePath);
3192
3320
  },
3321
+ /**
3322
+ * Re-transform whatever folded a value out of the file that just changed.
3323
+ *
3324
+ * Dev only — `hotUpdate` does not run in a build, where Rollup's own invalidation already
3325
+ * covers this — and additive: the modules Vite matched are returned alongside, so this
3326
+ * decides nothing about them.
3327
+ *
3328
+ * `handleHotUpdate` below stands in on Vite 5, which has no `hotUpdate`. Not quite the
3329
+ * same thing: Vite 5 calls that hook for an update and not for a file appearing or being
3330
+ * deleted, so a recipe file *created* while the server runs leaves its consumers stale
3331
+ * there. Vite 6 and up call `hotUpdate` for all three, and never call `handleHotUpdate`
3332
+ * when a plugin has both — including its deprecation warning — so exactly one of the two
3333
+ * runs on any supported version.
3334
+ *
3335
+ * `environment` optional-chained for the same reason `addWatchFile` is in `transform`:
3336
+ * a harness driving the hook need not supply a full plugin context, and a `TypeError`
3337
+ * here is swallowed into an HMR error payload that a middleware-mode server sends
3338
+ * nowhere.
3339
+ */
3340
+ hotUpdate({ file, modules }) {
3341
+ const graph = this.environment?.moduleGraph;
3342
+ if (!graph) return;
3343
+ return foldDependentModules(file, modules, graph);
3344
+ },
3345
+ handleHotUpdate({ file, modules, server }) {
3346
+ const legacy = server;
3347
+ if (legacy.environments) return;
3348
+ return foldDependentModules(file, modules, legacy.moduleGraph);
3349
+ },
3193
3350
  async transform(code, id) {
3194
3351
  if (!shouldTransform(id)) return null;
3195
3352
  try {
@@ -3257,6 +3414,7 @@ const bamboocss = (options = {}) => {
3257
3414
  }
3258
3415
  if (reportSkipped && result.skipped.length) _bamboocss_logger.logger.info("vite:transform", formatSkipped(filePath, result.skipped));
3259
3416
  for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
3417
+ recordFoldDependencies(normalizeFsPath(filePath), result.dependencies);
3260
3418
  const forFile = survivorsByFile.get(filePath);
3261
3419
  if (command === "serve" && forFile?.length) throw createSurvivorError(forFile);
3262
3420
  if (!result.folded.length) return null;
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { Builder, loadConfigAndCreateContext } from "@bamboocss/node";
1
+ import { findConfig, getConfigDependencies } from "@bamboocss/config";
2
+ import { Builder, loadConfigAndCreateContext, markStaticCompilerActive } from "@bamboocss/node";
2
3
  import { logger } from "@bamboocss/logger";
3
4
  import { compact, createCssUncached, createMergeCss, esc, memo, toHash, truncateList, viewTransitionClassName } from "@bamboocss/shared";
4
5
  import remapping from "@ampproject/remapping";
@@ -396,6 +397,39 @@ const bamboocssCss = (options) => {
396
397
  configResolved(config) {
397
398
  command = config.command;
398
399
  session.sourcemap = config.build.sourcemap;
400
+ /**
401
+ * Tell Vite that `bamboo.config.ts` is a config file, so editing one restarts the server.
402
+ *
403
+ * Tokens live there, and they are what a designer iterates on most — "restart the dev
404
+ * server to see a colour change" is the wrong instruction for the file most likely to be
405
+ * edited all afternoon. Nothing watched it: `watch` is the CLI's own watcher, and a
406
+ * project running `vite dev` never reaches it.
407
+ *
408
+ * A restart rather than re-emitting the stylesheet, because this plugin and the compiler
409
+ * hold *separate* contexts and only this one reloads its config. A token *value* edit
410
+ * came out right on the next source change, and an edit that changes what compiles —
411
+ * adding a token, a condition, a utility — left the compiler naming classes from the old
412
+ * config against a sheet emitted from the new one. Half-updated is worse than stale.
413
+ *
414
+ * Through Vite's own list rather than a watcher of ours. Vite adds these paths to the
415
+ * files it watches, which is what reaches a config *outside* `root` — a monorepo with one
416
+ * config above `apps/web`, or a preset resolved into `node_modules`, neither of which the
417
+ * project watcher covers. It also means the restart is Vite's, with its own concurrency
418
+ * guard and its own error reporting, rather than a second implementation of both.
419
+ *
420
+ * The config's own import graph is resolved the way `Builder` resolves it, minus the
421
+ * tsconfig paths it has not loaded yet at this point. `dependencies` globs are not
422
+ * expanded here: they are declared as an escape hatch for a *config reload*, and turning
423
+ * every file matching one into a full server restart is not what a project asking for
424
+ * that meant.
425
+ */
426
+ if (config.command === "serve") try {
427
+ const { deps } = getConfigDependencies(findConfig({
428
+ cwd: cwd ?? config.root,
429
+ file: configPath
430
+ }));
431
+ config.configFileDependencies.push(...deps);
432
+ } catch {}
399
433
  if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
400
434
  },
401
435
  /**
@@ -3003,6 +3037,18 @@ const isGeneratedOutput = (filePath, ctx) => {
3003
3037
  };
3004
3038
  /** 1-indexed line of a source offset, for an error a user can navigate to. */
3005
3039
  const lineAt = (code, offset) => code.slice(0, offset).split("\n").length;
3040
+ /**
3041
+ * One spelling for a path used as a map key against paths from somewhere else.
3042
+ *
3043
+ * The fold reports dependencies as ts-morph sees them and Vite reports a changed file as its
3044
+ * watcher saw it. On Windows those differ by separator, and can differ by the case of the
3045
+ * drive letter alone — chokidar reports what the OS handed it, `path.resolve` preserves
3046
+ * whatever the cwd had. Either would make every lookup below miss and restore the exact
3047
+ * staleness they exist to fix, silently, since a miss is indistinguishable from a module that
3048
+ * folded nothing. Only the drive letter is case-folded: the rest of the path is compared as
3049
+ * written, because elsewhere the filesystem may well be case-sensitive.
3050
+ */
3051
+ const normalizeFsPath = (file) => resolve(file).replaceAll("\\", "/").replace(/^[a-z]:\//, (drive) => drive.toUpperCase());
3006
3052
  const formatSkipped = (id, skipped) => {
3007
3053
  const counts = /* @__PURE__ */ new Map();
3008
3054
  for (const entry of skipped) counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1);
@@ -3023,6 +3069,7 @@ const formatSkipped = (id, skipped) => {
3023
3069
  */
3024
3070
  const bamboocss = (options = {}) => {
3025
3071
  const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
3072
+ markStaticCompilerActive();
3026
3073
  if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
3027
3074
  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.");
3028
3075
  /**
@@ -3084,6 +3131,84 @@ const bamboocss = (options = {}) => {
3084
3131
  * would otherwise re-parse its module fifty times, which is the transform path.
3085
3132
  */
3086
3133
  const recipeConfigCache = /* @__PURE__ */ new Map();
3134
+ /**
3135
+ * Which modules folded a value read out of which other module, for dev invalidation.
3136
+ *
3137
+ * `addWatchFile` reports the same edges, and in a build that is enough — Rollup discards a
3138
+ * module whose watched file changed. Vite's dev server does not: a module that *statically
3139
+ * imports* the changed one is only **soft**-invalidated, which by design keeps its cached
3140
+ * transform result and rewrites nothing but the timestamps on its import specifiers. That
3141
+ * cached result is where the compiled class string lives, so the edit never reaches it.
3142
+ *
3143
+ * The recipe case is the one users meet, because it is the one where the class is compiled
3144
+ * into somebody else's module: an inline `cva` declaration is erased, and each *call site*
3145
+ * becomes a literal in the module that calls it. Editing the recipe then updates the class
3146
+ * in a module Vite has decided not to re-transform, so the browser and the SSR render keep
3147
+ * the old class — with no error, and with Vite and Bamboo both logging as if the edit landed.
3148
+ * A restart applies it, which is what makes it read as "recipes do not hot-reload".
3149
+ *
3150
+ * `css(sharedObject)` across modules fails identically; it is rarer only because a consumer
3151
+ * that folds *nothing but* recipe calls has its import erased, and an erased import is not a
3152
+ * static one, so Vite hard-invalidates it and the bug hides. Import one more value from the
3153
+ * same module — the shape any real `ui.ts` has — and the import survives, and so does the
3154
+ * stale class.
3155
+ *
3156
+ * Keyed by dependency, since "what changed" is the question asked, and tracked in the other
3157
+ * direction as well so a re-transform can retract edges the module no longer has.
3158
+ */
3159
+ const dependentsByDependency = /* @__PURE__ */ new Map();
3160
+ const dependenciesByFile = /* @__PURE__ */ new Map();
3161
+ const recordFoldDependencies = (file, dependencies) => {
3162
+ const next = new Set(dependencies.map(normalizeFsPath).filter((dependency) => dependency !== file));
3163
+ const previous = dependenciesByFile.get(file);
3164
+ for (const dependency of previous ?? []) {
3165
+ if (next.has(dependency)) continue;
3166
+ const dependents = dependentsByDependency.get(dependency);
3167
+ if (!dependents?.delete(file)) continue;
3168
+ if (!dependents.size) dependentsByDependency.delete(dependency);
3169
+ }
3170
+ if (!next.size) {
3171
+ dependenciesByFile.delete(file);
3172
+ return;
3173
+ }
3174
+ dependenciesByFile.set(file, next);
3175
+ for (const dependency of next) {
3176
+ const dependents = dependentsByDependency.get(dependency);
3177
+ if (dependents) dependents.add(file);
3178
+ else dependentsByDependency.set(dependency, new Set([file]));
3179
+ }
3180
+ };
3181
+ /**
3182
+ * Modules to re-transform because `file` changed, hard-invalidated on the way out.
3183
+ *
3184
+ * Both halves are load-bearing. Invalidating drops the stale compiled result, which is the
3185
+ * defect itself; returning the modules is what makes Vite propagate an update for them, so
3186
+ * the client applies one rather than waiting for whatever request happens next.
3187
+ *
3188
+ * Invalidating here rather than leaving it to `updateModules` also survives another plugin
3189
+ * filtering the list afterwards — a framework's own `hotUpdate` decides what its route
3190
+ * modules do, and the stale bytes have to go either way.
3191
+ *
3192
+ * A consumer Vite already reached costs nothing: `propagateUpdate` skips a module it has
3193
+ * traversed, and every consumer of a *resolvable* dependency is one, since `addWatchFile`
3194
+ * makes it an importer whether or not the import survived the fold. What is left is the
3195
+ * consumer of a dependency with no module of its own, where Vite matched nothing and would
3196
+ * have done nothing at all. That one can end in a page reload, which is the honest outcome:
3197
+ * its compiled classes really did change, and a reload is what Vite does with any update
3198
+ * nothing accepts.
3199
+ */
3200
+ const foldDependentModules = (file, modules, graph) => {
3201
+ const dependents = dependentsByDependency.get(normalizeFsPath(file));
3202
+ if (!dependents?.size) return;
3203
+ const added = [];
3204
+ for (const dependent of dependents) for (const module of graph.getModulesByFile(dependent) ?? []) {
3205
+ if (modules.includes(module) || added.includes(module)) continue;
3206
+ graph.invalidateModule(module);
3207
+ added.push(module);
3208
+ }
3209
+ if (!added.length) return;
3210
+ return [...modules, ...added];
3211
+ };
3087
3212
  let ctx;
3088
3213
  let runtimeCss;
3089
3214
  let styleCompiler;
@@ -3119,6 +3244,8 @@ const bamboocss = (options = {}) => {
3119
3244
  perFile.clear();
3120
3245
  survivorsByFile.clear();
3121
3246
  recipeConfigCache.clear();
3247
+ dependentsByDependency.clear();
3248
+ dependenciesByFile.clear();
3122
3249
  resetStaticCompilationSession(staticSession);
3123
3250
  }
3124
3251
  staticSession.startedEnvironments.add(environment);
@@ -3156,10 +3283,40 @@ const bamboocss = (options = {}) => {
3156
3283
  recipeConfigCache.clear();
3157
3284
  if (change.event === "delete") {
3158
3285
  ctx.project.removeSourceFile(filePath);
3286
+ recordFoldDependencies(normalizeFsPath(filePath), []);
3159
3287
  return;
3160
3288
  }
3161
3289
  ctx.project.reloadSourceFile(filePath);
3162
3290
  },
3291
+ /**
3292
+ * Re-transform whatever folded a value out of the file that just changed.
3293
+ *
3294
+ * Dev only — `hotUpdate` does not run in a build, where Rollup's own invalidation already
3295
+ * covers this — and additive: the modules Vite matched are returned alongside, so this
3296
+ * decides nothing about them.
3297
+ *
3298
+ * `handleHotUpdate` below stands in on Vite 5, which has no `hotUpdate`. Not quite the
3299
+ * same thing: Vite 5 calls that hook for an update and not for a file appearing or being
3300
+ * deleted, so a recipe file *created* while the server runs leaves its consumers stale
3301
+ * there. Vite 6 and up call `hotUpdate` for all three, and never call `handleHotUpdate`
3302
+ * when a plugin has both — including its deprecation warning — so exactly one of the two
3303
+ * runs on any supported version.
3304
+ *
3305
+ * `environment` optional-chained for the same reason `addWatchFile` is in `transform`:
3306
+ * a harness driving the hook need not supply a full plugin context, and a `TypeError`
3307
+ * here is swallowed into an HMR error payload that a middleware-mode server sends
3308
+ * nowhere.
3309
+ */
3310
+ hotUpdate({ file, modules }) {
3311
+ const graph = this.environment?.moduleGraph;
3312
+ if (!graph) return;
3313
+ return foldDependentModules(file, modules, graph);
3314
+ },
3315
+ handleHotUpdate({ file, modules, server }) {
3316
+ const legacy = server;
3317
+ if (legacy.environments) return;
3318
+ return foldDependentModules(file, modules, legacy.moduleGraph);
3319
+ },
3163
3320
  async transform(code, id) {
3164
3321
  if (!shouldTransform(id)) return null;
3165
3322
  try {
@@ -3227,6 +3384,7 @@ const bamboocss = (options = {}) => {
3227
3384
  }
3228
3385
  if (reportSkipped && result.skipped.length) logger.info("vite:transform", formatSkipped(filePath, result.skipped));
3229
3386
  for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
3387
+ recordFoldDependencies(normalizeFsPath(filePath), result.dependencies);
3230
3388
  const forFile = survivorsByFile.get(filePath);
3231
3389
  if (command === "serve" && forFile?.length) throw createSurvivorError(forFile);
3232
3390
  if (!result.folded.length) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/vite",
3
- "version": "1.38.0",
3
+ "version": "1.39.0",
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.38.0",
44
- "@bamboocss/core": "1.38.0",
45
- "@bamboocss/logger": "1.38.0",
46
- "@bamboocss/node": "1.38.0",
47
- "@bamboocss/extractor": "1.38.0",
48
- "@bamboocss/shared": "1.38.0",
49
- "@bamboocss/types": "1.38.0"
43
+ "@bamboocss/core": "1.39.0",
44
+ "@bamboocss/extractor": "1.39.0",
45
+ "@bamboocss/config": "1.39.0",
46
+ "@bamboocss/logger": "1.39.0",
47
+ "@bamboocss/node": "1.39.0",
48
+ "@bamboocss/shared": "1.39.0",
49
+ "@bamboocss/types": "1.39.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@jridgewell/trace-mapping": "^0.3.31",
53
53
  "vite": "7.2.6",
54
- "@bamboocss/fixture": "1.38.0"
54
+ "@bamboocss/fixture": "1.39.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": ">=5"