@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 +158 -0
- package/dist/index.mjs +159 -1
- package/package.json +9 -9
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 {
|
|
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.
|
|
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/
|
|
44
|
-
"@bamboocss/
|
|
45
|
-
"@bamboocss/
|
|
46
|
-
"@bamboocss/
|
|
47
|
-
"@bamboocss/
|
|
48
|
-
"@bamboocss/shared": "1.
|
|
49
|
-
"@bamboocss/types": "1.
|
|
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.
|
|
54
|
+
"@bamboocss/fixture": "1.39.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vite": ">=5"
|