@bamboocss/vite 1.38.0 → 1.39.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 +183 -0
- package/dist/index.mjs +184 -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");
|
|
@@ -365,6 +366,8 @@ const bamboocssCss = (options) => {
|
|
|
365
366
|
const builder = new _bamboocss_node.Builder();
|
|
366
367
|
let server;
|
|
367
368
|
let command = "build";
|
|
369
|
+
/** The run's own `build` options, for a bundler with no per-environment config. */
|
|
370
|
+
let ssrBuildOptions;
|
|
368
371
|
/**
|
|
369
372
|
* Serialised, because both `load` and the watcher can reach it and `Builder` keeps one
|
|
370
373
|
* context. Two overlapping passes would extract into the same encoder and emit the
|
|
@@ -426,6 +429,43 @@ const bamboocssCss = (options) => {
|
|
|
426
429
|
configResolved(config) {
|
|
427
430
|
command = config.command;
|
|
428
431
|
session.sourcemap = config.build.sourcemap;
|
|
432
|
+
ssrBuildOptions = {
|
|
433
|
+
ssr: config.build.ssr,
|
|
434
|
+
ssrEmitAssets: config.build.ssrEmitAssets
|
|
435
|
+
};
|
|
436
|
+
/**
|
|
437
|
+
* Tell Vite that `bamboo.config.ts` is a config file, so editing one restarts the server.
|
|
438
|
+
*
|
|
439
|
+
* Tokens live there, and they are what a designer iterates on most — "restart the dev
|
|
440
|
+
* server to see a colour change" is the wrong instruction for the file most likely to be
|
|
441
|
+
* edited all afternoon. Nothing watched it: `watch` is the CLI's own watcher, and a
|
|
442
|
+
* project running `vite dev` never reaches it.
|
|
443
|
+
*
|
|
444
|
+
* A restart rather than re-emitting the stylesheet, because this plugin and the compiler
|
|
445
|
+
* hold *separate* contexts and only this one reloads its config. A token *value* edit
|
|
446
|
+
* came out right on the next source change, and an edit that changes what compiles —
|
|
447
|
+
* adding a token, a condition, a utility — left the compiler naming classes from the old
|
|
448
|
+
* config against a sheet emitted from the new one. Half-updated is worse than stale.
|
|
449
|
+
*
|
|
450
|
+
* Through Vite's own list rather than a watcher of ours. Vite adds these paths to the
|
|
451
|
+
* files it watches, which is what reaches a config *outside* `root` — a monorepo with one
|
|
452
|
+
* config above `apps/web`, or a preset resolved into `node_modules`, neither of which the
|
|
453
|
+
* project watcher covers. It also means the restart is Vite's, with its own concurrency
|
|
454
|
+
* guard and its own error reporting, rather than a second implementation of both.
|
|
455
|
+
*
|
|
456
|
+
* The config's own import graph is resolved the way `Builder` resolves it, minus the
|
|
457
|
+
* tsconfig paths it has not loaded yet at this point. `dependencies` globs are not
|
|
458
|
+
* expanded here: they are declared as an escape hatch for a *config reload*, and turning
|
|
459
|
+
* every file matching one into a full server restart is not what a project asking for
|
|
460
|
+
* that meant.
|
|
461
|
+
*/
|
|
462
|
+
if (config.command === "serve") try {
|
|
463
|
+
const { deps } = (0, _bamboocss_config.getConfigDependencies)((0, _bamboocss_config.findConfig)({
|
|
464
|
+
cwd: cwd ?? config.root,
|
|
465
|
+
file: configPath
|
|
466
|
+
}));
|
|
467
|
+
config.configFileDependencies.push(...deps);
|
|
468
|
+
} catch {}
|
|
429
469
|
if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
|
|
430
470
|
},
|
|
431
471
|
/**
|
|
@@ -506,6 +546,25 @@ const bamboocssCss = (options) => {
|
|
|
506
546
|
})} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
|
|
507
547
|
if (!servedEnvironments.has(environment?.name ?? "default")) return;
|
|
508
548
|
if (!session.transformedFiles.size) return;
|
|
549
|
+
/**
|
|
550
|
+
* An SSR bundle emits no CSS assets, and is not supposed to.
|
|
551
|
+
*
|
|
552
|
+
* `build.ssrEmitAssets` is off by default, so Vite discards them: the client build is
|
|
553
|
+
* what carries the stylesheet, and a server bundle that imports `virtual:bamboo.css`
|
|
554
|
+
* from shared code — a root component, a layout — still asks this plugin to load it.
|
|
555
|
+
* Which means the environment *served* the sheet and then emitted nothing, and the
|
|
556
|
+
* check below read that as the failure it exists to catch.
|
|
557
|
+
*
|
|
558
|
+
* It fails a build that is entirely correct. Qwik's `vite build --ssr` is the shape
|
|
559
|
+
* that showed it: 7/7 calls compiled, the client bundle carrying the stylesheet, and
|
|
560
|
+
* the server bundle refusing to finish. React Router does not hit it only because its
|
|
561
|
+
* plugin turns `ssrEmitAssets` on.
|
|
562
|
+
*
|
|
563
|
+
* Read per environment where that exists, falling back to the run's own config, so
|
|
564
|
+
* Vite 5's single-config builds are answered by the same question.
|
|
565
|
+
*/
|
|
566
|
+
const buildOptions = environment?.config?.build ?? ssrBuildOptions;
|
|
567
|
+
if (buildOptions?.ssr && !buildOptions.ssrEmitAssets) return;
|
|
509
568
|
if (!Object.values(bundle).some((output) => {
|
|
510
569
|
if (!carriesGeneratedCss(output)) return false;
|
|
511
570
|
return (typeof output.source === "string" ? output.source : Buffer.from(output.source).toString()).includes("--made-with-bamboo");
|
|
@@ -3033,6 +3092,18 @@ const isGeneratedOutput = (filePath, ctx) => {
|
|
|
3033
3092
|
};
|
|
3034
3093
|
/** 1-indexed line of a source offset, for an error a user can navigate to. */
|
|
3035
3094
|
const lineAt = (code, offset) => code.slice(0, offset).split("\n").length;
|
|
3095
|
+
/**
|
|
3096
|
+
* One spelling for a path used as a map key against paths from somewhere else.
|
|
3097
|
+
*
|
|
3098
|
+
* The fold reports dependencies as ts-morph sees them and Vite reports a changed file as its
|
|
3099
|
+
* watcher saw it. On Windows those differ by separator, and can differ by the case of the
|
|
3100
|
+
* drive letter alone — chokidar reports what the OS handed it, `path.resolve` preserves
|
|
3101
|
+
* whatever the cwd had. Either would make every lookup below miss and restore the exact
|
|
3102
|
+
* staleness they exist to fix, silently, since a miss is indistinguishable from a module that
|
|
3103
|
+
* folded nothing. Only the drive letter is case-folded: the rest of the path is compared as
|
|
3104
|
+
* written, because elsewhere the filesystem may well be case-sensitive.
|
|
3105
|
+
*/
|
|
3106
|
+
const normalizeFsPath = (file) => (0, node_path.resolve)(file).replaceAll("\\", "/").replace(/^[a-z]:\//, (drive) => drive.toUpperCase());
|
|
3036
3107
|
const formatSkipped = (id, skipped) => {
|
|
3037
3108
|
const counts = /* @__PURE__ */ new Map();
|
|
3038
3109
|
for (const entry of skipped) counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1);
|
|
@@ -3053,6 +3124,7 @@ const formatSkipped = (id, skipped) => {
|
|
|
3053
3124
|
*/
|
|
3054
3125
|
const bamboocss = (options = {}) => {
|
|
3055
3126
|
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
|
|
3127
|
+
(0, _bamboocss_node.markStaticCompilerActive)();
|
|
3056
3128
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
3057
3129
|
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
3130
|
/**
|
|
@@ -3114,6 +3186,84 @@ const bamboocss = (options = {}) => {
|
|
|
3114
3186
|
* would otherwise re-parse its module fifty times, which is the transform path.
|
|
3115
3187
|
*/
|
|
3116
3188
|
const recipeConfigCache = /* @__PURE__ */ new Map();
|
|
3189
|
+
/**
|
|
3190
|
+
* Which modules folded a value read out of which other module, for dev invalidation.
|
|
3191
|
+
*
|
|
3192
|
+
* `addWatchFile` reports the same edges, and in a build that is enough — Rollup discards a
|
|
3193
|
+
* module whose watched file changed. Vite's dev server does not: a module that *statically
|
|
3194
|
+
* imports* the changed one is only **soft**-invalidated, which by design keeps its cached
|
|
3195
|
+
* transform result and rewrites nothing but the timestamps on its import specifiers. That
|
|
3196
|
+
* cached result is where the compiled class string lives, so the edit never reaches it.
|
|
3197
|
+
*
|
|
3198
|
+
* The recipe case is the one users meet, because it is the one where the class is compiled
|
|
3199
|
+
* into somebody else's module: an inline `cva` declaration is erased, and each *call site*
|
|
3200
|
+
* becomes a literal in the module that calls it. Editing the recipe then updates the class
|
|
3201
|
+
* in a module Vite has decided not to re-transform, so the browser and the SSR render keep
|
|
3202
|
+
* the old class — with no error, and with Vite and Bamboo both logging as if the edit landed.
|
|
3203
|
+
* A restart applies it, which is what makes it read as "recipes do not hot-reload".
|
|
3204
|
+
*
|
|
3205
|
+
* `css(sharedObject)` across modules fails identically; it is rarer only because a consumer
|
|
3206
|
+
* that folds *nothing but* recipe calls has its import erased, and an erased import is not a
|
|
3207
|
+
* static one, so Vite hard-invalidates it and the bug hides. Import one more value from the
|
|
3208
|
+
* same module — the shape any real `ui.ts` has — and the import survives, and so does the
|
|
3209
|
+
* stale class.
|
|
3210
|
+
*
|
|
3211
|
+
* Keyed by dependency, since "what changed" is the question asked, and tracked in the other
|
|
3212
|
+
* direction as well so a re-transform can retract edges the module no longer has.
|
|
3213
|
+
*/
|
|
3214
|
+
const dependentsByDependency = /* @__PURE__ */ new Map();
|
|
3215
|
+
const dependenciesByFile = /* @__PURE__ */ new Map();
|
|
3216
|
+
const recordFoldDependencies = (file, dependencies) => {
|
|
3217
|
+
const next = new Set(dependencies.map(normalizeFsPath).filter((dependency) => dependency !== file));
|
|
3218
|
+
const previous = dependenciesByFile.get(file);
|
|
3219
|
+
for (const dependency of previous ?? []) {
|
|
3220
|
+
if (next.has(dependency)) continue;
|
|
3221
|
+
const dependents = dependentsByDependency.get(dependency);
|
|
3222
|
+
if (!dependents?.delete(file)) continue;
|
|
3223
|
+
if (!dependents.size) dependentsByDependency.delete(dependency);
|
|
3224
|
+
}
|
|
3225
|
+
if (!next.size) {
|
|
3226
|
+
dependenciesByFile.delete(file);
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
dependenciesByFile.set(file, next);
|
|
3230
|
+
for (const dependency of next) {
|
|
3231
|
+
const dependents = dependentsByDependency.get(dependency);
|
|
3232
|
+
if (dependents) dependents.add(file);
|
|
3233
|
+
else dependentsByDependency.set(dependency, new Set([file]));
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3236
|
+
/**
|
|
3237
|
+
* Modules to re-transform because `file` changed, hard-invalidated on the way out.
|
|
3238
|
+
*
|
|
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.
|
|
3242
|
+
*
|
|
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.
|
|
3246
|
+
*
|
|
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.
|
|
3254
|
+
*/
|
|
3255
|
+
const foldDependentModules = (file, modules, graph) => {
|
|
3256
|
+
const dependents = dependentsByDependency.get(normalizeFsPath(file));
|
|
3257
|
+
if (!dependents?.size) return;
|
|
3258
|
+
const added = [];
|
|
3259
|
+
for (const dependent of dependents) for (const module of graph.getModulesByFile(dependent) ?? []) {
|
|
3260
|
+
if (modules.includes(module) || added.includes(module)) continue;
|
|
3261
|
+
graph.invalidateModule(module);
|
|
3262
|
+
added.push(module);
|
|
3263
|
+
}
|
|
3264
|
+
if (!added.length) return;
|
|
3265
|
+
return [...modules, ...added];
|
|
3266
|
+
};
|
|
3117
3267
|
let ctx;
|
|
3118
3268
|
let runtimeCss;
|
|
3119
3269
|
let styleCompiler;
|
|
@@ -3149,6 +3299,8 @@ const bamboocss = (options = {}) => {
|
|
|
3149
3299
|
perFile.clear();
|
|
3150
3300
|
survivorsByFile.clear();
|
|
3151
3301
|
recipeConfigCache.clear();
|
|
3302
|
+
dependentsByDependency.clear();
|
|
3303
|
+
dependenciesByFile.clear();
|
|
3152
3304
|
resetStaticCompilationSession(staticSession);
|
|
3153
3305
|
}
|
|
3154
3306
|
staticSession.startedEnvironments.add(environment);
|
|
@@ -3186,10 +3338,40 @@ const bamboocss = (options = {}) => {
|
|
|
3186
3338
|
recipeConfigCache.clear();
|
|
3187
3339
|
if (change.event === "delete") {
|
|
3188
3340
|
ctx.project.removeSourceFile(filePath);
|
|
3341
|
+
recordFoldDependencies(normalizeFsPath(filePath), []);
|
|
3189
3342
|
return;
|
|
3190
3343
|
}
|
|
3191
3344
|
ctx.project.reloadSourceFile(filePath);
|
|
3192
3345
|
},
|
|
3346
|
+
/**
|
|
3347
|
+
* Re-transform whatever folded a value out of the file that just changed.
|
|
3348
|
+
*
|
|
3349
|
+
* Dev only — `hotUpdate` does not run in a build, where Rollup's own invalidation already
|
|
3350
|
+
* covers this — and additive: the modules Vite matched are returned alongside, so this
|
|
3351
|
+
* decides nothing about them.
|
|
3352
|
+
*
|
|
3353
|
+
* `handleHotUpdate` below stands in on Vite 5, which has no `hotUpdate`. Not quite the
|
|
3354
|
+
* same thing: Vite 5 calls that hook for an update and not for a file appearing or being
|
|
3355
|
+
* deleted, so a recipe file *created* while the server runs leaves its consumers stale
|
|
3356
|
+
* there. Vite 6 and up call `hotUpdate` for all three, and never call `handleHotUpdate`
|
|
3357
|
+
* when a plugin has both — including its deprecation warning — so exactly one of the two
|
|
3358
|
+
* runs on any supported version.
|
|
3359
|
+
*
|
|
3360
|
+
* `environment` optional-chained for the same reason `addWatchFile` is in `transform`:
|
|
3361
|
+
* a harness driving the hook need not supply a full plugin context, and a `TypeError`
|
|
3362
|
+
* here is swallowed into an HMR error payload that a middleware-mode server sends
|
|
3363
|
+
* nowhere.
|
|
3364
|
+
*/
|
|
3365
|
+
hotUpdate({ file, modules }) {
|
|
3366
|
+
const graph = this.environment?.moduleGraph;
|
|
3367
|
+
if (!graph) return;
|
|
3368
|
+
return foldDependentModules(file, modules, graph);
|
|
3369
|
+
},
|
|
3370
|
+
handleHotUpdate({ file, modules, server }) {
|
|
3371
|
+
const legacy = server;
|
|
3372
|
+
if (legacy.environments) return;
|
|
3373
|
+
return foldDependentModules(file, modules, legacy.moduleGraph);
|
|
3374
|
+
},
|
|
3193
3375
|
async transform(code, id) {
|
|
3194
3376
|
if (!shouldTransform(id)) return null;
|
|
3195
3377
|
try {
|
|
@@ -3257,6 +3439,7 @@ const bamboocss = (options = {}) => {
|
|
|
3257
3439
|
}
|
|
3258
3440
|
if (reportSkipped && result.skipped.length) _bamboocss_logger.logger.info("vite:transform", formatSkipped(filePath, result.skipped));
|
|
3259
3441
|
for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
|
|
3442
|
+
recordFoldDependencies(normalizeFsPath(filePath), result.dependencies);
|
|
3260
3443
|
const forFile = survivorsByFile.get(filePath);
|
|
3261
3444
|
if (command === "serve" && forFile?.length) throw createSurvivorError(forFile);
|
|
3262
3445
|
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";
|
|
@@ -335,6 +336,8 @@ const bamboocssCss = (options) => {
|
|
|
335
336
|
const builder = new Builder();
|
|
336
337
|
let server;
|
|
337
338
|
let command = "build";
|
|
339
|
+
/** The run's own `build` options, for a bundler with no per-environment config. */
|
|
340
|
+
let ssrBuildOptions;
|
|
338
341
|
/**
|
|
339
342
|
* Serialised, because both `load` and the watcher can reach it and `Builder` keeps one
|
|
340
343
|
* context. Two overlapping passes would extract into the same encoder and emit the
|
|
@@ -396,6 +399,43 @@ const bamboocssCss = (options) => {
|
|
|
396
399
|
configResolved(config) {
|
|
397
400
|
command = config.command;
|
|
398
401
|
session.sourcemap = config.build.sourcemap;
|
|
402
|
+
ssrBuildOptions = {
|
|
403
|
+
ssr: config.build.ssr,
|
|
404
|
+
ssrEmitAssets: config.build.ssrEmitAssets
|
|
405
|
+
};
|
|
406
|
+
/**
|
|
407
|
+
* Tell Vite that `bamboo.config.ts` is a config file, so editing one restarts the server.
|
|
408
|
+
*
|
|
409
|
+
* Tokens live there, and they are what a designer iterates on most — "restart the dev
|
|
410
|
+
* server to see a colour change" is the wrong instruction for the file most likely to be
|
|
411
|
+
* edited all afternoon. Nothing watched it: `watch` is the CLI's own watcher, and a
|
|
412
|
+
* project running `vite dev` never reaches it.
|
|
413
|
+
*
|
|
414
|
+
* A restart rather than re-emitting the stylesheet, because this plugin and the compiler
|
|
415
|
+
* hold *separate* contexts and only this one reloads its config. A token *value* edit
|
|
416
|
+
* came out right on the next source change, and an edit that changes what compiles —
|
|
417
|
+
* adding a token, a condition, a utility — left the compiler naming classes from the old
|
|
418
|
+
* config against a sheet emitted from the new one. Half-updated is worse than stale.
|
|
419
|
+
*
|
|
420
|
+
* Through Vite's own list rather than a watcher of ours. Vite adds these paths to the
|
|
421
|
+
* files it watches, which is what reaches a config *outside* `root` — a monorepo with one
|
|
422
|
+
* config above `apps/web`, or a preset resolved into `node_modules`, neither of which the
|
|
423
|
+
* project watcher covers. It also means the restart is Vite's, with its own concurrency
|
|
424
|
+
* guard and its own error reporting, rather than a second implementation of both.
|
|
425
|
+
*
|
|
426
|
+
* The config's own import graph is resolved the way `Builder` resolves it, minus the
|
|
427
|
+
* tsconfig paths it has not loaded yet at this point. `dependencies` globs are not
|
|
428
|
+
* expanded here: they are declared as an escape hatch for a *config reload*, and turning
|
|
429
|
+
* every file matching one into a full server restart is not what a project asking for
|
|
430
|
+
* that meant.
|
|
431
|
+
*/
|
|
432
|
+
if (config.command === "serve") try {
|
|
433
|
+
const { deps } = getConfigDependencies(findConfig({
|
|
434
|
+
cwd: cwd ?? config.root,
|
|
435
|
+
file: configPath
|
|
436
|
+
}));
|
|
437
|
+
config.configFileDependencies.push(...deps);
|
|
438
|
+
} catch {}
|
|
399
439
|
if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
|
|
400
440
|
},
|
|
401
441
|
/**
|
|
@@ -476,6 +516,25 @@ const bamboocssCss = (options) => {
|
|
|
476
516
|
})} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
|
|
477
517
|
if (!servedEnvironments.has(environment?.name ?? "default")) return;
|
|
478
518
|
if (!session.transformedFiles.size) return;
|
|
519
|
+
/**
|
|
520
|
+
* An SSR bundle emits no CSS assets, and is not supposed to.
|
|
521
|
+
*
|
|
522
|
+
* `build.ssrEmitAssets` is off by default, so Vite discards them: the client build is
|
|
523
|
+
* what carries the stylesheet, and a server bundle that imports `virtual:bamboo.css`
|
|
524
|
+
* from shared code — a root component, a layout — still asks this plugin to load it.
|
|
525
|
+
* Which means the environment *served* the sheet and then emitted nothing, and the
|
|
526
|
+
* check below read that as the failure it exists to catch.
|
|
527
|
+
*
|
|
528
|
+
* It fails a build that is entirely correct. Qwik's `vite build --ssr` is the shape
|
|
529
|
+
* that showed it: 7/7 calls compiled, the client bundle carrying the stylesheet, and
|
|
530
|
+
* the server bundle refusing to finish. React Router does not hit it only because its
|
|
531
|
+
* plugin turns `ssrEmitAssets` on.
|
|
532
|
+
*
|
|
533
|
+
* Read per environment where that exists, falling back to the run's own config, so
|
|
534
|
+
* Vite 5's single-config builds are answered by the same question.
|
|
535
|
+
*/
|
|
536
|
+
const buildOptions = environment?.config?.build ?? ssrBuildOptions;
|
|
537
|
+
if (buildOptions?.ssr && !buildOptions.ssrEmitAssets) return;
|
|
479
538
|
if (!Object.values(bundle).some((output) => {
|
|
480
539
|
if (!carriesGeneratedCss(output)) return false;
|
|
481
540
|
return (typeof output.source === "string" ? output.source : Buffer.from(output.source).toString()).includes("--made-with-bamboo");
|
|
@@ -3003,6 +3062,18 @@ const isGeneratedOutput = (filePath, ctx) => {
|
|
|
3003
3062
|
};
|
|
3004
3063
|
/** 1-indexed line of a source offset, for an error a user can navigate to. */
|
|
3005
3064
|
const lineAt = (code, offset) => code.slice(0, offset).split("\n").length;
|
|
3065
|
+
/**
|
|
3066
|
+
* One spelling for a path used as a map key against paths from somewhere else.
|
|
3067
|
+
*
|
|
3068
|
+
* The fold reports dependencies as ts-morph sees them and Vite reports a changed file as its
|
|
3069
|
+
* watcher saw it. On Windows those differ by separator, and can differ by the case of the
|
|
3070
|
+
* drive letter alone — chokidar reports what the OS handed it, `path.resolve` preserves
|
|
3071
|
+
* whatever the cwd had. Either would make every lookup below miss and restore the exact
|
|
3072
|
+
* staleness they exist to fix, silently, since a miss is indistinguishable from a module that
|
|
3073
|
+
* folded nothing. Only the drive letter is case-folded: the rest of the path is compared as
|
|
3074
|
+
* written, because elsewhere the filesystem may well be case-sensitive.
|
|
3075
|
+
*/
|
|
3076
|
+
const normalizeFsPath = (file) => resolve(file).replaceAll("\\", "/").replace(/^[a-z]:\//, (drive) => drive.toUpperCase());
|
|
3006
3077
|
const formatSkipped = (id, skipped) => {
|
|
3007
3078
|
const counts = /* @__PURE__ */ new Map();
|
|
3008
3079
|
for (const entry of skipped) counts.set(entry.reason, (counts.get(entry.reason) ?? 0) + 1);
|
|
@@ -3023,6 +3094,7 @@ const formatSkipped = (id, skipped) => {
|
|
|
3023
3094
|
*/
|
|
3024
3095
|
const bamboocss = (options = {}) => {
|
|
3025
3096
|
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
|
|
3097
|
+
markStaticCompilerActive();
|
|
3026
3098
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
3027
3099
|
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
3100
|
/**
|
|
@@ -3084,6 +3156,84 @@ const bamboocss = (options = {}) => {
|
|
|
3084
3156
|
* would otherwise re-parse its module fifty times, which is the transform path.
|
|
3085
3157
|
*/
|
|
3086
3158
|
const recipeConfigCache = /* @__PURE__ */ new Map();
|
|
3159
|
+
/**
|
|
3160
|
+
* Which modules folded a value read out of which other module, for dev invalidation.
|
|
3161
|
+
*
|
|
3162
|
+
* `addWatchFile` reports the same edges, and in a build that is enough — Rollup discards a
|
|
3163
|
+
* module whose watched file changed. Vite's dev server does not: a module that *statically
|
|
3164
|
+
* imports* the changed one is only **soft**-invalidated, which by design keeps its cached
|
|
3165
|
+
* transform result and rewrites nothing but the timestamps on its import specifiers. That
|
|
3166
|
+
* cached result is where the compiled class string lives, so the edit never reaches it.
|
|
3167
|
+
*
|
|
3168
|
+
* The recipe case is the one users meet, because it is the one where the class is compiled
|
|
3169
|
+
* into somebody else's module: an inline `cva` declaration is erased, and each *call site*
|
|
3170
|
+
* becomes a literal in the module that calls it. Editing the recipe then updates the class
|
|
3171
|
+
* in a module Vite has decided not to re-transform, so the browser and the SSR render keep
|
|
3172
|
+
* the old class — with no error, and with Vite and Bamboo both logging as if the edit landed.
|
|
3173
|
+
* A restart applies it, which is what makes it read as "recipes do not hot-reload".
|
|
3174
|
+
*
|
|
3175
|
+
* `css(sharedObject)` across modules fails identically; it is rarer only because a consumer
|
|
3176
|
+
* that folds *nothing but* recipe calls has its import erased, and an erased import is not a
|
|
3177
|
+
* static one, so Vite hard-invalidates it and the bug hides. Import one more value from the
|
|
3178
|
+
* same module — the shape any real `ui.ts` has — and the import survives, and so does the
|
|
3179
|
+
* stale class.
|
|
3180
|
+
*
|
|
3181
|
+
* Keyed by dependency, since "what changed" is the question asked, and tracked in the other
|
|
3182
|
+
* direction as well so a re-transform can retract edges the module no longer has.
|
|
3183
|
+
*/
|
|
3184
|
+
const dependentsByDependency = /* @__PURE__ */ new Map();
|
|
3185
|
+
const dependenciesByFile = /* @__PURE__ */ new Map();
|
|
3186
|
+
const recordFoldDependencies = (file, dependencies) => {
|
|
3187
|
+
const next = new Set(dependencies.map(normalizeFsPath).filter((dependency) => dependency !== file));
|
|
3188
|
+
const previous = dependenciesByFile.get(file);
|
|
3189
|
+
for (const dependency of previous ?? []) {
|
|
3190
|
+
if (next.has(dependency)) continue;
|
|
3191
|
+
const dependents = dependentsByDependency.get(dependency);
|
|
3192
|
+
if (!dependents?.delete(file)) continue;
|
|
3193
|
+
if (!dependents.size) dependentsByDependency.delete(dependency);
|
|
3194
|
+
}
|
|
3195
|
+
if (!next.size) {
|
|
3196
|
+
dependenciesByFile.delete(file);
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
dependenciesByFile.set(file, next);
|
|
3200
|
+
for (const dependency of next) {
|
|
3201
|
+
const dependents = dependentsByDependency.get(dependency);
|
|
3202
|
+
if (dependents) dependents.add(file);
|
|
3203
|
+
else dependentsByDependency.set(dependency, new Set([file]));
|
|
3204
|
+
}
|
|
3205
|
+
};
|
|
3206
|
+
/**
|
|
3207
|
+
* Modules to re-transform because `file` changed, hard-invalidated on the way out.
|
|
3208
|
+
*
|
|
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.
|
|
3212
|
+
*
|
|
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.
|
|
3216
|
+
*
|
|
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.
|
|
3224
|
+
*/
|
|
3225
|
+
const foldDependentModules = (file, modules, graph) => {
|
|
3226
|
+
const dependents = dependentsByDependency.get(normalizeFsPath(file));
|
|
3227
|
+
if (!dependents?.size) return;
|
|
3228
|
+
const added = [];
|
|
3229
|
+
for (const dependent of dependents) for (const module of graph.getModulesByFile(dependent) ?? []) {
|
|
3230
|
+
if (modules.includes(module) || added.includes(module)) continue;
|
|
3231
|
+
graph.invalidateModule(module);
|
|
3232
|
+
added.push(module);
|
|
3233
|
+
}
|
|
3234
|
+
if (!added.length) return;
|
|
3235
|
+
return [...modules, ...added];
|
|
3236
|
+
};
|
|
3087
3237
|
let ctx;
|
|
3088
3238
|
let runtimeCss;
|
|
3089
3239
|
let styleCompiler;
|
|
@@ -3119,6 +3269,8 @@ const bamboocss = (options = {}) => {
|
|
|
3119
3269
|
perFile.clear();
|
|
3120
3270
|
survivorsByFile.clear();
|
|
3121
3271
|
recipeConfigCache.clear();
|
|
3272
|
+
dependentsByDependency.clear();
|
|
3273
|
+
dependenciesByFile.clear();
|
|
3122
3274
|
resetStaticCompilationSession(staticSession);
|
|
3123
3275
|
}
|
|
3124
3276
|
staticSession.startedEnvironments.add(environment);
|
|
@@ -3156,10 +3308,40 @@ const bamboocss = (options = {}) => {
|
|
|
3156
3308
|
recipeConfigCache.clear();
|
|
3157
3309
|
if (change.event === "delete") {
|
|
3158
3310
|
ctx.project.removeSourceFile(filePath);
|
|
3311
|
+
recordFoldDependencies(normalizeFsPath(filePath), []);
|
|
3159
3312
|
return;
|
|
3160
3313
|
}
|
|
3161
3314
|
ctx.project.reloadSourceFile(filePath);
|
|
3162
3315
|
},
|
|
3316
|
+
/**
|
|
3317
|
+
* Re-transform whatever folded a value out of the file that just changed.
|
|
3318
|
+
*
|
|
3319
|
+
* Dev only — `hotUpdate` does not run in a build, where Rollup's own invalidation already
|
|
3320
|
+
* covers this — and additive: the modules Vite matched are returned alongside, so this
|
|
3321
|
+
* decides nothing about them.
|
|
3322
|
+
*
|
|
3323
|
+
* `handleHotUpdate` below stands in on Vite 5, which has no `hotUpdate`. Not quite the
|
|
3324
|
+
* same thing: Vite 5 calls that hook for an update and not for a file appearing or being
|
|
3325
|
+
* deleted, so a recipe file *created* while the server runs leaves its consumers stale
|
|
3326
|
+
* there. Vite 6 and up call `hotUpdate` for all three, and never call `handleHotUpdate`
|
|
3327
|
+
* when a plugin has both — including its deprecation warning — so exactly one of the two
|
|
3328
|
+
* runs on any supported version.
|
|
3329
|
+
*
|
|
3330
|
+
* `environment` optional-chained for the same reason `addWatchFile` is in `transform`:
|
|
3331
|
+
* a harness driving the hook need not supply a full plugin context, and a `TypeError`
|
|
3332
|
+
* here is swallowed into an HMR error payload that a middleware-mode server sends
|
|
3333
|
+
* nowhere.
|
|
3334
|
+
*/
|
|
3335
|
+
hotUpdate({ file, modules }) {
|
|
3336
|
+
const graph = this.environment?.moduleGraph;
|
|
3337
|
+
if (!graph) return;
|
|
3338
|
+
return foldDependentModules(file, modules, graph);
|
|
3339
|
+
},
|
|
3340
|
+
handleHotUpdate({ file, modules, server }) {
|
|
3341
|
+
const legacy = server;
|
|
3342
|
+
if (legacy.environments) return;
|
|
3343
|
+
return foldDependentModules(file, modules, legacy.moduleGraph);
|
|
3344
|
+
},
|
|
3163
3345
|
async transform(code, id) {
|
|
3164
3346
|
if (!shouldTransform(id)) return null;
|
|
3165
3347
|
try {
|
|
@@ -3227,6 +3409,7 @@ const bamboocss = (options = {}) => {
|
|
|
3227
3409
|
}
|
|
3228
3410
|
if (reportSkipped && result.skipped.length) logger.info("vite:transform", formatSkipped(filePath, result.skipped));
|
|
3229
3411
|
for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
|
|
3412
|
+
recordFoldDependencies(normalizeFsPath(filePath), result.dependencies);
|
|
3230
3413
|
const forFile = survivorsByFile.get(filePath);
|
|
3231
3414
|
if (command === "serve" && forFile?.length) throw createSurvivorError(forFile);
|
|
3232
3415
|
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.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.
|
|
44
|
-
"@bamboocss/
|
|
45
|
-
"@bamboocss/
|
|
46
|
-
"@bamboocss/
|
|
47
|
-
"@bamboocss/
|
|
48
|
-
"@bamboocss/shared": "1.
|
|
49
|
-
"@bamboocss/types": "1.
|
|
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"
|
|
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.1"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vite": ">=5"
|