@bamboocss/vite 1.53.0 → 1.54.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/css-output-module.cjs +411 -17
- package/dist/css-output-module.mjs +409 -18
- package/dist/fold-module.cjs +1 -1
- package/dist/fold-module.mjs +1 -1
- package/dist/index.cjs +346 -51
- package/dist/index.d.cts +11 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.mjs +346 -51
- package/package.json +11 -10
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as bare } from "./class-name.mjs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
2
3
|
import { logger } from "@bamboocss/logger";
|
|
3
4
|
import { esc, truncateList } from "@bamboocss/shared";
|
|
4
5
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
5
6
|
import { readFileSync } from "node:fs";
|
|
6
|
-
import { resolve } from "node:path";
|
|
7
7
|
import { markStaticCompilerActive } from "@bamboocss/node/static-compiler";
|
|
8
8
|
//#region src/lazy-modules.ts
|
|
9
9
|
/**
|
|
@@ -59,6 +59,7 @@ const createCompilationHost = (options = {}) => {
|
|
|
59
59
|
const { configPath, cwd } = options;
|
|
60
60
|
const loadBuilder = options.loadBuilder ?? createLazyBuilder();
|
|
61
61
|
let command = "build";
|
|
62
|
+
let devSourcemap = false;
|
|
62
63
|
let builder;
|
|
63
64
|
let generation;
|
|
64
65
|
let nextGenerationId = 0;
|
|
@@ -121,6 +122,7 @@ const createCompilationHost = (options = {}) => {
|
|
|
121
122
|
configPath,
|
|
122
123
|
cwd,
|
|
123
124
|
dev: command === "serve",
|
|
125
|
+
atomOrigins: command === "serve" && devSourcemap,
|
|
124
126
|
...command === "serve" ? { sourceChanges } : {}
|
|
125
127
|
});
|
|
126
128
|
return publish();
|
|
@@ -153,6 +155,12 @@ const createCompilationHost = (options = {}) => {
|
|
|
153
155
|
setCommand(next) {
|
|
154
156
|
command = next;
|
|
155
157
|
},
|
|
158
|
+
setDevSourcemap(enabled) {
|
|
159
|
+
devSourcemap = enabled;
|
|
160
|
+
},
|
|
161
|
+
isSourceFile(filePath) {
|
|
162
|
+
return builder?.context ? builder.isPotentialSourceFile(filePath) : true;
|
|
163
|
+
},
|
|
156
164
|
current: () => generation,
|
|
157
165
|
async ensureGeneration() {
|
|
158
166
|
if (cssPass) await settled(cssPass);
|
|
@@ -211,6 +219,11 @@ const createStaticCompilationSession = () => {
|
|
|
211
219
|
participatingEnvironments: /* @__PURE__ */ new Set(),
|
|
212
220
|
completedEnvironments: /* @__PURE__ */ new Set(),
|
|
213
221
|
prunedClasses: /* @__PURE__ */ new Set(),
|
|
222
|
+
deferredSheets: [],
|
|
223
|
+
writtenOutputs: [],
|
|
224
|
+
prunedAssets: /* @__PURE__ */ new WeakSet(),
|
|
225
|
+
prunedSheetNames: /* @__PURE__ */ new WeakMap(),
|
|
226
|
+
splitCss: true,
|
|
214
227
|
beginOutputProjection(_environment, _outputOptions, _bundle, replacesGeneratedStylesheet) {
|
|
215
228
|
if (replacesGeneratedStylesheet) session.prunedClasses.clear();
|
|
216
229
|
const prunable = new Set([...session.prunableClasses].map((className) => className.replaceAll("\\", "")));
|
|
@@ -300,6 +313,173 @@ const queryOf = (id) => {
|
|
|
300
313
|
*/
|
|
301
314
|
const asError = (error, context) => error instanceof Error ? error : new Error(`bamboocss: ${context}: ${String(error)}`, { cause: error });
|
|
302
315
|
/**
|
|
316
|
+
* Which lazily loaded chunk each atom exclusive to one belongs to.
|
|
317
|
+
*
|
|
318
|
+
* An atom belongs to a chunk when every module that emits it is in that chunk, and the chunk
|
|
319
|
+
* is not loaded with an entry anyway — an entry, or anything an entry statically imports,
|
|
320
|
+
* would put the atom in the entry sheet's own company either way. An atom two chunks share,
|
|
321
|
+
* or one no compiled module emits — `staticCss` — has no owner and stays where every route
|
|
322
|
+
* finds it. Loading with an entry is the static import closure of every entry, which is what
|
|
323
|
+
* the browser fetches before the first render.
|
|
324
|
+
*/
|
|
325
|
+
const chunkOwnership = (bundle, environment, session) => {
|
|
326
|
+
const classNamesOf = session.classNamesOf;
|
|
327
|
+
const ownership = /* @__PURE__ */ new Map();
|
|
328
|
+
if (!classNamesOf) return ownership;
|
|
329
|
+
const chunks = Object.values(bundle).filter((output) => output.type === "chunk");
|
|
330
|
+
const byFileName = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
|
|
331
|
+
const eager = /* @__PURE__ */ new Set();
|
|
332
|
+
const visit = (fileName) => {
|
|
333
|
+
if (eager.has(fileName)) return;
|
|
334
|
+
eager.add(fileName);
|
|
335
|
+
for (const imported of byFileName.get(fileName)?.imports ?? []) visit(imported);
|
|
336
|
+
};
|
|
337
|
+
for (const chunk of chunks) if (chunk.isEntry) visit(chunk.fileName);
|
|
338
|
+
const owners = /* @__PURE__ */ new Map();
|
|
339
|
+
for (const chunk of chunks) {
|
|
340
|
+
const owner = eager.has(chunk.fileName) ? null : chunk.fileName;
|
|
341
|
+
for (const moduleId of Object.keys(chunk.modules)) for (const classNames of classNamesOf(environment, moduleId) ?? []) for (const token of classNames.split(" ")) {
|
|
342
|
+
if (!token) continue;
|
|
343
|
+
const className = bare(token);
|
|
344
|
+
const previous = owners.get(className);
|
|
345
|
+
if (previous === void 0) owners.set(className, owner);
|
|
346
|
+
else if (previous !== owner) owners.set(className, null);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const [className, owner] of owners) if (owner !== null) ownership.set(className, owner);
|
|
350
|
+
logger.debug("vite", `Split: ${chunks.length} chunk(s), ${eager.size} loaded with an entry, ${owners.size} atom(s) seen, ${ownership.size} owned by a lazy chunk.`);
|
|
351
|
+
return ownership;
|
|
352
|
+
};
|
|
353
|
+
/**
|
|
354
|
+
* Prune every generated sheet in `bundle` this generation has not pruned yet.
|
|
355
|
+
*
|
|
356
|
+
* Reached twice per output. The early hook, ordered `pre`, reaches a sheet Vite emitted while
|
|
357
|
+
* rendering chunks — every `cssCodeSplit: true` build — before any other plugin's
|
|
358
|
+
* `generateBundle` reads its name, so a framework recording asset names records the final one.
|
|
359
|
+
* That was not a courtesy: `@vitejs/plugin-rsc` snapshots the server build's stylesheet name in
|
|
360
|
+
* a normal-order hook and writes it into a manifest at the end of the run, so a rename in a
|
|
361
|
+
* `post` hook left every server-rendered page linking a stylesheet that no longer existed. The
|
|
362
|
+
* late hook, ordered `post`, reaches what Vite emits from its own `generateBundle` — the single
|
|
363
|
+
* `style.css` of a `cssCodeSplit: false` build. Exactly one of the two opens a projection for a
|
|
364
|
+
* given sheet.
|
|
365
|
+
*
|
|
366
|
+
* Pruning waits for no one; finalizing does. The stylesheet is emitted by the environment that
|
|
367
|
+
* *imports* it, which in an SSR app is the client — and the client builds first, before the
|
|
368
|
+
* server environment has transformed a single module. Two answers were tried before this one.
|
|
369
|
+
* Holding the sheet back until every environment had contributed meant never pruning in any SSR
|
|
370
|
+
* framework, since the client's output is on disk before the server starts. Pruning against the
|
|
371
|
+
* client alone, with a guard that failed the build when a later environment reached a rule it had
|
|
372
|
+
* removed, made a styled component that renders only on the server a build failure — and under
|
|
373
|
+
* React Server Components most components never reach the client graph at all.
|
|
374
|
+
*
|
|
375
|
+
* So while environments remain, the sheet is pruned against what the run knows so far and
|
|
376
|
+
* written under a name hashed from those bytes, and it is recorded as deferred with its unpruned
|
|
377
|
+
* source. When the last environment writes its output, `bamboocss:output-write-observer` prunes
|
|
378
|
+
* that source again against the union of every environment's reachability. Usually the result is
|
|
379
|
+
* the bytes already on disk, and nothing moves. When a later environment restored a rule, the
|
|
380
|
+
* final bytes go under a new name, every written reference moves with them, and so does any copy
|
|
381
|
+
* of the provisional sheet another output carries. A single-environment build, a run whose
|
|
382
|
+
* sheet-carrying environment builds last, and an in-memory build take the direct path, where the
|
|
383
|
+
* guard in `buildEnd` still fails a later environment that reaches a rule the sheet lost.
|
|
384
|
+
*/
|
|
385
|
+
const pruneEmittedSheets = async (context, session, outputOptions, bundle, isWrite, pruneCss, splitCss) => {
|
|
386
|
+
const { containsGeneratedCssAsset, optimizeStaticCssAssets } = await loadCssOutputModule();
|
|
387
|
+
let handledNames = session.prunedSheetNames.get(outputOptions);
|
|
388
|
+
if (!handledNames) {
|
|
389
|
+
handledNames = /* @__PURE__ */ new Set();
|
|
390
|
+
session.prunedSheetNames.set(outputOptions, handledNames);
|
|
391
|
+
}
|
|
392
|
+
const carriesSheet = containsGeneratedCssAsset(bundle, session.prunedAssets, handledNames);
|
|
393
|
+
const environmentName = context.environment?.name ?? "default";
|
|
394
|
+
const pending = remainingEnvironments(session, environmentName);
|
|
395
|
+
const completesRun = !pending.length && session.deferredSheets.length > 0;
|
|
396
|
+
if (!carriesSheet && !completesRun) return;
|
|
397
|
+
const outputProjection = session.beginOutputProjection(environmentName, outputOptions, bundle, carriesSheet);
|
|
398
|
+
try {
|
|
399
|
+
const outputDir = outputOptions.dir ?? (outputOptions.file ? dirname(outputOptions.file) : void 0);
|
|
400
|
+
const deferred = pruneCss && isWrite === true && outputDir !== void 0 && pending.length > 0;
|
|
401
|
+
const remaining = truncateList(pending, {
|
|
402
|
+
unit: "environment",
|
|
403
|
+
separator: ", "
|
|
404
|
+
});
|
|
405
|
+
const sourcemap = context.environment?.config?.build?.sourcemap ?? session.sourcemap;
|
|
406
|
+
if (completesRun) await session.finalizeDeferred?.({
|
|
407
|
+
environment: environmentName,
|
|
408
|
+
bundle,
|
|
409
|
+
sourcemap
|
|
410
|
+
});
|
|
411
|
+
if (!carriesSheet) return;
|
|
412
|
+
const cssCodeSplit = context.environment?.config?.build?.cssCodeSplit ?? session.cssCodeSplit ?? true;
|
|
413
|
+
const split = splitCss && session.splitCss && pruneCss && cssCodeSplit && context.emitFile && context.getFileName ? {
|
|
414
|
+
ownership: chunkOwnership(bundle, environmentName, session),
|
|
415
|
+
emit: (chunkFileName, css) => {
|
|
416
|
+
const chunk = bundle[chunkFileName];
|
|
417
|
+
const referenceId = context.emitFile({
|
|
418
|
+
type: "asset",
|
|
419
|
+
name: `${chunk?.name ?? "chunk"}.css`,
|
|
420
|
+
source: css
|
|
421
|
+
});
|
|
422
|
+
const fileName = context.getFileName(referenceId);
|
|
423
|
+
chunk?.viteMetadata?.importedCss?.add(fileName);
|
|
424
|
+
const emitted = bundle[fileName];
|
|
425
|
+
if (emitted) session.prunedAssets.add(emitted);
|
|
426
|
+
}
|
|
427
|
+
} : void 0;
|
|
428
|
+
const { sheets, results } = optimizeStaticCssAssets(bundle, session, {
|
|
429
|
+
environment: environmentName,
|
|
430
|
+
prune: pruneCss,
|
|
431
|
+
requiredClasses: outputProjection.requiredClasses,
|
|
432
|
+
sourcemap: context.environment?.config?.build?.sourcemap,
|
|
433
|
+
handled: session.prunedAssets,
|
|
434
|
+
handledNames,
|
|
435
|
+
split
|
|
436
|
+
});
|
|
437
|
+
if (sheets && !pruneCss) logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
|
|
438
|
+
if (!sheets || !pruneCss || !pending.length) return;
|
|
439
|
+
if (deferred) {
|
|
440
|
+
for (const result of results) session.deferredSheets.push({
|
|
441
|
+
environment: environmentName,
|
|
442
|
+
dir: resolve(outputDir),
|
|
443
|
+
originalFileName: result.original,
|
|
444
|
+
fileName: result.fileName,
|
|
445
|
+
source: result.source,
|
|
446
|
+
provisional: result.optimized,
|
|
447
|
+
sourcemap,
|
|
448
|
+
asset: result.asset,
|
|
449
|
+
bundle,
|
|
450
|
+
moved: result.moved
|
|
451
|
+
});
|
|
452
|
+
logger.debug("vite", `Pruned the stylesheet against what the run knows, with ${remaining} still to compile. It is pruned again from source once the last environment has written its output, and renamed only if that restores a rule.`);
|
|
453
|
+
} else logger.debug("vite", `Pruning against the ${JSON.stringify(environmentName)} environment with ${remaining} still to compile. An in-memory build has no file to finalize, so a class only those reach fails the build rather than shipping without its rule.`);
|
|
454
|
+
} finally {
|
|
455
|
+
outputProjection.restore();
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* The early half of the stylesheet's output lifecycle. @see `pruneEmittedSheets`
|
|
460
|
+
*
|
|
461
|
+
* A plugin of its own because one plugin carries one `generateBundle`, and this one has to be
|
|
462
|
+
* ordered `pre` while the checks in `bamboocssCss` have to see the finished bundle.
|
|
463
|
+
*/
|
|
464
|
+
const bamboocssCssEarly = (options) => ({
|
|
465
|
+
name: "bamboocss:css-early",
|
|
466
|
+
sharedDuringBuild: true,
|
|
467
|
+
/**
|
|
468
|
+
* A watch rebuild renders into the same output options object, so the names pruned by the
|
|
469
|
+
* previous build would otherwise still read as handled, and the sheet a rebuild re-emits
|
|
470
|
+
* under an unchanged name would ship unpruned.
|
|
471
|
+
*/
|
|
472
|
+
renderStart(outputOptions) {
|
|
473
|
+
options.session.prunedSheetNames.delete(outputOptions);
|
|
474
|
+
},
|
|
475
|
+
generateBundle: {
|
|
476
|
+
order: "pre",
|
|
477
|
+
async handler(outputOptions, bundle, isWrite) {
|
|
478
|
+
await pruneEmittedSheets(this, options.session, outputOptions, bundle, isWrite, options.pruneCss ?? true, true);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
/**
|
|
303
483
|
* Serve bamboo's stylesheet as a virtual module, in dev and in build.
|
|
304
484
|
*
|
|
305
485
|
* This is the integration itself, not an optimisation: without it nothing emits css and
|
|
@@ -358,6 +538,13 @@ const bamboocssCss = (options) => {
|
|
|
358
538
|
let pendingGeneration = -1;
|
|
359
539
|
let servedCss;
|
|
360
540
|
/**
|
|
541
|
+
* Whether the served stylesheet carries a source map: a dev server with Vite's
|
|
542
|
+
* `css.devSourcemap` on. Off, and the extraction pass records no call sites at all.
|
|
543
|
+
*/
|
|
544
|
+
let devSourcemap = false;
|
|
545
|
+
/** Each atom's first call site, by class name, from the last pass. @see `Builder.getAtomOrigins` */
|
|
546
|
+
let atomOrigins;
|
|
547
|
+
/**
|
|
361
548
|
* Held by the host for its whole length, rather than only around each mutation.
|
|
362
549
|
*
|
|
363
550
|
* Extraction fills the encoder this sheet is emitted from and `toCss` reads it back, with a
|
|
@@ -383,6 +570,7 @@ const bamboocssCss = (options) => {
|
|
|
383
570
|
graphAtomHashes = new Set(activeBuilder.context.encoder.atomic);
|
|
384
571
|
}
|
|
385
572
|
const css = activeBuilder.toCss({ layerParams: true });
|
|
573
|
+
atomOrigins = devSourcemap ? activeBuilder.getAtomOrigins?.() : void 0;
|
|
386
574
|
session.prunableClasses.clear();
|
|
387
575
|
session.viewTransitionClasses.clear();
|
|
388
576
|
if (graphAtomHashes && activeBuilder.context) {
|
|
@@ -475,7 +663,10 @@ const bamboocssCss = (options) => {
|
|
|
475
663
|
async configResolved(config) {
|
|
476
664
|
command = config.command;
|
|
477
665
|
host.setCommand(config.command);
|
|
666
|
+
devSourcemap = config.command === "serve" && Boolean(config.css?.devSourcemap);
|
|
667
|
+
host.setDevSourcemap(devSourcemap);
|
|
478
668
|
session.sourcemap = config.build.sourcemap;
|
|
669
|
+
session.cssCodeSplit = config.build.cssCodeSplit;
|
|
479
670
|
ssrBuildOptions = {
|
|
480
671
|
ssr: config.build.ssr,
|
|
481
672
|
ssrEmitAssets: config.build.ssrEmitAssets
|
|
@@ -551,24 +742,34 @@ const bamboocssCss = (options) => {
|
|
|
551
742
|
const generationAtStart = changeGeneration;
|
|
552
743
|
if (command === "serve" && servedCss?.generation === generationAtStart) {
|
|
553
744
|
if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
|
|
554
|
-
return servedCss.
|
|
745
|
+
return servedCss.map ? {
|
|
746
|
+
code: servedCss.css,
|
|
747
|
+
map: servedCss.map
|
|
748
|
+
} : servedCss.css;
|
|
555
749
|
}
|
|
556
750
|
let css;
|
|
751
|
+
let map;
|
|
557
752
|
try {
|
|
558
|
-
const
|
|
753
|
+
const cssOutput = command === "serve" ? await loadCssOutput() : void 0;
|
|
754
|
+
const validateDevCss = cssOutput?.pruneStaticCss;
|
|
559
755
|
const first = prebuilt;
|
|
560
756
|
prebuilt = void 0;
|
|
561
757
|
css = await (first ?? generate());
|
|
562
758
|
if (validateDevCss) css = validateDevCss(css, session, { prune: false });
|
|
759
|
+
if (devSourcemap && atomOrigins?.size) map = cssOutput?.cssSourceMap?.(css, atomOrigins);
|
|
563
760
|
} catch (error) {
|
|
564
761
|
throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
|
|
565
762
|
}
|
|
566
763
|
if (command === "serve" && generationAtStart === changeGeneration) servedCss = {
|
|
567
764
|
generation: generationAtStart,
|
|
568
|
-
css
|
|
765
|
+
css,
|
|
766
|
+
map
|
|
569
767
|
};
|
|
570
768
|
if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
|
|
571
|
-
return
|
|
769
|
+
return map ? {
|
|
770
|
+
code: css,
|
|
771
|
+
map
|
|
772
|
+
} : css;
|
|
572
773
|
},
|
|
573
774
|
configureServer(devServer) {
|
|
574
775
|
server = devServer;
|
|
@@ -610,49 +811,15 @@ const bamboocssCss = (options) => {
|
|
|
610
811
|
},
|
|
611
812
|
generateBundle: {
|
|
612
813
|
order: "post",
|
|
613
|
-
async handler(outputOptions, bundle) {
|
|
614
|
-
const
|
|
615
|
-
|
|
814
|
+
async handler(outputOptions, bundle, isWrite) {
|
|
815
|
+
const context = this;
|
|
816
|
+
await pruneEmittedSheets(context, session, outputOptions, bundle, isWrite, pruneCss, false);
|
|
817
|
+
const { containsGeneratedCssAsset } = await loadCssOutputModule();
|
|
818
|
+
const environment = context.environment;
|
|
616
819
|
const environmentName = environment?.name ?? "default";
|
|
617
820
|
const replacesGeneratedStylesheet = containsGeneratedCssAsset(bundle);
|
|
618
|
-
const outputProjection = session.beginOutputProjection(environmentName, outputOptions, bundle,
|
|
821
|
+
const outputProjection = session.beginOutputProjection(environmentName, outputOptions, bundle, false);
|
|
619
822
|
try {
|
|
620
|
-
/**
|
|
621
|
-
* Pruned against what this environment compiled, without waiting for the rest.
|
|
622
|
-
*
|
|
623
|
-
* The stylesheet is emitted and finalized by the environment that *imports* it, which
|
|
624
|
-
* in an SSR app is the client — and the client builds first, before the server
|
|
625
|
-
* environment has transformed a single module. Waiting for a complete answer therefore
|
|
626
|
-
* meant never pruning at all in any SSR framework: react-router, Remix, Nuxt, SvelteKit
|
|
627
|
-
* and Qwik all build the client first, and the client's output is on disk before the
|
|
628
|
-
* server environment starts. That is most production apps, and the feature was inert in
|
|
629
|
-
* every one of them — silently, since a build with nothing to prune looks identical.
|
|
630
|
-
*
|
|
631
|
-
* The reason for waiting was real: a class only the server graph reaches is not in this
|
|
632
|
-
* environment's reachability set, so pruning here removes rules the server-rendered
|
|
633
|
-
* markup still names. What makes it safe to prune anyway is that the mistake is
|
|
634
|
-
* *detectable* rather than silent — `buildEnd` in `plugin.ts` intersects every later
|
|
635
|
-
* environment's compiled classes against `prunedClasses` and fails the build naming
|
|
636
|
-
* them. A styled component that only ever renders on the server is the shape that
|
|
637
|
-
* trips it, and `pruneCss: false` is the answer when it does.
|
|
638
|
-
*
|
|
639
|
-
* So the trade is deliberate: a loud build failure in the rare case, in exchange for
|
|
640
|
-
* the feature working at all in the common one. It is the same reasoning as the
|
|
641
|
-
* unimported-`virtual:bamboo.css` check — a class with no rule behind it must never
|
|
642
|
-
* leave the build quietly.
|
|
643
|
-
*/
|
|
644
|
-
const pending = remainingEnvironments(session);
|
|
645
|
-
if (pending.length) logger.debug("vite", `Pruning against the ${JSON.stringify(environment?.name ?? "default")} environment with ${truncateList(pending, {
|
|
646
|
-
unit: "environment",
|
|
647
|
-
separator: ", "
|
|
648
|
-
})} still to compile. A class only those reach fails the build rather than shipping without its rule.`);
|
|
649
|
-
const { sheets } = optimizeStaticCssAssets(bundle, session, {
|
|
650
|
-
environment: environmentName,
|
|
651
|
-
prune: pruneCss,
|
|
652
|
-
requiredClasses: outputProjection.requiredClasses,
|
|
653
|
-
sourcemap: environment?.config?.build?.sourcemap
|
|
654
|
-
});
|
|
655
|
-
if (sheets && !pruneCss) logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
|
|
656
823
|
if (!outputProjection.cssLoaded) return;
|
|
657
824
|
if (!session.transformedFiles.size) return;
|
|
658
825
|
/**
|
|
@@ -754,6 +921,64 @@ const shouldTransform = (id) => {
|
|
|
754
921
|
* Returns `null` when the module is still a raw SFC and must be left to the framework plugin.
|
|
755
922
|
* Astro frontmatter is `---`, not `<script>`, so a tag check alone would parse the template.
|
|
756
923
|
*/
|
|
924
|
+
/** The module specifiers that reach bamboo — the `styled-system` paths and any `importMap` — per context. */
|
|
925
|
+
const entrypointNeedles = /* @__PURE__ */ new WeakMap();
|
|
926
|
+
/**
|
|
927
|
+
* Whether a module's text names a bamboo entrypoint at all.
|
|
928
|
+
*
|
|
929
|
+
* A textual test on purpose: it runs before the module is parsed, on modules outside the
|
|
930
|
+
* extraction inventory, to decide whether parsing is worth it. The outdir's own name is among
|
|
931
|
+
* the needles, so a relative import of the generated `styled-system` counts too.
|
|
932
|
+
*/
|
|
933
|
+
const namesEntrypoint = (ctx, code) => {
|
|
934
|
+
let needles = entrypointNeedles.get(ctx);
|
|
935
|
+
if (!needles) {
|
|
936
|
+
const outdirName = ctx.imports.outdir.split("/").filter(Boolean).at(-1);
|
|
937
|
+
needles = [...new Set([outdirName ?? "", ...Object.values(ctx.imports.value).flat()].filter((needle) => needle.length > 0))];
|
|
938
|
+
entrypointNeedles.set(ctx, needles);
|
|
939
|
+
}
|
|
940
|
+
return needles.some((needle) => code.includes(needle));
|
|
941
|
+
};
|
|
942
|
+
/** A module specifier in an import, export-from, dynamic import or require. */
|
|
943
|
+
const MODULE_SPECIFIER = /\b(?:from|import|require)\s*\(?\s*['"]([^'"]+)['"]/g;
|
|
944
|
+
/** How a relative specifier may name a file, in the order the bundler tries them. */
|
|
945
|
+
const SPECIFIER_SUFFIXES = [
|
|
946
|
+
"",
|
|
947
|
+
".ts",
|
|
948
|
+
".tsx",
|
|
949
|
+
".mts",
|
|
950
|
+
".cts",
|
|
951
|
+
".js",
|
|
952
|
+
".jsx",
|
|
953
|
+
".mjs",
|
|
954
|
+
".cjs",
|
|
955
|
+
"/index.ts",
|
|
956
|
+
"/index.tsx",
|
|
957
|
+
"/index.js"
|
|
958
|
+
];
|
|
959
|
+
/**
|
|
960
|
+
* Whether a module imports a module the shared project holds.
|
|
961
|
+
*
|
|
962
|
+
* Answered from project membership alone — no parse, no disk, no round trip to the compiler —
|
|
963
|
+
* which is what makes it affordable to ask of every module outside the extraction inventory.
|
|
964
|
+
* A relative specifier is tried against the file names a bundler would; a bare one against
|
|
965
|
+
* where the parser last resolved that package to, since a package the project holds a source
|
|
966
|
+
* of is one an included file imports a recipe from.
|
|
967
|
+
*/
|
|
968
|
+
const importsProjectModule = (ctx, filePath, code) => {
|
|
969
|
+
const directory = dirname(filePath);
|
|
970
|
+
for (const match of code.matchAll(MODULE_SPECIFIER)) {
|
|
971
|
+
const specifier = match[1];
|
|
972
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
973
|
+
const base = specifier.startsWith("/") ? specifier : resolve(directory, specifier);
|
|
974
|
+
for (const suffix of SPECIFIER_SUFFIXES) if (ctx.project.hasSourceFile(base + suffix)) return true;
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
const target = ctx.project.bareSpecifierTarget(specifier);
|
|
978
|
+
if (target && ctx.project.hasSourceFile(target)) return true;
|
|
979
|
+
}
|
|
980
|
+
return false;
|
|
981
|
+
};
|
|
757
982
|
const compilerParsePath = (id, code) => {
|
|
758
983
|
const [filePath, query = ""] = id.split("?");
|
|
759
984
|
if (!filePath) return null;
|
|
@@ -832,11 +1057,12 @@ const formatSkipped = (id, skipped) => {
|
|
|
832
1057
|
* therefore fold in the post plugin, after the framework has extracted them.
|
|
833
1058
|
*/
|
|
834
1059
|
const bamboocss = (options = {}) => {
|
|
835
|
-
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
|
|
1060
|
+
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true, splitCss = true } = options;
|
|
836
1061
|
markStaticCompilerActive();
|
|
837
1062
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
838
1063
|
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.");
|
|
839
1064
|
const staticSession = createStaticCompilationSession();
|
|
1065
|
+
staticSession.splitCss = splitCss;
|
|
840
1066
|
/**
|
|
841
1067
|
* One Builder, one resolved config, one context and one ts-morph project for the run.
|
|
842
1068
|
*
|
|
@@ -1266,6 +1492,42 @@ const bamboocss = (options = {}) => {
|
|
|
1266
1492
|
staticSession.prunedClasses.clear();
|
|
1267
1493
|
for (const slots of liveOutputSlotsByEnvironment.values()) for (const slot of slots.values()) for (const className of slot.prunedClasses ?? []) staticSession.prunedClasses.add(className);
|
|
1268
1494
|
};
|
|
1495
|
+
/**
|
|
1496
|
+
* Prune the sheets the run wrote whole, now that every environment has contributed.
|
|
1497
|
+
*
|
|
1498
|
+
* Reached from the write hook of whichever environment completes the run, which is the first
|
|
1499
|
+
* point at which the union of every environment's reachability exists and every reference to
|
|
1500
|
+
* the sheet is on disk. A run that never completes — an environment declared and never built
|
|
1501
|
+
* — leaves the sheets whole, and says so as the process exits.
|
|
1502
|
+
*/
|
|
1503
|
+
const finalizeDeferredSheetsIfComplete = async (candidate, bundle, sourcemap) => {
|
|
1504
|
+
if (!staticSession.deferredSheets.length) return;
|
|
1505
|
+
if (remainingEnvironments(staticSession, candidate).length) return;
|
|
1506
|
+
const sheets = staticSession.deferredSheets.splice(0);
|
|
1507
|
+
const { finalizeDeferredSheets } = await loadCssOutputModule();
|
|
1508
|
+
const committed = staticSession.prunedClasses;
|
|
1509
|
+
staticSession.prunedClasses = /* @__PURE__ */ new Set();
|
|
1510
|
+
let finalized;
|
|
1511
|
+
let lost;
|
|
1512
|
+
try {
|
|
1513
|
+
finalized = finalizeDeferredSheets(sheets, staticSession, {
|
|
1514
|
+
prune: pruneCss,
|
|
1515
|
+
requiredClasses: currentRequiredClasses(),
|
|
1516
|
+
outputs: staticSession.writtenOutputs,
|
|
1517
|
+
bundle,
|
|
1518
|
+
sourcemap
|
|
1519
|
+
});
|
|
1520
|
+
} finally {
|
|
1521
|
+
lost = staticSession.prunedClasses;
|
|
1522
|
+
staticSession.prunedClasses = committed;
|
|
1523
|
+
}
|
|
1524
|
+
for (const sheet of finalized) for (const slot of liveOutputSlotsByEnvironment.get(sheet.environment)?.values() ?? []) slot.prunedClasses = new Set(lost);
|
|
1525
|
+
rebuildLivePrunedClasses();
|
|
1526
|
+
for (const sheet of finalized) {
|
|
1527
|
+
if (!sheet.renamed) continue;
|
|
1528
|
+
logger.info("vite", `Pruned ${sheet.originalFileName} against every environment once the last had written, which restored a rule the earlier prune removed: ${sheet.before} → ${sheet.after} bytes, now ${sheet.renamed}.`);
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1269
1531
|
const observeEnvironmentBuildStart = (environment) => {
|
|
1270
1532
|
const serial = (nextBuildSerialByEnvironment.get(environment) ?? 0) + 1;
|
|
1271
1533
|
nextBuildSerialByEnvironment.set(environment, serial);
|
|
@@ -1385,6 +1647,8 @@ const bamboocss = (options = {}) => {
|
|
|
1385
1647
|
rebuildStaticTransformContributions();
|
|
1386
1648
|
rebuildLivePrunedClasses();
|
|
1387
1649
|
};
|
|
1650
|
+
staticSession.finalizeDeferred = ({ environment, bundle, sourcemap }) => finalizeDeferredSheetsIfComplete(environment, bundle, sourcemap);
|
|
1651
|
+
staticSession.classNamesOf = (environment, moduleId) => transformStateByEnvironment.get(environment)?.transformArtifactsByModule.get(moduleId)?.classNames;
|
|
1388
1652
|
staticSession.beginOutputProjection = (environment, outputOptions, bundle, replacesGeneratedStylesheet) => {
|
|
1389
1653
|
const generation = preparedGenerations.get(environment);
|
|
1390
1654
|
if (!generation || transformStateByEnvironment.get(environment) !== generation.state) {
|
|
@@ -1407,9 +1671,13 @@ const bamboocss = (options = {}) => {
|
|
|
1407
1671
|
restore() {
|
|
1408
1672
|
if (restored) return;
|
|
1409
1673
|
restored = true;
|
|
1674
|
+
const previous = outputStageByBundle.get(bundle) ?? outputStageByOptions.get(outputOptions);
|
|
1410
1675
|
const stage = replacesGeneratedStylesheet ? {
|
|
1411
1676
|
cssDigest: bambooCssDigest(bundle),
|
|
1412
|
-
prunedClasses: new Set(staticSession.prunedClasses)
|
|
1677
|
+
prunedClasses: new Set([...previous?.prunedClasses ?? [], ...staticSession.prunedClasses])
|
|
1678
|
+
} : previous ? {
|
|
1679
|
+
cssDigest: bambooCssDigest(bundle),
|
|
1680
|
+
prunedClasses: new Set(previous.prunedClasses ?? [])
|
|
1413
1681
|
} : {};
|
|
1414
1682
|
outputStageByBundle.set(bundle, stage);
|
|
1415
1683
|
outputStageByOptions.set(outputOptions, stage);
|
|
@@ -1751,6 +2019,7 @@ const bamboocss = (options = {}) => {
|
|
|
1751
2019
|
let styleCompiler;
|
|
1752
2020
|
let command = "build";
|
|
1753
2021
|
let defaultEmitAssets = true;
|
|
2022
|
+
let exitWarningInstalled = false;
|
|
1754
2023
|
/**
|
|
1755
2024
|
* Expand semantic leaf reads through the Project's exact resolution paths.
|
|
1756
2025
|
*
|
|
@@ -1993,6 +2262,16 @@ const bamboocss = (options = {}) => {
|
|
|
1993
2262
|
command = config.command;
|
|
1994
2263
|
host.setCommand(config.command);
|
|
1995
2264
|
defaultEmitAssets = config.build?.emitAssets ?? (!config.build?.ssr || config.build?.ssrEmitAssets === true);
|
|
2265
|
+
if (config.command === "build" && !exitWarningInstalled) {
|
|
2266
|
+
exitWarningInstalled = true;
|
|
2267
|
+
process.once("beforeExit", () => {
|
|
2268
|
+
if (!staticSession.deferredSheets.length) return;
|
|
2269
|
+
logger.warn("vite", `The stylesheet was pruned against an incomplete run: ${truncateList(remainingEnvironments(staticSession), {
|
|
2270
|
+
unit: "environment",
|
|
2271
|
+
separator: ", "
|
|
2272
|
+
})} never completed, so a rule only those reach was never restored. Build every declared environment, or set \`bamboocss({ pruneCss: false })\` to ship the full extracted stylesheet.`);
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
1996
2275
|
const plugins = config.plugins;
|
|
1997
2276
|
if (plugins) {
|
|
1998
2277
|
for (const finalizer of [outputWriteObserver, memoryOutputCommitter]) {
|
|
@@ -2177,11 +2456,11 @@ const bamboocss = (options = {}) => {
|
|
|
2177
2456
|
rebuildStaticTransformContributions(environment, state);
|
|
2178
2457
|
const survivors = allSurvivors(states);
|
|
2179
2458
|
if (survivors.length) throw createSurvivorError(survivors);
|
|
2180
|
-
const lost = currentWillEmitCss ? [] : [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(bare(className)));
|
|
2459
|
+
const lost = currentWillEmitCss || staticSession.deferredSheets.length ? [] : [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(bare(className)));
|
|
2181
2460
|
if (lost.length) throw new Error(`bamboocss: ${lost.length} class(es) compiled in the ${JSON.stringify(environment)} environment were already pruned out of a stylesheet emitted by an earlier one. Elements carrying them would render unstyled.\n\n${truncateList(lost.map((className) => ` ${className}`), {
|
|
2182
2461
|
unit: "class",
|
|
2183
2462
|
separator: "\n"
|
|
2184
|
-
})}\n\nThe stylesheet
|
|
2463
|
+
})}\n\nThe stylesheet was pruned before this environment compiled. A run that announces its environments — \`builder\` in the Vite config, which every framework building more than one sets — holds pruning back until the last one has written, so this is a run that built environments one at a time without saying so, or a rebuild of this environment alone after the sheet was finalized. These classes are reached only from here, so no rule for them survived.\n\nConfigure \`builder\` so the run announces its environments, rebuild every environment together, or set \`bamboocss({ pruneCss: false })\` to ship the whole extracted stylesheet.`);
|
|
2185
2464
|
const remaining = remainingEnvironments(staticSession, environment);
|
|
2186
2465
|
if (typeof this.getModuleInfo === "function" && !remaining.length) {
|
|
2187
2466
|
if (!staticSession.cssLoaded) throw new Error(`bamboocss: compiled class values were produced, but ${JSON.stringify(VIRTUAL_CSS_ID)} was not imported. Add \`import ${JSON.stringify(VIRTUAL_CSS_ID)}\` once, from a JavaScript or TypeScript module in the application entry graph.\n\nIt has to be a JS import. \`@import\` from a stylesheet does not reach it: the id names a virtual module resolved by this plugin, and Vite resolves CSS \`@import\` before plugin resolution, so it fails as an unresolvable path. A project that ships one preloaded stylesheet imports this from its entry module instead, and lets Vite emit the CSS asset.`);
|
|
@@ -2226,6 +2505,10 @@ const bamboocss = (options = {}) => {
|
|
|
2226
2505
|
if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return null;
|
|
2227
2506
|
const [filePath] = id.split("?");
|
|
2228
2507
|
if (isGeneratedOutput(filePath, ctx)) return null;
|
|
2508
|
+
if (!(ctx.project.hasSourceFile(filePath) && ctx.project.getSourceFile(filePath)?.getFullText() === code) && !namesEntrypoint(ctx, code) && !importsProjectModule(ctx, filePath, code)) {
|
|
2509
|
+
logger.debug("vite:transform", `Skipped ${filePath}: ${host.isSourceFile(filePath) ? "rewritten before bamboo" : "outside `include`"}, and it reaches nothing bamboo`);
|
|
2510
|
+
return null;
|
|
2511
|
+
}
|
|
2229
2512
|
const requestedParsePath = compilerParsePath(id, code);
|
|
2230
2513
|
if (requestedParsePath === null) return null;
|
|
2231
2514
|
const state = environmentState(this);
|
|
@@ -2388,9 +2671,17 @@ const bamboocss = (options = {}) => {
|
|
|
2388
2671
|
writeBundle: {
|
|
2389
2672
|
order: "pre",
|
|
2390
2673
|
sequential: true,
|
|
2391
|
-
handler(outputOptions, bundle) {
|
|
2674
|
+
async handler(outputOptions, bundle) {
|
|
2675
|
+
const environment = environmentName(this);
|
|
2676
|
+
const outputDir = outputOptions.dir ?? (outputOptions.file ? dirname(outputOptions.file) : void 0);
|
|
2677
|
+
if (outputDir) staticSession.writtenOutputs.push({
|
|
2678
|
+
environment,
|
|
2679
|
+
dir: resolve(outputDir),
|
|
2680
|
+
files: Object.values(bundle).map((output) => output.fileName)
|
|
2681
|
+
});
|
|
2392
2682
|
const identity = outputIdentityByBundle.get(bundle) ?? outputIdentityByOptions.get(outputOptions);
|
|
2393
|
-
if (identity?.environment ===
|
|
2683
|
+
if (identity?.environment === environment) publishPreparedOutput(identity.environment, identity.outputToken, identity.outputSlot, true);
|
|
2684
|
+
await finalizeDeferredSheetsIfComplete();
|
|
2394
2685
|
}
|
|
2395
2686
|
}
|
|
2396
2687
|
};
|
|
@@ -2414,6 +2705,10 @@ const bamboocss = (options = {}) => {
|
|
|
2414
2705
|
session: staticSession,
|
|
2415
2706
|
pruneCss
|
|
2416
2707
|
}),
|
|
2708
|
+
bamboocssCssEarly({
|
|
2709
|
+
session: staticSession,
|
|
2710
|
+
pruneCss
|
|
2711
|
+
}),
|
|
2417
2712
|
compiler,
|
|
2418
2713
|
compilerSfc
|
|
2419
2714
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/vite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.54.0",
|
|
4
4
|
"description": "Vite integration for Bamboo CSS",
|
|
5
5
|
"homepage": "https://bamboocss.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,23 +38,24 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@ampproject/remapping": "2.3.0",
|
|
41
|
+
"@jridgewell/gen-mapping": "0.3.13",
|
|
41
42
|
"magic-string": "0.30.21",
|
|
42
43
|
"postcss": "8.5.26",
|
|
43
44
|
"postcss-selector-parser": "7.1.5",
|
|
44
|
-
"@bamboocss/config": "1.
|
|
45
|
-
"@bamboocss/core": "1.
|
|
46
|
-
"@bamboocss/
|
|
47
|
-
"@bamboocss/
|
|
48
|
-
"@bamboocss/
|
|
49
|
-
"@bamboocss/
|
|
50
|
-
"@bamboocss/ts-ast": "1.
|
|
51
|
-
"@bamboocss/types": "1.
|
|
45
|
+
"@bamboocss/config": "1.54.0",
|
|
46
|
+
"@bamboocss/core": "1.54.0",
|
|
47
|
+
"@bamboocss/extractor": "1.54.0",
|
|
48
|
+
"@bamboocss/node": "1.54.0",
|
|
49
|
+
"@bamboocss/logger": "1.54.0",
|
|
50
|
+
"@bamboocss/shared": "1.54.0",
|
|
51
|
+
"@bamboocss/ts-ast": "1.54.0",
|
|
52
|
+
"@bamboocss/types": "1.54.0"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
55
56
|
"@typescript/api": "npm:typescript@7.1.0-dev.20260826.1",
|
|
56
57
|
"vite": "7.2.6",
|
|
57
|
-
"@bamboocss/fixture": "1.
|
|
58
|
+
"@bamboocss/fixture": "1.54.0"
|
|
58
59
|
},
|
|
59
60
|
"peerDependencies": {
|
|
60
61
|
"vite": ">=5"
|