@bamboocss/vite 1.45.2 → 1.45.4
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 +196 -6
- package/dist/index.mjs +196 -6
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,8 @@ let postcss = require("postcss");
|
|
|
36
36
|
postcss = __toESM(postcss);
|
|
37
37
|
let postcss_selector_parser = require("postcss-selector-parser");
|
|
38
38
|
postcss_selector_parser = __toESM(postcss_selector_parser);
|
|
39
|
+
let node_crypto = require("node:crypto");
|
|
40
|
+
let node_fs = require("node:fs");
|
|
39
41
|
let node_path = require("node:path");
|
|
40
42
|
let _bamboocss_config_ts_path = require("@bamboocss/config/ts-path");
|
|
41
43
|
let _bamboocss_extractor = require("@bamboocss/extractor");
|
|
@@ -3321,6 +3323,141 @@ const bamboocss = (options = {}) => {
|
|
|
3321
3323
|
}
|
|
3322
3324
|
};
|
|
3323
3325
|
/**
|
|
3326
|
+
* What each cross-file consumer was last handed, and what it last compiled to.
|
|
3327
|
+
*
|
|
3328
|
+
* Editing a shared module re-transforms everything that folded a value out of it, and most of
|
|
3329
|
+
* those re-transforms recompute the bytes they already had: an edit to one export changes the
|
|
3330
|
+
* consumers reading *that* export, not the ones reading something else from the same file.
|
|
3331
|
+
* `foldDependentModules` uses this to tell those two apart.
|
|
3332
|
+
*
|
|
3333
|
+
* Digests, not text. Retaining every consumer's source and compiled output for the life of the
|
|
3334
|
+
* process is the same order of memory as the ts-morph project already holding it; two 44-byte
|
|
3335
|
+
* strings per entry is not, and it does not grow with module size. The set is bounded the way
|
|
3336
|
+
* `dependenciesByFile` is — an entry exists only while a module's fold actually reads another
|
|
3337
|
+
* file, which most modules never do — so a project that folds nothing across a boundary pays
|
|
3338
|
+
* neither the bytes nor the hashing.
|
|
3339
|
+
*
|
|
3340
|
+
* The *input* digest is what makes the output digest safe to act on. The check below re-folds
|
|
3341
|
+
* a consumer from disk, and disk is the right text only if that is what the last transform was
|
|
3342
|
+
* handed: a module built by another plugin's `load`, or one edited in the same save, fails
|
|
3343
|
+
* that comparison and is treated as changed. `path` is the spelling `transform` used, because
|
|
3344
|
+
* ts-morph and the fold both key on it and Windows spells it more than one way.
|
|
3345
|
+
*/
|
|
3346
|
+
const foldSignatures = /* @__PURE__ */ new Map();
|
|
3347
|
+
const digest = (text) => (0, node_crypto.createHash)("sha256").update(text).digest("base64");
|
|
3348
|
+
/**
|
|
3349
|
+
* Consumers this edit cannot move, resolved once per watcher event.
|
|
3350
|
+
*
|
|
3351
|
+
* `hotUpdate` runs once per environment — client and ssr both — and the answer is a property
|
|
3352
|
+
* of the files, not of which environment is asking. Cleared in `watchChange`, which every Vite
|
|
3353
|
+
* in the peer range calls for every file event, before either update hook.
|
|
3354
|
+
*/
|
|
3355
|
+
const unchangedFolds = /* @__PURE__ */ new Map();
|
|
3356
|
+
/**
|
|
3357
|
+
* How many dependents in a row may come back changed before the check gives up for this event.
|
|
3358
|
+
*
|
|
3359
|
+
* The check costs a re-fold — ~0.2 ms per dependent measured on a twenty-consumer fan-out — and
|
|
3360
|
+
* it is paid on the awaited path, before Vite is told anything. That is linear in the number of
|
|
3361
|
+
* consumers and does not bound itself: a shared module with three hundred of them adds ~59 ms to
|
|
3362
|
+
* every edit, including the edits where nothing *can* be suppressed because the change moved the
|
|
3363
|
+
* recipe base and every consumer really did recompile.
|
|
3364
|
+
*
|
|
3365
|
+
* The trade is worth it whenever anything is suppressible. Measured on the same fan-out, a
|
|
3366
|
+
* suppressed consumer saves ~2.3 ms of re-transform for the ~0.23 ms its check costs, so the
|
|
3367
|
+
* check pays for itself at about one consumer in ten. What does not pay is the case where the
|
|
3368
|
+
* answer is going to be "changed" for all of them, and that case announces itself: a run of
|
|
3369
|
+
* consumers that all came back changed. Stopping after eight bounds it. On a three-hundred
|
|
3370
|
+
* consumer fan-out, an edit to the recipe base — where nothing can be suppressed — costs 3.6 ms
|
|
3371
|
+
* here rather than the 59 ms checking every one of them would, stacked onto an edit already
|
|
3372
|
+
* spending 600 ms re-transforming. A single unchanged consumer resets the run, so the same
|
|
3373
|
+
* fan-out editing a value no fold reads still checks all three hundred: 49 ms spent against
|
|
3374
|
+
* 520 ms of re-transform not done, and a 715 ms edit reduced to 183 ms.
|
|
3375
|
+
*
|
|
3376
|
+
* Giving up is always the safe direction — an unchecked dependent is treated as changed, which
|
|
3377
|
+
* is what this path did before any of this existed — so the bound can only cost the
|
|
3378
|
+
* optimisation, never correctness. It costs it where the optimisation was worth least: for the
|
|
3379
|
+
* run to reach eight, the consumers seen so far have to be uniformly changed.
|
|
3380
|
+
*/
|
|
3381
|
+
const CHANGED_RUN_LIMIT = 8;
|
|
3382
|
+
let changedRun = 0;
|
|
3383
|
+
/**
|
|
3384
|
+
* Whether re-folding `dependent` now produces exactly the bytes it produced last time.
|
|
3385
|
+
*
|
|
3386
|
+
* Only the fold can answer that. The consumer's own source has not changed, so whether its
|
|
3387
|
+
* compiled output moves depends entirely on what the edited module resolves to *this* time,
|
|
3388
|
+
* and there is no cheaper way to learn that than to resolve it. Doing it here rather than
|
|
3389
|
+
* waiting for the re-transform is the whole point: the decision is needed before the update is
|
|
3390
|
+
* announced, and for a consumer that turns out to be unchanged this fold *replaces* the
|
|
3391
|
+
* re-transform rather than adding to it.
|
|
3392
|
+
*
|
|
3393
|
+
* Conservative in every failure — no recorded signature, a file that will not read, a parse
|
|
3394
|
+
* that returns nothing, a fold that throws — because "changed" is what this path did before,
|
|
3395
|
+
* and a wrong "unchanged" is a stale class string in the browser.
|
|
3396
|
+
*/
|
|
3397
|
+
const foldOutputUnchanged = (dependent) => {
|
|
3398
|
+
const memoized = unchangedFolds.get(dependent);
|
|
3399
|
+
if (memoized !== void 0) return memoized;
|
|
3400
|
+
const unchanged = changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(dependent);
|
|
3401
|
+
changedRun = unchanged ? 0 : changedRun + 1;
|
|
3402
|
+
unchangedFolds.set(dependent, unchanged);
|
|
3403
|
+
return unchanged;
|
|
3404
|
+
};
|
|
3405
|
+
const refoldMatchesSignature = (dependent) => {
|
|
3406
|
+
const signature = foldSignatures.get(dependent);
|
|
3407
|
+
if (!signature || !ctx || !runtimeCss || !styleCompiler) return false;
|
|
3408
|
+
try {
|
|
3409
|
+
const code = (0, node_fs.readFileSync)(signature.path, "utf8");
|
|
3410
|
+
if (digest(code) !== signature.input) return false;
|
|
3411
|
+
const sourceFile = ctx.project.addSourceFile(signature.path, code);
|
|
3412
|
+
const parserResult = ctx.project.parseSourceFile(signature.path);
|
|
3413
|
+
if (!parserResult) return false;
|
|
3414
|
+
const result = foldSource({
|
|
3415
|
+
ctx,
|
|
3416
|
+
code,
|
|
3417
|
+
parserResult,
|
|
3418
|
+
filePath: signature.path,
|
|
3419
|
+
runtimeCss,
|
|
3420
|
+
styleCompiler,
|
|
3421
|
+
maxRecipeStates,
|
|
3422
|
+
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
3423
|
+
recipeConfigCache,
|
|
3424
|
+
reportSurvivors: false,
|
|
3425
|
+
sourceFile
|
|
3426
|
+
});
|
|
3427
|
+
const unchanged = digest(result.code) === signature.output;
|
|
3428
|
+
/**
|
|
3429
|
+
* Edges re-recorded on the way to suppressing a module, and never on the way to
|
|
3430
|
+
* invalidating one.
|
|
3431
|
+
*
|
|
3432
|
+
* The first is necessary: *which* files a module folds from can move while the bytes it
|
|
3433
|
+
* emits do not — a value now re-exported through a different module, say — and suppressing
|
|
3434
|
+
* the announcement means no transform will run to notice. Leaving the old edges would make
|
|
3435
|
+
* the next edit to the new dependency reach nobody.
|
|
3436
|
+
*
|
|
3437
|
+
* The second would be a bug, and is the reason this is a branch rather than an
|
|
3438
|
+
* unconditional write. `hotUpdate` runs once per environment, client then ssr, against one
|
|
3439
|
+
* shared map. A fold that now yields nothing — an export renamed, a call commented out, any
|
|
3440
|
+
* ordinary mid-edit state — returns `dependencies: []`, so writing it here would retract the
|
|
3441
|
+
* consumer's edge during the *client* pass and leave the *ssr* pass finding an empty set and
|
|
3442
|
+
* returning without invalidating anything: the stale compiled class kept in the SSR cache,
|
|
3443
|
+
* with the client half correctly updated. That is the shape of the bug the self-accepting
|
|
3444
|
+
* fix was about, one environment over. `transform` performs the same retraction, but only
|
|
3445
|
+
* after every environment has already invalidated, which is why it is safe there.
|
|
3446
|
+
*
|
|
3447
|
+
* Confining the write to the unchanged branch removes the hazard rather than sequencing
|
|
3448
|
+
* around it, because a retraction cannot reach that branch: `dependencies` is empty only
|
|
3449
|
+
* when `folded.length === 0`, and a fold that folds nothing returns the module's own source,
|
|
3450
|
+
* which cannot equal an output digest recorded from a pass that replaced a call with a
|
|
3451
|
+
* literal. And where an unchanged verdict *does* narrow the edges, both passes agree anyway
|
|
3452
|
+
* — neither invalidates a module it has just called unchanged.
|
|
3453
|
+
*/
|
|
3454
|
+
if (unchanged) recordFoldDependencies(dependent, result.dependencies);
|
|
3455
|
+
return unchanged;
|
|
3456
|
+
} catch {
|
|
3457
|
+
return false;
|
|
3458
|
+
}
|
|
3459
|
+
};
|
|
3460
|
+
/**
|
|
3324
3461
|
* Modules to re-transform because `file` changed, hard-invalidated on the way out.
|
|
3325
3462
|
*
|
|
3326
3463
|
* Invalidating is the fix. It drops the stale compiled result, which is the defect itself,
|
|
@@ -3355,10 +3492,46 @@ const bamboocss = (options = {}) => {
|
|
|
3355
3492
|
const dependents = dependentsByDependency.get(normalizeFsPath(file));
|
|
3356
3493
|
if (!dependents?.size) return;
|
|
3357
3494
|
const added = [];
|
|
3358
|
-
for (const dependent of dependents
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3495
|
+
for (const dependent of [...dependents]) {
|
|
3496
|
+
/**
|
|
3497
|
+
* A consumer whose compiled bytes this edit does not move is left entirely alone.
|
|
3498
|
+
*
|
|
3499
|
+
* Announcing one tells the browser to refetch a module it already has verbatim: a round
|
|
3500
|
+
* trip, and behind it — in a framework that re-drives HMR per entry, as react-router does
|
|
3501
|
+
* with `reloadModule` in both its client and its ssr pass — a router revalidation.
|
|
3502
|
+
*
|
|
3503
|
+
* Skipping the *invalidation* follows from the same fact, and is where the cost actually
|
|
3504
|
+
* sits. Vite only soft-invalidates a module that statically imports the changed one, which
|
|
3505
|
+
* keeps its cached transform result and re-serves it for the price of rewriting import
|
|
3506
|
+
* timestamps; hard-invalidating turns that into a full re-transform through every plugin
|
|
3507
|
+
* in the chain. On a fan-out of twenty consumers of one shared module, editing a runtime
|
|
3508
|
+
* value no fold reads took the transforms Bamboo runs for that edit from 22 to 2.
|
|
3509
|
+
*
|
|
3510
|
+
* How much that is worth depends on whether the consumer's import statement *survives* the
|
|
3511
|
+
* fold. When it imports nothing from the module but the value being folded, the binding
|
|
3512
|
+
* goes dead, esbuild drops the statement, and the only edge left is the non-static one
|
|
3513
|
+
* `addWatchFile` created — which Vite hard-invalidates by itself, so declining to here
|
|
3514
|
+
* saves nothing and the re-fold is pure cost. Import one more thing from the same module,
|
|
3515
|
+
* which is the shape a real shared `ui.ts` has, and the statement stays, the consumer is a
|
|
3516
|
+
* static importer, and Bamboo's invalidation is the only reason it re-transforms at all.
|
|
3517
|
+
*
|
|
3518
|
+
* Safe by what "identical" means. The invalidation exists to drop a compiled class string
|
|
3519
|
+
* that no longer matches the source it was compiled from; when recompiling produces the
|
|
3520
|
+
* same string, there is nothing stale to drop. Whichever way the module is reached next —
|
|
3521
|
+
* Vite's own propagation, a later request, or nothing at all — the bytes it yields are the
|
|
3522
|
+
* bytes this fold just computed.
|
|
3523
|
+
*
|
|
3524
|
+
* Nor can it mask an update Vite would have sent by itself. This only ever withholds a
|
|
3525
|
+
* name Bamboo added; `modules` is passed through untouched. A consumer that also *imports*
|
|
3526
|
+
* the edited module for a runtime value is still reached by `propagateUpdate` exactly as
|
|
3527
|
+
* it would be with no plugin here at all — that direction was never this list's to decide.
|
|
3528
|
+
*/
|
|
3529
|
+
if (foldOutputUnchanged(dependent)) continue;
|
|
3530
|
+
for (const module of graph.getModulesByFile(dependent) ?? []) {
|
|
3531
|
+
if (modules.includes(module) || added.includes(module)) continue;
|
|
3532
|
+
graph.invalidateModule(module);
|
|
3533
|
+
added.push(module);
|
|
3534
|
+
}
|
|
3362
3535
|
}
|
|
3363
3536
|
if (!added.length) return;
|
|
3364
3537
|
/**
|
|
@@ -3420,6 +3593,9 @@ const bamboocss = (options = {}) => {
|
|
|
3420
3593
|
recipeConfigCache.clear();
|
|
3421
3594
|
dependentsByDependency.clear();
|
|
3422
3595
|
dependenciesByFile.clear();
|
|
3596
|
+
foldSignatures.clear();
|
|
3597
|
+
unchangedFolds.clear();
|
|
3598
|
+
changedRun = 0;
|
|
3423
3599
|
resetStaticCompilationSession(staticSession);
|
|
3424
3600
|
}
|
|
3425
3601
|
staticSession.startedEnvironments.add(environment);
|
|
@@ -3450,6 +3626,8 @@ const bamboocss = (options = {}) => {
|
|
|
3450
3626
|
* the parser still holds the file.
|
|
3451
3627
|
*/
|
|
3452
3628
|
watchChange(id, change) {
|
|
3629
|
+
unchangedFolds.clear();
|
|
3630
|
+
changedRun = 0;
|
|
3453
3631
|
if (!ctx) return;
|
|
3454
3632
|
if (!shouldTransform(id)) return;
|
|
3455
3633
|
const [filePath] = id.split("?");
|
|
@@ -3458,6 +3636,7 @@ const bamboocss = (options = {}) => {
|
|
|
3458
3636
|
if (change.event === "delete") {
|
|
3459
3637
|
ctx.project.removeSourceFile(filePath);
|
|
3460
3638
|
recordFoldDependencies(normalizeFsPath(filePath), []);
|
|
3639
|
+
foldSignatures.delete(normalizeFsPath(filePath));
|
|
3461
3640
|
return;
|
|
3462
3641
|
}
|
|
3463
3642
|
ctx.project.reloadSourceFile(filePath);
|
|
@@ -3526,6 +3705,7 @@ const bamboocss = (options = {}) => {
|
|
|
3526
3705
|
folded: 0,
|
|
3527
3706
|
skipped: new Map([["compile-failed", 1]])
|
|
3528
3707
|
});
|
|
3708
|
+
foldSignatures.delete(normalizeFsPath(filePath));
|
|
3529
3709
|
addSurvivor({
|
|
3530
3710
|
file: filePath,
|
|
3531
3711
|
line: 1,
|
|
@@ -3558,9 +3738,19 @@ const bamboocss = (options = {}) => {
|
|
|
3558
3738
|
}
|
|
3559
3739
|
if (reportSkipped && result.skipped.length) _bamboocss_logger.logger.info("vite:transform", formatSkipped(filePath, result.skipped));
|
|
3560
3740
|
for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
|
|
3561
|
-
|
|
3741
|
+
const dependentKey = normalizeFsPath(filePath);
|
|
3742
|
+
recordFoldDependencies(dependentKey, result.dependencies);
|
|
3743
|
+
if (result.dependencies.length) foldSignatures.set(dependentKey, {
|
|
3744
|
+
input: digest(code),
|
|
3745
|
+
output: digest(result.code),
|
|
3746
|
+
path: filePath
|
|
3747
|
+
});
|
|
3748
|
+
else foldSignatures.delete(dependentKey);
|
|
3562
3749
|
const forFile = survivorsByFile.get(filePath);
|
|
3563
|
-
if (command === "serve" && forFile?.length)
|
|
3750
|
+
if (command === "serve" && forFile?.length) {
|
|
3751
|
+
foldSignatures.delete(dependentKey);
|
|
3752
|
+
throw createSurvivorError(forFile);
|
|
3753
|
+
}
|
|
3564
3754
|
if (!result.folded.length) return null;
|
|
3565
3755
|
_bamboocss_logger.logger.debug("vite:transform", `Compiled ${result.folded.length} call(s) in ${filePath}`);
|
|
3566
3756
|
return {
|
package/dist/index.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import remapping from "@ampproject/remapping";
|
|
|
6
6
|
import MagicString from "magic-string";
|
|
7
7
|
import postcss from "postcss";
|
|
8
8
|
import selectorParser from "postcss-selector-parser";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
9
11
|
import { dirname, relative, resolve } from "node:path";
|
|
10
12
|
import { resolveTsPathPattern } from "@bamboocss/config/ts-path";
|
|
11
13
|
import { box, maybeBoxNode } from "@bamboocss/extractor";
|
|
@@ -3291,6 +3293,141 @@ const bamboocss = (options = {}) => {
|
|
|
3291
3293
|
}
|
|
3292
3294
|
};
|
|
3293
3295
|
/**
|
|
3296
|
+
* What each cross-file consumer was last handed, and what it last compiled to.
|
|
3297
|
+
*
|
|
3298
|
+
* Editing a shared module re-transforms everything that folded a value out of it, and most of
|
|
3299
|
+
* those re-transforms recompute the bytes they already had: an edit to one export changes the
|
|
3300
|
+
* consumers reading *that* export, not the ones reading something else from the same file.
|
|
3301
|
+
* `foldDependentModules` uses this to tell those two apart.
|
|
3302
|
+
*
|
|
3303
|
+
* Digests, not text. Retaining every consumer's source and compiled output for the life of the
|
|
3304
|
+
* process is the same order of memory as the ts-morph project already holding it; two 44-byte
|
|
3305
|
+
* strings per entry is not, and it does not grow with module size. The set is bounded the way
|
|
3306
|
+
* `dependenciesByFile` is — an entry exists only while a module's fold actually reads another
|
|
3307
|
+
* file, which most modules never do — so a project that folds nothing across a boundary pays
|
|
3308
|
+
* neither the bytes nor the hashing.
|
|
3309
|
+
*
|
|
3310
|
+
* The *input* digest is what makes the output digest safe to act on. The check below re-folds
|
|
3311
|
+
* a consumer from disk, and disk is the right text only if that is what the last transform was
|
|
3312
|
+
* handed: a module built by another plugin's `load`, or one edited in the same save, fails
|
|
3313
|
+
* that comparison and is treated as changed. `path` is the spelling `transform` used, because
|
|
3314
|
+
* ts-morph and the fold both key on it and Windows spells it more than one way.
|
|
3315
|
+
*/
|
|
3316
|
+
const foldSignatures = /* @__PURE__ */ new Map();
|
|
3317
|
+
const digest = (text) => createHash("sha256").update(text).digest("base64");
|
|
3318
|
+
/**
|
|
3319
|
+
* Consumers this edit cannot move, resolved once per watcher event.
|
|
3320
|
+
*
|
|
3321
|
+
* `hotUpdate` runs once per environment — client and ssr both — and the answer is a property
|
|
3322
|
+
* of the files, not of which environment is asking. Cleared in `watchChange`, which every Vite
|
|
3323
|
+
* in the peer range calls for every file event, before either update hook.
|
|
3324
|
+
*/
|
|
3325
|
+
const unchangedFolds = /* @__PURE__ */ new Map();
|
|
3326
|
+
/**
|
|
3327
|
+
* How many dependents in a row may come back changed before the check gives up for this event.
|
|
3328
|
+
*
|
|
3329
|
+
* The check costs a re-fold — ~0.2 ms per dependent measured on a twenty-consumer fan-out — and
|
|
3330
|
+
* it is paid on the awaited path, before Vite is told anything. That is linear in the number of
|
|
3331
|
+
* consumers and does not bound itself: a shared module with three hundred of them adds ~59 ms to
|
|
3332
|
+
* every edit, including the edits where nothing *can* be suppressed because the change moved the
|
|
3333
|
+
* recipe base and every consumer really did recompile.
|
|
3334
|
+
*
|
|
3335
|
+
* The trade is worth it whenever anything is suppressible. Measured on the same fan-out, a
|
|
3336
|
+
* suppressed consumer saves ~2.3 ms of re-transform for the ~0.23 ms its check costs, so the
|
|
3337
|
+
* check pays for itself at about one consumer in ten. What does not pay is the case where the
|
|
3338
|
+
* answer is going to be "changed" for all of them, and that case announces itself: a run of
|
|
3339
|
+
* consumers that all came back changed. Stopping after eight bounds it. On a three-hundred
|
|
3340
|
+
* consumer fan-out, an edit to the recipe base — where nothing can be suppressed — costs 3.6 ms
|
|
3341
|
+
* here rather than the 59 ms checking every one of them would, stacked onto an edit already
|
|
3342
|
+
* spending 600 ms re-transforming. A single unchanged consumer resets the run, so the same
|
|
3343
|
+
* fan-out editing a value no fold reads still checks all three hundred: 49 ms spent against
|
|
3344
|
+
* 520 ms of re-transform not done, and a 715 ms edit reduced to 183 ms.
|
|
3345
|
+
*
|
|
3346
|
+
* Giving up is always the safe direction — an unchecked dependent is treated as changed, which
|
|
3347
|
+
* is what this path did before any of this existed — so the bound can only cost the
|
|
3348
|
+
* optimisation, never correctness. It costs it where the optimisation was worth least: for the
|
|
3349
|
+
* run to reach eight, the consumers seen so far have to be uniformly changed.
|
|
3350
|
+
*/
|
|
3351
|
+
const CHANGED_RUN_LIMIT = 8;
|
|
3352
|
+
let changedRun = 0;
|
|
3353
|
+
/**
|
|
3354
|
+
* Whether re-folding `dependent` now produces exactly the bytes it produced last time.
|
|
3355
|
+
*
|
|
3356
|
+
* Only the fold can answer that. The consumer's own source has not changed, so whether its
|
|
3357
|
+
* compiled output moves depends entirely on what the edited module resolves to *this* time,
|
|
3358
|
+
* and there is no cheaper way to learn that than to resolve it. Doing it here rather than
|
|
3359
|
+
* waiting for the re-transform is the whole point: the decision is needed before the update is
|
|
3360
|
+
* announced, and for a consumer that turns out to be unchanged this fold *replaces* the
|
|
3361
|
+
* re-transform rather than adding to it.
|
|
3362
|
+
*
|
|
3363
|
+
* Conservative in every failure — no recorded signature, a file that will not read, a parse
|
|
3364
|
+
* that returns nothing, a fold that throws — because "changed" is what this path did before,
|
|
3365
|
+
* and a wrong "unchanged" is a stale class string in the browser.
|
|
3366
|
+
*/
|
|
3367
|
+
const foldOutputUnchanged = (dependent) => {
|
|
3368
|
+
const memoized = unchangedFolds.get(dependent);
|
|
3369
|
+
if (memoized !== void 0) return memoized;
|
|
3370
|
+
const unchanged = changedRun < CHANGED_RUN_LIMIT && refoldMatchesSignature(dependent);
|
|
3371
|
+
changedRun = unchanged ? 0 : changedRun + 1;
|
|
3372
|
+
unchangedFolds.set(dependent, unchanged);
|
|
3373
|
+
return unchanged;
|
|
3374
|
+
};
|
|
3375
|
+
const refoldMatchesSignature = (dependent) => {
|
|
3376
|
+
const signature = foldSignatures.get(dependent);
|
|
3377
|
+
if (!signature || !ctx || !runtimeCss || !styleCompiler) return false;
|
|
3378
|
+
try {
|
|
3379
|
+
const code = readFileSync(signature.path, "utf8");
|
|
3380
|
+
if (digest(code) !== signature.input) return false;
|
|
3381
|
+
const sourceFile = ctx.project.addSourceFile(signature.path, code);
|
|
3382
|
+
const parserResult = ctx.project.parseSourceFile(signature.path);
|
|
3383
|
+
if (!parserResult) return false;
|
|
3384
|
+
const result = foldSource({
|
|
3385
|
+
ctx,
|
|
3386
|
+
code,
|
|
3387
|
+
parserResult,
|
|
3388
|
+
filePath: signature.path,
|
|
3389
|
+
runtimeCss,
|
|
3390
|
+
styleCompiler,
|
|
3391
|
+
maxRecipeStates,
|
|
3392
|
+
parseModule: (path) => ctx?.project.parseSourceFile(path),
|
|
3393
|
+
recipeConfigCache,
|
|
3394
|
+
reportSurvivors: false,
|
|
3395
|
+
sourceFile
|
|
3396
|
+
});
|
|
3397
|
+
const unchanged = digest(result.code) === signature.output;
|
|
3398
|
+
/**
|
|
3399
|
+
* Edges re-recorded on the way to suppressing a module, and never on the way to
|
|
3400
|
+
* invalidating one.
|
|
3401
|
+
*
|
|
3402
|
+
* The first is necessary: *which* files a module folds from can move while the bytes it
|
|
3403
|
+
* emits do not — a value now re-exported through a different module, say — and suppressing
|
|
3404
|
+
* the announcement means no transform will run to notice. Leaving the old edges would make
|
|
3405
|
+
* the next edit to the new dependency reach nobody.
|
|
3406
|
+
*
|
|
3407
|
+
* The second would be a bug, and is the reason this is a branch rather than an
|
|
3408
|
+
* unconditional write. `hotUpdate` runs once per environment, client then ssr, against one
|
|
3409
|
+
* shared map. A fold that now yields nothing — an export renamed, a call commented out, any
|
|
3410
|
+
* ordinary mid-edit state — returns `dependencies: []`, so writing it here would retract the
|
|
3411
|
+
* consumer's edge during the *client* pass and leave the *ssr* pass finding an empty set and
|
|
3412
|
+
* returning without invalidating anything: the stale compiled class kept in the SSR cache,
|
|
3413
|
+
* with the client half correctly updated. That is the shape of the bug the self-accepting
|
|
3414
|
+
* fix was about, one environment over. `transform` performs the same retraction, but only
|
|
3415
|
+
* after every environment has already invalidated, which is why it is safe there.
|
|
3416
|
+
*
|
|
3417
|
+
* Confining the write to the unchanged branch removes the hazard rather than sequencing
|
|
3418
|
+
* around it, because a retraction cannot reach that branch: `dependencies` is empty only
|
|
3419
|
+
* when `folded.length === 0`, and a fold that folds nothing returns the module's own source,
|
|
3420
|
+
* which cannot equal an output digest recorded from a pass that replaced a call with a
|
|
3421
|
+
* literal. And where an unchanged verdict *does* narrow the edges, both passes agree anyway
|
|
3422
|
+
* — neither invalidates a module it has just called unchanged.
|
|
3423
|
+
*/
|
|
3424
|
+
if (unchanged) recordFoldDependencies(dependent, result.dependencies);
|
|
3425
|
+
return unchanged;
|
|
3426
|
+
} catch {
|
|
3427
|
+
return false;
|
|
3428
|
+
}
|
|
3429
|
+
};
|
|
3430
|
+
/**
|
|
3294
3431
|
* Modules to re-transform because `file` changed, hard-invalidated on the way out.
|
|
3295
3432
|
*
|
|
3296
3433
|
* Invalidating is the fix. It drops the stale compiled result, which is the defect itself,
|
|
@@ -3325,10 +3462,46 @@ const bamboocss = (options = {}) => {
|
|
|
3325
3462
|
const dependents = dependentsByDependency.get(normalizeFsPath(file));
|
|
3326
3463
|
if (!dependents?.size) return;
|
|
3327
3464
|
const added = [];
|
|
3328
|
-
for (const dependent of dependents
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3465
|
+
for (const dependent of [...dependents]) {
|
|
3466
|
+
/**
|
|
3467
|
+
* A consumer whose compiled bytes this edit does not move is left entirely alone.
|
|
3468
|
+
*
|
|
3469
|
+
* Announcing one tells the browser to refetch a module it already has verbatim: a round
|
|
3470
|
+
* trip, and behind it — in a framework that re-drives HMR per entry, as react-router does
|
|
3471
|
+
* with `reloadModule` in both its client and its ssr pass — a router revalidation.
|
|
3472
|
+
*
|
|
3473
|
+
* Skipping the *invalidation* follows from the same fact, and is where the cost actually
|
|
3474
|
+
* sits. Vite only soft-invalidates a module that statically imports the changed one, which
|
|
3475
|
+
* keeps its cached transform result and re-serves it for the price of rewriting import
|
|
3476
|
+
* timestamps; hard-invalidating turns that into a full re-transform through every plugin
|
|
3477
|
+
* in the chain. On a fan-out of twenty consumers of one shared module, editing a runtime
|
|
3478
|
+
* value no fold reads took the transforms Bamboo runs for that edit from 22 to 2.
|
|
3479
|
+
*
|
|
3480
|
+
* How much that is worth depends on whether the consumer's import statement *survives* the
|
|
3481
|
+
* fold. When it imports nothing from the module but the value being folded, the binding
|
|
3482
|
+
* goes dead, esbuild drops the statement, and the only edge left is the non-static one
|
|
3483
|
+
* `addWatchFile` created — which Vite hard-invalidates by itself, so declining to here
|
|
3484
|
+
* saves nothing and the re-fold is pure cost. Import one more thing from the same module,
|
|
3485
|
+
* which is the shape a real shared `ui.ts` has, and the statement stays, the consumer is a
|
|
3486
|
+
* static importer, and Bamboo's invalidation is the only reason it re-transforms at all.
|
|
3487
|
+
*
|
|
3488
|
+
* Safe by what "identical" means. The invalidation exists to drop a compiled class string
|
|
3489
|
+
* that no longer matches the source it was compiled from; when recompiling produces the
|
|
3490
|
+
* same string, there is nothing stale to drop. Whichever way the module is reached next —
|
|
3491
|
+
* Vite's own propagation, a later request, or nothing at all — the bytes it yields are the
|
|
3492
|
+
* bytes this fold just computed.
|
|
3493
|
+
*
|
|
3494
|
+
* Nor can it mask an update Vite would have sent by itself. This only ever withholds a
|
|
3495
|
+
* name Bamboo added; `modules` is passed through untouched. A consumer that also *imports*
|
|
3496
|
+
* the edited module for a runtime value is still reached by `propagateUpdate` exactly as
|
|
3497
|
+
* it would be with no plugin here at all — that direction was never this list's to decide.
|
|
3498
|
+
*/
|
|
3499
|
+
if (foldOutputUnchanged(dependent)) continue;
|
|
3500
|
+
for (const module of graph.getModulesByFile(dependent) ?? []) {
|
|
3501
|
+
if (modules.includes(module) || added.includes(module)) continue;
|
|
3502
|
+
graph.invalidateModule(module);
|
|
3503
|
+
added.push(module);
|
|
3504
|
+
}
|
|
3332
3505
|
}
|
|
3333
3506
|
if (!added.length) return;
|
|
3334
3507
|
/**
|
|
@@ -3390,6 +3563,9 @@ const bamboocss = (options = {}) => {
|
|
|
3390
3563
|
recipeConfigCache.clear();
|
|
3391
3564
|
dependentsByDependency.clear();
|
|
3392
3565
|
dependenciesByFile.clear();
|
|
3566
|
+
foldSignatures.clear();
|
|
3567
|
+
unchangedFolds.clear();
|
|
3568
|
+
changedRun = 0;
|
|
3393
3569
|
resetStaticCompilationSession(staticSession);
|
|
3394
3570
|
}
|
|
3395
3571
|
staticSession.startedEnvironments.add(environment);
|
|
@@ -3420,6 +3596,8 @@ const bamboocss = (options = {}) => {
|
|
|
3420
3596
|
* the parser still holds the file.
|
|
3421
3597
|
*/
|
|
3422
3598
|
watchChange(id, change) {
|
|
3599
|
+
unchangedFolds.clear();
|
|
3600
|
+
changedRun = 0;
|
|
3423
3601
|
if (!ctx) return;
|
|
3424
3602
|
if (!shouldTransform(id)) return;
|
|
3425
3603
|
const [filePath] = id.split("?");
|
|
@@ -3428,6 +3606,7 @@ const bamboocss = (options = {}) => {
|
|
|
3428
3606
|
if (change.event === "delete") {
|
|
3429
3607
|
ctx.project.removeSourceFile(filePath);
|
|
3430
3608
|
recordFoldDependencies(normalizeFsPath(filePath), []);
|
|
3609
|
+
foldSignatures.delete(normalizeFsPath(filePath));
|
|
3431
3610
|
return;
|
|
3432
3611
|
}
|
|
3433
3612
|
ctx.project.reloadSourceFile(filePath);
|
|
@@ -3496,6 +3675,7 @@ const bamboocss = (options = {}) => {
|
|
|
3496
3675
|
folded: 0,
|
|
3497
3676
|
skipped: new Map([["compile-failed", 1]])
|
|
3498
3677
|
});
|
|
3678
|
+
foldSignatures.delete(normalizeFsPath(filePath));
|
|
3499
3679
|
addSurvivor({
|
|
3500
3680
|
file: filePath,
|
|
3501
3681
|
line: 1,
|
|
@@ -3528,9 +3708,19 @@ const bamboocss = (options = {}) => {
|
|
|
3528
3708
|
}
|
|
3529
3709
|
if (reportSkipped && result.skipped.length) logger.info("vite:transform", formatSkipped(filePath, result.skipped));
|
|
3530
3710
|
for (const dependency of result.dependencies) this.addWatchFile?.(dependency);
|
|
3531
|
-
|
|
3711
|
+
const dependentKey = normalizeFsPath(filePath);
|
|
3712
|
+
recordFoldDependencies(dependentKey, result.dependencies);
|
|
3713
|
+
if (result.dependencies.length) foldSignatures.set(dependentKey, {
|
|
3714
|
+
input: digest(code),
|
|
3715
|
+
output: digest(result.code),
|
|
3716
|
+
path: filePath
|
|
3717
|
+
});
|
|
3718
|
+
else foldSignatures.delete(dependentKey);
|
|
3532
3719
|
const forFile = survivorsByFile.get(filePath);
|
|
3533
|
-
if (command === "serve" && forFile?.length)
|
|
3720
|
+
if (command === "serve" && forFile?.length) {
|
|
3721
|
+
foldSignatures.delete(dependentKey);
|
|
3722
|
+
throw createSurvivorError(forFile);
|
|
3723
|
+
}
|
|
3534
3724
|
if (!result.folded.length) return null;
|
|
3535
3725
|
logger.debug("vite:transform", `Compiled ${result.folded.length} call(s) in ${filePath}`);
|
|
3536
3726
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/vite",
|
|
3
|
-
"version": "1.45.
|
|
3
|
+
"version": "1.45.4",
|
|
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/node": "1.45.
|
|
47
|
-
"@bamboocss/
|
|
48
|
-
"@bamboocss/shared": "1.45.
|
|
49
|
-
"@bamboocss/types": "1.45.
|
|
43
|
+
"@bamboocss/core": "1.45.4",
|
|
44
|
+
"@bamboocss/config": "1.45.4",
|
|
45
|
+
"@bamboocss/logger": "1.45.4",
|
|
46
|
+
"@bamboocss/node": "1.45.4",
|
|
47
|
+
"@bamboocss/extractor": "1.45.4",
|
|
48
|
+
"@bamboocss/shared": "1.45.4",
|
|
49
|
+
"@bamboocss/types": "1.45.4"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
53
53
|
"vite": "7.2.6",
|
|
54
|
-
"@bamboocss/fixture": "1.45.
|
|
54
|
+
"@bamboocss/fixture": "1.45.4"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vite": ">=5"
|