@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.cjs
CHANGED
|
@@ -4,11 +4,11 @@ Object.defineProperties(exports, {
|
|
|
4
4
|
});
|
|
5
5
|
require("./chunk.cjs");
|
|
6
6
|
const require_class_name = require("./class-name.cjs");
|
|
7
|
+
let node_path = require("node:path");
|
|
7
8
|
let _bamboocss_logger = require("@bamboocss/logger");
|
|
8
9
|
let _bamboocss_shared = require("@bamboocss/shared");
|
|
9
10
|
let node_crypto = require("node:crypto");
|
|
10
11
|
let node_fs = require("node:fs");
|
|
11
|
-
let node_path = require("node:path");
|
|
12
12
|
let _bamboocss_node_static_compiler = require("@bamboocss/node/static-compiler");
|
|
13
13
|
//#region src/lazy-modules.ts
|
|
14
14
|
/**
|
|
@@ -64,6 +64,7 @@ const createCompilationHost = (options = {}) => {
|
|
|
64
64
|
const { configPath, cwd } = options;
|
|
65
65
|
const loadBuilder = options.loadBuilder ?? createLazyBuilder();
|
|
66
66
|
let command = "build";
|
|
67
|
+
let devSourcemap = false;
|
|
67
68
|
let builder;
|
|
68
69
|
let generation;
|
|
69
70
|
let nextGenerationId = 0;
|
|
@@ -126,6 +127,7 @@ const createCompilationHost = (options = {}) => {
|
|
|
126
127
|
configPath,
|
|
127
128
|
cwd,
|
|
128
129
|
dev: command === "serve",
|
|
130
|
+
atomOrigins: command === "serve" && devSourcemap,
|
|
129
131
|
...command === "serve" ? { sourceChanges } : {}
|
|
130
132
|
});
|
|
131
133
|
return publish();
|
|
@@ -158,6 +160,12 @@ const createCompilationHost = (options = {}) => {
|
|
|
158
160
|
setCommand(next) {
|
|
159
161
|
command = next;
|
|
160
162
|
},
|
|
163
|
+
setDevSourcemap(enabled) {
|
|
164
|
+
devSourcemap = enabled;
|
|
165
|
+
},
|
|
166
|
+
isSourceFile(filePath) {
|
|
167
|
+
return builder?.context ? builder.isPotentialSourceFile(filePath) : true;
|
|
168
|
+
},
|
|
161
169
|
current: () => generation,
|
|
162
170
|
async ensureGeneration() {
|
|
163
171
|
if (cssPass) await settled(cssPass);
|
|
@@ -216,6 +224,11 @@ const createStaticCompilationSession = () => {
|
|
|
216
224
|
participatingEnvironments: /* @__PURE__ */ new Set(),
|
|
217
225
|
completedEnvironments: /* @__PURE__ */ new Set(),
|
|
218
226
|
prunedClasses: /* @__PURE__ */ new Set(),
|
|
227
|
+
deferredSheets: [],
|
|
228
|
+
writtenOutputs: [],
|
|
229
|
+
prunedAssets: /* @__PURE__ */ new WeakSet(),
|
|
230
|
+
prunedSheetNames: /* @__PURE__ */ new WeakMap(),
|
|
231
|
+
splitCss: true,
|
|
219
232
|
beginOutputProjection(_environment, _outputOptions, _bundle, replacesGeneratedStylesheet) {
|
|
220
233
|
if (replacesGeneratedStylesheet) session.prunedClasses.clear();
|
|
221
234
|
const prunable = new Set([...session.prunableClasses].map((className) => className.replaceAll("\\", "")));
|
|
@@ -305,6 +318,173 @@ const queryOf = (id) => {
|
|
|
305
318
|
*/
|
|
306
319
|
const asError = (error, context) => error instanceof Error ? error : new Error(`bamboocss: ${context}: ${String(error)}`, { cause: error });
|
|
307
320
|
/**
|
|
321
|
+
* Which lazily loaded chunk each atom exclusive to one belongs to.
|
|
322
|
+
*
|
|
323
|
+
* An atom belongs to a chunk when every module that emits it is in that chunk, and the chunk
|
|
324
|
+
* is not loaded with an entry anyway — an entry, or anything an entry statically imports,
|
|
325
|
+
* would put the atom in the entry sheet's own company either way. An atom two chunks share,
|
|
326
|
+
* or one no compiled module emits — `staticCss` — has no owner and stays where every route
|
|
327
|
+
* finds it. Loading with an entry is the static import closure of every entry, which is what
|
|
328
|
+
* the browser fetches before the first render.
|
|
329
|
+
*/
|
|
330
|
+
const chunkOwnership = (bundle, environment, session) => {
|
|
331
|
+
const classNamesOf = session.classNamesOf;
|
|
332
|
+
const ownership = /* @__PURE__ */ new Map();
|
|
333
|
+
if (!classNamesOf) return ownership;
|
|
334
|
+
const chunks = Object.values(bundle).filter((output) => output.type === "chunk");
|
|
335
|
+
const byFileName = new Map(chunks.map((chunk) => [chunk.fileName, chunk]));
|
|
336
|
+
const eager = /* @__PURE__ */ new Set();
|
|
337
|
+
const visit = (fileName) => {
|
|
338
|
+
if (eager.has(fileName)) return;
|
|
339
|
+
eager.add(fileName);
|
|
340
|
+
for (const imported of byFileName.get(fileName)?.imports ?? []) visit(imported);
|
|
341
|
+
};
|
|
342
|
+
for (const chunk of chunks) if (chunk.isEntry) visit(chunk.fileName);
|
|
343
|
+
const owners = /* @__PURE__ */ new Map();
|
|
344
|
+
for (const chunk of chunks) {
|
|
345
|
+
const owner = eager.has(chunk.fileName) ? null : chunk.fileName;
|
|
346
|
+
for (const moduleId of Object.keys(chunk.modules)) for (const classNames of classNamesOf(environment, moduleId) ?? []) for (const token of classNames.split(" ")) {
|
|
347
|
+
if (!token) continue;
|
|
348
|
+
const className = require_class_name.bare(token);
|
|
349
|
+
const previous = owners.get(className);
|
|
350
|
+
if (previous === void 0) owners.set(className, owner);
|
|
351
|
+
else if (previous !== owner) owners.set(className, null);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
for (const [className, owner] of owners) if (owner !== null) ownership.set(className, owner);
|
|
355
|
+
_bamboocss_logger.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.`);
|
|
356
|
+
return ownership;
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* Prune every generated sheet in `bundle` this generation has not pruned yet.
|
|
360
|
+
*
|
|
361
|
+
* Reached twice per output. The early hook, ordered `pre`, reaches a sheet Vite emitted while
|
|
362
|
+
* rendering chunks — every `cssCodeSplit: true` build — before any other plugin's
|
|
363
|
+
* `generateBundle` reads its name, so a framework recording asset names records the final one.
|
|
364
|
+
* That was not a courtesy: `@vitejs/plugin-rsc` snapshots the server build's stylesheet name in
|
|
365
|
+
* a normal-order hook and writes it into a manifest at the end of the run, so a rename in a
|
|
366
|
+
* `post` hook left every server-rendered page linking a stylesheet that no longer existed. The
|
|
367
|
+
* late hook, ordered `post`, reaches what Vite emits from its own `generateBundle` — the single
|
|
368
|
+
* `style.css` of a `cssCodeSplit: false` build. Exactly one of the two opens a projection for a
|
|
369
|
+
* given sheet.
|
|
370
|
+
*
|
|
371
|
+
* Pruning waits for no one; finalizing does. The stylesheet is emitted by the environment that
|
|
372
|
+
* *imports* it, which in an SSR app is the client — and the client builds first, before the
|
|
373
|
+
* server environment has transformed a single module. Two answers were tried before this one.
|
|
374
|
+
* Holding the sheet back until every environment had contributed meant never pruning in any SSR
|
|
375
|
+
* framework, since the client's output is on disk before the server starts. Pruning against the
|
|
376
|
+
* client alone, with a guard that failed the build when a later environment reached a rule it had
|
|
377
|
+
* removed, made a styled component that renders only on the server a build failure — and under
|
|
378
|
+
* React Server Components most components never reach the client graph at all.
|
|
379
|
+
*
|
|
380
|
+
* So while environments remain, the sheet is pruned against what the run knows so far and
|
|
381
|
+
* written under a name hashed from those bytes, and it is recorded as deferred with its unpruned
|
|
382
|
+
* source. When the last environment writes its output, `bamboocss:output-write-observer` prunes
|
|
383
|
+
* that source again against the union of every environment's reachability. Usually the result is
|
|
384
|
+
* the bytes already on disk, and nothing moves. When a later environment restored a rule, the
|
|
385
|
+
* final bytes go under a new name, every written reference moves with them, and so does any copy
|
|
386
|
+
* of the provisional sheet another output carries. A single-environment build, a run whose
|
|
387
|
+
* sheet-carrying environment builds last, and an in-memory build take the direct path, where the
|
|
388
|
+
* guard in `buildEnd` still fails a later environment that reaches a rule the sheet lost.
|
|
389
|
+
*/
|
|
390
|
+
const pruneEmittedSheets = async (context, session, outputOptions, bundle, isWrite, pruneCss, splitCss) => {
|
|
391
|
+
const { containsGeneratedCssAsset, optimizeStaticCssAssets } = await loadCssOutputModule();
|
|
392
|
+
let handledNames = session.prunedSheetNames.get(outputOptions);
|
|
393
|
+
if (!handledNames) {
|
|
394
|
+
handledNames = /* @__PURE__ */ new Set();
|
|
395
|
+
session.prunedSheetNames.set(outputOptions, handledNames);
|
|
396
|
+
}
|
|
397
|
+
const carriesSheet = containsGeneratedCssAsset(bundle, session.prunedAssets, handledNames);
|
|
398
|
+
const environmentName = context.environment?.name ?? "default";
|
|
399
|
+
const pending = remainingEnvironments(session, environmentName);
|
|
400
|
+
const completesRun = !pending.length && session.deferredSheets.length > 0;
|
|
401
|
+
if (!carriesSheet && !completesRun) return;
|
|
402
|
+
const outputProjection = session.beginOutputProjection(environmentName, outputOptions, bundle, carriesSheet);
|
|
403
|
+
try {
|
|
404
|
+
const outputDir = outputOptions.dir ?? (outputOptions.file ? (0, node_path.dirname)(outputOptions.file) : void 0);
|
|
405
|
+
const deferred = pruneCss && isWrite === true && outputDir !== void 0 && pending.length > 0;
|
|
406
|
+
const remaining = (0, _bamboocss_shared.truncateList)(pending, {
|
|
407
|
+
unit: "environment",
|
|
408
|
+
separator: ", "
|
|
409
|
+
});
|
|
410
|
+
const sourcemap = context.environment?.config?.build?.sourcemap ?? session.sourcemap;
|
|
411
|
+
if (completesRun) await session.finalizeDeferred?.({
|
|
412
|
+
environment: environmentName,
|
|
413
|
+
bundle,
|
|
414
|
+
sourcemap
|
|
415
|
+
});
|
|
416
|
+
if (!carriesSheet) return;
|
|
417
|
+
const cssCodeSplit = context.environment?.config?.build?.cssCodeSplit ?? session.cssCodeSplit ?? true;
|
|
418
|
+
const split = splitCss && session.splitCss && pruneCss && cssCodeSplit && context.emitFile && context.getFileName ? {
|
|
419
|
+
ownership: chunkOwnership(bundle, environmentName, session),
|
|
420
|
+
emit: (chunkFileName, css) => {
|
|
421
|
+
const chunk = bundle[chunkFileName];
|
|
422
|
+
const referenceId = context.emitFile({
|
|
423
|
+
type: "asset",
|
|
424
|
+
name: `${chunk?.name ?? "chunk"}.css`,
|
|
425
|
+
source: css
|
|
426
|
+
});
|
|
427
|
+
const fileName = context.getFileName(referenceId);
|
|
428
|
+
chunk?.viteMetadata?.importedCss?.add(fileName);
|
|
429
|
+
const emitted = bundle[fileName];
|
|
430
|
+
if (emitted) session.prunedAssets.add(emitted);
|
|
431
|
+
}
|
|
432
|
+
} : void 0;
|
|
433
|
+
const { sheets, results } = optimizeStaticCssAssets(bundle, session, {
|
|
434
|
+
environment: environmentName,
|
|
435
|
+
prune: pruneCss,
|
|
436
|
+
requiredClasses: outputProjection.requiredClasses,
|
|
437
|
+
sourcemap: context.environment?.config?.build?.sourcemap,
|
|
438
|
+
handled: session.prunedAssets,
|
|
439
|
+
handledNames,
|
|
440
|
+
split
|
|
441
|
+
});
|
|
442
|
+
if (sheets && !pruneCss) _bamboocss_logger.logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
|
|
443
|
+
if (!sheets || !pruneCss || !pending.length) return;
|
|
444
|
+
if (deferred) {
|
|
445
|
+
for (const result of results) session.deferredSheets.push({
|
|
446
|
+
environment: environmentName,
|
|
447
|
+
dir: (0, node_path.resolve)(outputDir),
|
|
448
|
+
originalFileName: result.original,
|
|
449
|
+
fileName: result.fileName,
|
|
450
|
+
source: result.source,
|
|
451
|
+
provisional: result.optimized,
|
|
452
|
+
sourcemap,
|
|
453
|
+
asset: result.asset,
|
|
454
|
+
bundle,
|
|
455
|
+
moved: result.moved
|
|
456
|
+
});
|
|
457
|
+
_bamboocss_logger.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.`);
|
|
458
|
+
} else _bamboocss_logger.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.`);
|
|
459
|
+
} finally {
|
|
460
|
+
outputProjection.restore();
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
/**
|
|
464
|
+
* The early half of the stylesheet's output lifecycle. @see `pruneEmittedSheets`
|
|
465
|
+
*
|
|
466
|
+
* A plugin of its own because one plugin carries one `generateBundle`, and this one has to be
|
|
467
|
+
* ordered `pre` while the checks in `bamboocssCss` have to see the finished bundle.
|
|
468
|
+
*/
|
|
469
|
+
const bamboocssCssEarly = (options) => ({
|
|
470
|
+
name: "bamboocss:css-early",
|
|
471
|
+
sharedDuringBuild: true,
|
|
472
|
+
/**
|
|
473
|
+
* A watch rebuild renders into the same output options object, so the names pruned by the
|
|
474
|
+
* previous build would otherwise still read as handled, and the sheet a rebuild re-emits
|
|
475
|
+
* under an unchanged name would ship unpruned.
|
|
476
|
+
*/
|
|
477
|
+
renderStart(outputOptions) {
|
|
478
|
+
options.session.prunedSheetNames.delete(outputOptions);
|
|
479
|
+
},
|
|
480
|
+
generateBundle: {
|
|
481
|
+
order: "pre",
|
|
482
|
+
async handler(outputOptions, bundle, isWrite) {
|
|
483
|
+
await pruneEmittedSheets(this, options.session, outputOptions, bundle, isWrite, options.pruneCss ?? true, true);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
/**
|
|
308
488
|
* Serve bamboo's stylesheet as a virtual module, in dev and in build.
|
|
309
489
|
*
|
|
310
490
|
* This is the integration itself, not an optimisation: without it nothing emits css and
|
|
@@ -363,6 +543,13 @@ const bamboocssCss = (options) => {
|
|
|
363
543
|
let pendingGeneration = -1;
|
|
364
544
|
let servedCss;
|
|
365
545
|
/**
|
|
546
|
+
* Whether the served stylesheet carries a source map: a dev server with Vite's
|
|
547
|
+
* `css.devSourcemap` on. Off, and the extraction pass records no call sites at all.
|
|
548
|
+
*/
|
|
549
|
+
let devSourcemap = false;
|
|
550
|
+
/** Each atom's first call site, by class name, from the last pass. @see `Builder.getAtomOrigins` */
|
|
551
|
+
let atomOrigins;
|
|
552
|
+
/**
|
|
366
553
|
* Held by the host for its whole length, rather than only around each mutation.
|
|
367
554
|
*
|
|
368
555
|
* Extraction fills the encoder this sheet is emitted from and `toCss` reads it back, with a
|
|
@@ -388,6 +575,7 @@ const bamboocssCss = (options) => {
|
|
|
388
575
|
graphAtomHashes = new Set(activeBuilder.context.encoder.atomic);
|
|
389
576
|
}
|
|
390
577
|
const css = activeBuilder.toCss({ layerParams: true });
|
|
578
|
+
atomOrigins = devSourcemap ? activeBuilder.getAtomOrigins?.() : void 0;
|
|
391
579
|
session.prunableClasses.clear();
|
|
392
580
|
session.viewTransitionClasses.clear();
|
|
393
581
|
if (graphAtomHashes && activeBuilder.context) {
|
|
@@ -480,7 +668,10 @@ const bamboocssCss = (options) => {
|
|
|
480
668
|
async configResolved(config) {
|
|
481
669
|
command = config.command;
|
|
482
670
|
host.setCommand(config.command);
|
|
671
|
+
devSourcemap = config.command === "serve" && Boolean(config.css?.devSourcemap);
|
|
672
|
+
host.setDevSourcemap(devSourcemap);
|
|
483
673
|
session.sourcemap = config.build.sourcemap;
|
|
674
|
+
session.cssCodeSplit = config.build.cssCodeSplit;
|
|
484
675
|
ssrBuildOptions = {
|
|
485
676
|
ssr: config.build.ssr,
|
|
486
677
|
ssrEmitAssets: config.build.ssrEmitAssets
|
|
@@ -556,24 +747,34 @@ const bamboocssCss = (options) => {
|
|
|
556
747
|
const generationAtStart = changeGeneration;
|
|
557
748
|
if (command === "serve" && servedCss?.generation === generationAtStart) {
|
|
558
749
|
if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
|
|
559
|
-
return servedCss.
|
|
750
|
+
return servedCss.map ? {
|
|
751
|
+
code: servedCss.css,
|
|
752
|
+
map: servedCss.map
|
|
753
|
+
} : servedCss.css;
|
|
560
754
|
}
|
|
561
755
|
let css;
|
|
756
|
+
let map;
|
|
562
757
|
try {
|
|
563
|
-
const
|
|
758
|
+
const cssOutput = command === "serve" ? await loadCssOutput() : void 0;
|
|
759
|
+
const validateDevCss = cssOutput?.pruneStaticCss;
|
|
564
760
|
const first = prebuilt;
|
|
565
761
|
prebuilt = void 0;
|
|
566
762
|
css = await (first ?? generate());
|
|
567
763
|
if (validateDevCss) css = validateDevCss(css, session, { prune: false });
|
|
764
|
+
if (devSourcemap && atomOrigins?.size) map = cssOutput?.cssSourceMap?.(css, atomOrigins);
|
|
568
765
|
} catch (error) {
|
|
569
766
|
throw asError(error, `failed to generate ${VIRTUAL_CSS_ID}`);
|
|
570
767
|
}
|
|
571
768
|
if (command === "serve" && generationAtStart === changeGeneration) servedCss = {
|
|
572
769
|
generation: generationAtStart,
|
|
573
|
-
css
|
|
770
|
+
css,
|
|
771
|
+
map
|
|
574
772
|
};
|
|
575
773
|
if (this.addWatchFile) for (const file of session.extractedFiles) this.addWatchFile(file);
|
|
576
|
-
return
|
|
774
|
+
return map ? {
|
|
775
|
+
code: css,
|
|
776
|
+
map
|
|
777
|
+
} : css;
|
|
577
778
|
},
|
|
578
779
|
configureServer(devServer) {
|
|
579
780
|
server = devServer;
|
|
@@ -615,49 +816,15 @@ const bamboocssCss = (options) => {
|
|
|
615
816
|
},
|
|
616
817
|
generateBundle: {
|
|
617
818
|
order: "post",
|
|
618
|
-
async handler(outputOptions, bundle) {
|
|
619
|
-
const
|
|
620
|
-
|
|
819
|
+
async handler(outputOptions, bundle, isWrite) {
|
|
820
|
+
const context = this;
|
|
821
|
+
await pruneEmittedSheets(context, session, outputOptions, bundle, isWrite, pruneCss, false);
|
|
822
|
+
const { containsGeneratedCssAsset } = await loadCssOutputModule();
|
|
823
|
+
const environment = context.environment;
|
|
621
824
|
const environmentName = environment?.name ?? "default";
|
|
622
825
|
const replacesGeneratedStylesheet = containsGeneratedCssAsset(bundle);
|
|
623
|
-
const outputProjection = session.beginOutputProjection(environmentName, outputOptions, bundle,
|
|
826
|
+
const outputProjection = session.beginOutputProjection(environmentName, outputOptions, bundle, false);
|
|
624
827
|
try {
|
|
625
|
-
/**
|
|
626
|
-
* Pruned against what this environment compiled, without waiting for the rest.
|
|
627
|
-
*
|
|
628
|
-
* The stylesheet is emitted and finalized by the environment that *imports* it, which
|
|
629
|
-
* in an SSR app is the client — and the client builds first, before the server
|
|
630
|
-
* environment has transformed a single module. Waiting for a complete answer therefore
|
|
631
|
-
* meant never pruning at all in any SSR framework: react-router, Remix, Nuxt, SvelteKit
|
|
632
|
-
* and Qwik all build the client first, and the client's output is on disk before the
|
|
633
|
-
* server environment starts. That is most production apps, and the feature was inert in
|
|
634
|
-
* every one of them — silently, since a build with nothing to prune looks identical.
|
|
635
|
-
*
|
|
636
|
-
* The reason for waiting was real: a class only the server graph reaches is not in this
|
|
637
|
-
* environment's reachability set, so pruning here removes rules the server-rendered
|
|
638
|
-
* markup still names. What makes it safe to prune anyway is that the mistake is
|
|
639
|
-
* *detectable* rather than silent — `buildEnd` in `plugin.ts` intersects every later
|
|
640
|
-
* environment's compiled classes against `prunedClasses` and fails the build naming
|
|
641
|
-
* them. A styled component that only ever renders on the server is the shape that
|
|
642
|
-
* trips it, and `pruneCss: false` is the answer when it does.
|
|
643
|
-
*
|
|
644
|
-
* So the trade is deliberate: a loud build failure in the rare case, in exchange for
|
|
645
|
-
* the feature working at all in the common one. It is the same reasoning as the
|
|
646
|
-
* unimported-`virtual:bamboo.css` check — a class with no rule behind it must never
|
|
647
|
-
* leave the build quietly.
|
|
648
|
-
*/
|
|
649
|
-
const pending = remainingEnvironments(session);
|
|
650
|
-
if (pending.length) _bamboocss_logger.logger.debug("vite", `Pruning against the ${JSON.stringify(environment?.name ?? "default")} environment with ${(0, _bamboocss_shared.truncateList)(pending, {
|
|
651
|
-
unit: "environment",
|
|
652
|
-
separator: ", "
|
|
653
|
-
})} still to compile. A class only those reach fails the build rather than shipping without its rule.`);
|
|
654
|
-
const { sheets } = optimizeStaticCssAssets(bundle, session, {
|
|
655
|
-
environment: environmentName,
|
|
656
|
-
prune: pruneCss,
|
|
657
|
-
requiredClasses: outputProjection.requiredClasses,
|
|
658
|
-
sourcemap: environment?.config?.build?.sourcemap
|
|
659
|
-
});
|
|
660
|
-
if (sheets && !pruneCss) _bamboocss_logger.logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
|
|
661
828
|
if (!outputProjection.cssLoaded) return;
|
|
662
829
|
if (!session.transformedFiles.size) return;
|
|
663
830
|
/**
|
|
@@ -759,6 +926,64 @@ const shouldTransform = (id) => {
|
|
|
759
926
|
* Returns `null` when the module is still a raw SFC and must be left to the framework plugin.
|
|
760
927
|
* Astro frontmatter is `---`, not `<script>`, so a tag check alone would parse the template.
|
|
761
928
|
*/
|
|
929
|
+
/** The module specifiers that reach bamboo — the `styled-system` paths and any `importMap` — per context. */
|
|
930
|
+
const entrypointNeedles = /* @__PURE__ */ new WeakMap();
|
|
931
|
+
/**
|
|
932
|
+
* Whether a module's text names a bamboo entrypoint at all.
|
|
933
|
+
*
|
|
934
|
+
* A textual test on purpose: it runs before the module is parsed, on modules outside the
|
|
935
|
+
* extraction inventory, to decide whether parsing is worth it. The outdir's own name is among
|
|
936
|
+
* the needles, so a relative import of the generated `styled-system` counts too.
|
|
937
|
+
*/
|
|
938
|
+
const namesEntrypoint = (ctx, code) => {
|
|
939
|
+
let needles = entrypointNeedles.get(ctx);
|
|
940
|
+
if (!needles) {
|
|
941
|
+
const outdirName = ctx.imports.outdir.split("/").filter(Boolean).at(-1);
|
|
942
|
+
needles = [...new Set([outdirName ?? "", ...Object.values(ctx.imports.value).flat()].filter((needle) => needle.length > 0))];
|
|
943
|
+
entrypointNeedles.set(ctx, needles);
|
|
944
|
+
}
|
|
945
|
+
return needles.some((needle) => code.includes(needle));
|
|
946
|
+
};
|
|
947
|
+
/** A module specifier in an import, export-from, dynamic import or require. */
|
|
948
|
+
const MODULE_SPECIFIER = /\b(?:from|import|require)\s*\(?\s*['"]([^'"]+)['"]/g;
|
|
949
|
+
/** How a relative specifier may name a file, in the order the bundler tries them. */
|
|
950
|
+
const SPECIFIER_SUFFIXES = [
|
|
951
|
+
"",
|
|
952
|
+
".ts",
|
|
953
|
+
".tsx",
|
|
954
|
+
".mts",
|
|
955
|
+
".cts",
|
|
956
|
+
".js",
|
|
957
|
+
".jsx",
|
|
958
|
+
".mjs",
|
|
959
|
+
".cjs",
|
|
960
|
+
"/index.ts",
|
|
961
|
+
"/index.tsx",
|
|
962
|
+
"/index.js"
|
|
963
|
+
];
|
|
964
|
+
/**
|
|
965
|
+
* Whether a module imports a module the shared project holds.
|
|
966
|
+
*
|
|
967
|
+
* Answered from project membership alone — no parse, no disk, no round trip to the compiler —
|
|
968
|
+
* which is what makes it affordable to ask of every module outside the extraction inventory.
|
|
969
|
+
* A relative specifier is tried against the file names a bundler would; a bare one against
|
|
970
|
+
* where the parser last resolved that package to, since a package the project holds a source
|
|
971
|
+
* of is one an included file imports a recipe from.
|
|
972
|
+
*/
|
|
973
|
+
const importsProjectModule = (ctx, filePath, code) => {
|
|
974
|
+
const directory = (0, node_path.dirname)(filePath);
|
|
975
|
+
for (const match of code.matchAll(MODULE_SPECIFIER)) {
|
|
976
|
+
const specifier = match[1];
|
|
977
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
978
|
+
const base = specifier.startsWith("/") ? specifier : (0, node_path.resolve)(directory, specifier);
|
|
979
|
+
for (const suffix of SPECIFIER_SUFFIXES) if (ctx.project.hasSourceFile(base + suffix)) return true;
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
const target = ctx.project.bareSpecifierTarget(specifier);
|
|
983
|
+
if (target && ctx.project.hasSourceFile(target)) return true;
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
};
|
|
762
987
|
const compilerParsePath = (id, code) => {
|
|
763
988
|
const [filePath, query = ""] = id.split("?");
|
|
764
989
|
if (!filePath) return null;
|
|
@@ -837,11 +1062,12 @@ const formatSkipped = (id, skipped) => {
|
|
|
837
1062
|
* therefore fold in the post plugin, after the framework has extracted them.
|
|
838
1063
|
*/
|
|
839
1064
|
const bamboocss = (options = {}) => {
|
|
840
|
-
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
|
|
1065
|
+
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true, splitCss = true } = options;
|
|
841
1066
|
(0, _bamboocss_node_static_compiler.markStaticCompilerActive)();
|
|
842
1067
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
843
1068
|
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.");
|
|
844
1069
|
const staticSession = createStaticCompilationSession();
|
|
1070
|
+
staticSession.splitCss = splitCss;
|
|
845
1071
|
/**
|
|
846
1072
|
* One Builder, one resolved config, one context and one ts-morph project for the run.
|
|
847
1073
|
*
|
|
@@ -1271,6 +1497,42 @@ const bamboocss = (options = {}) => {
|
|
|
1271
1497
|
staticSession.prunedClasses.clear();
|
|
1272
1498
|
for (const slots of liveOutputSlotsByEnvironment.values()) for (const slot of slots.values()) for (const className of slot.prunedClasses ?? []) staticSession.prunedClasses.add(className);
|
|
1273
1499
|
};
|
|
1500
|
+
/**
|
|
1501
|
+
* Prune the sheets the run wrote whole, now that every environment has contributed.
|
|
1502
|
+
*
|
|
1503
|
+
* Reached from the write hook of whichever environment completes the run, which is the first
|
|
1504
|
+
* point at which the union of every environment's reachability exists and every reference to
|
|
1505
|
+
* the sheet is on disk. A run that never completes — an environment declared and never built
|
|
1506
|
+
* — leaves the sheets whole, and says so as the process exits.
|
|
1507
|
+
*/
|
|
1508
|
+
const finalizeDeferredSheetsIfComplete = async (candidate, bundle, sourcemap) => {
|
|
1509
|
+
if (!staticSession.deferredSheets.length) return;
|
|
1510
|
+
if (remainingEnvironments(staticSession, candidate).length) return;
|
|
1511
|
+
const sheets = staticSession.deferredSheets.splice(0);
|
|
1512
|
+
const { finalizeDeferredSheets } = await loadCssOutputModule();
|
|
1513
|
+
const committed = staticSession.prunedClasses;
|
|
1514
|
+
staticSession.prunedClasses = /* @__PURE__ */ new Set();
|
|
1515
|
+
let finalized;
|
|
1516
|
+
let lost;
|
|
1517
|
+
try {
|
|
1518
|
+
finalized = finalizeDeferredSheets(sheets, staticSession, {
|
|
1519
|
+
prune: pruneCss,
|
|
1520
|
+
requiredClasses: currentRequiredClasses(),
|
|
1521
|
+
outputs: staticSession.writtenOutputs,
|
|
1522
|
+
bundle,
|
|
1523
|
+
sourcemap
|
|
1524
|
+
});
|
|
1525
|
+
} finally {
|
|
1526
|
+
lost = staticSession.prunedClasses;
|
|
1527
|
+
staticSession.prunedClasses = committed;
|
|
1528
|
+
}
|
|
1529
|
+
for (const sheet of finalized) for (const slot of liveOutputSlotsByEnvironment.get(sheet.environment)?.values() ?? []) slot.prunedClasses = new Set(lost);
|
|
1530
|
+
rebuildLivePrunedClasses();
|
|
1531
|
+
for (const sheet of finalized) {
|
|
1532
|
+
if (!sheet.renamed) continue;
|
|
1533
|
+
_bamboocss_logger.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}.`);
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1274
1536
|
const observeEnvironmentBuildStart = (environment) => {
|
|
1275
1537
|
const serial = (nextBuildSerialByEnvironment.get(environment) ?? 0) + 1;
|
|
1276
1538
|
nextBuildSerialByEnvironment.set(environment, serial);
|
|
@@ -1390,6 +1652,8 @@ const bamboocss = (options = {}) => {
|
|
|
1390
1652
|
rebuildStaticTransformContributions();
|
|
1391
1653
|
rebuildLivePrunedClasses();
|
|
1392
1654
|
};
|
|
1655
|
+
staticSession.finalizeDeferred = ({ environment, bundle, sourcemap }) => finalizeDeferredSheetsIfComplete(environment, bundle, sourcemap);
|
|
1656
|
+
staticSession.classNamesOf = (environment, moduleId) => transformStateByEnvironment.get(environment)?.transformArtifactsByModule.get(moduleId)?.classNames;
|
|
1393
1657
|
staticSession.beginOutputProjection = (environment, outputOptions, bundle, replacesGeneratedStylesheet) => {
|
|
1394
1658
|
const generation = preparedGenerations.get(environment);
|
|
1395
1659
|
if (!generation || transformStateByEnvironment.get(environment) !== generation.state) {
|
|
@@ -1412,9 +1676,13 @@ const bamboocss = (options = {}) => {
|
|
|
1412
1676
|
restore() {
|
|
1413
1677
|
if (restored) return;
|
|
1414
1678
|
restored = true;
|
|
1679
|
+
const previous = outputStageByBundle.get(bundle) ?? outputStageByOptions.get(outputOptions);
|
|
1415
1680
|
const stage = replacesGeneratedStylesheet ? {
|
|
1416
1681
|
cssDigest: bambooCssDigest(bundle),
|
|
1417
|
-
prunedClasses: new Set(staticSession.prunedClasses)
|
|
1682
|
+
prunedClasses: new Set([...previous?.prunedClasses ?? [], ...staticSession.prunedClasses])
|
|
1683
|
+
} : previous ? {
|
|
1684
|
+
cssDigest: bambooCssDigest(bundle),
|
|
1685
|
+
prunedClasses: new Set(previous.prunedClasses ?? [])
|
|
1418
1686
|
} : {};
|
|
1419
1687
|
outputStageByBundle.set(bundle, stage);
|
|
1420
1688
|
outputStageByOptions.set(outputOptions, stage);
|
|
@@ -1756,6 +2024,7 @@ const bamboocss = (options = {}) => {
|
|
|
1756
2024
|
let styleCompiler;
|
|
1757
2025
|
let command = "build";
|
|
1758
2026
|
let defaultEmitAssets = true;
|
|
2027
|
+
let exitWarningInstalled = false;
|
|
1759
2028
|
/**
|
|
1760
2029
|
* Expand semantic leaf reads through the Project's exact resolution paths.
|
|
1761
2030
|
*
|
|
@@ -1998,6 +2267,16 @@ const bamboocss = (options = {}) => {
|
|
|
1998
2267
|
command = config.command;
|
|
1999
2268
|
host.setCommand(config.command);
|
|
2000
2269
|
defaultEmitAssets = config.build?.emitAssets ?? (!config.build?.ssr || config.build?.ssrEmitAssets === true);
|
|
2270
|
+
if (config.command === "build" && !exitWarningInstalled) {
|
|
2271
|
+
exitWarningInstalled = true;
|
|
2272
|
+
process.once("beforeExit", () => {
|
|
2273
|
+
if (!staticSession.deferredSheets.length) return;
|
|
2274
|
+
_bamboocss_logger.logger.warn("vite", `The stylesheet was pruned against an incomplete run: ${(0, _bamboocss_shared.truncateList)(remainingEnvironments(staticSession), {
|
|
2275
|
+
unit: "environment",
|
|
2276
|
+
separator: ", "
|
|
2277
|
+
})} 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.`);
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2001
2280
|
const plugins = config.plugins;
|
|
2002
2281
|
if (plugins) {
|
|
2003
2282
|
for (const finalizer of [outputWriteObserver, memoryOutputCommitter]) {
|
|
@@ -2182,11 +2461,11 @@ const bamboocss = (options = {}) => {
|
|
|
2182
2461
|
rebuildStaticTransformContributions(environment, state);
|
|
2183
2462
|
const survivors = allSurvivors(states);
|
|
2184
2463
|
if (survivors.length) throw createSurvivorError(survivors);
|
|
2185
|
-
const lost = currentWillEmitCss ? [] : [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(require_class_name.bare(className)));
|
|
2464
|
+
const lost = currentWillEmitCss || staticSession.deferredSheets.length ? [] : [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(require_class_name.bare(className)));
|
|
2186
2465
|
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${(0, _bamboocss_shared.truncateList)(lost.map((className) => ` ${className}`), {
|
|
2187
2466
|
unit: "class",
|
|
2188
2467
|
separator: "\n"
|
|
2189
|
-
})}\n\nThe stylesheet
|
|
2468
|
+
})}\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.`);
|
|
2190
2469
|
const remaining = remainingEnvironments(staticSession, environment);
|
|
2191
2470
|
if (typeof this.getModuleInfo === "function" && !remaining.length) {
|
|
2192
2471
|
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.`);
|
|
@@ -2231,6 +2510,10 @@ const bamboocss = (options = {}) => {
|
|
|
2231
2510
|
if (!ctx || !foldSourceImpl || !runtimeCss || !styleCompiler) return null;
|
|
2232
2511
|
const [filePath] = id.split("?");
|
|
2233
2512
|
if (isGeneratedOutput(filePath, ctx)) return null;
|
|
2513
|
+
if (!(ctx.project.hasSourceFile(filePath) && ctx.project.getSourceFile(filePath)?.getFullText() === code) && !namesEntrypoint(ctx, code) && !importsProjectModule(ctx, filePath, code)) {
|
|
2514
|
+
_bamboocss_logger.logger.debug("vite:transform", `Skipped ${filePath}: ${host.isSourceFile(filePath) ? "rewritten before bamboo" : "outside `include`"}, and it reaches nothing bamboo`);
|
|
2515
|
+
return null;
|
|
2516
|
+
}
|
|
2234
2517
|
const requestedParsePath = compilerParsePath(id, code);
|
|
2235
2518
|
if (requestedParsePath === null) return null;
|
|
2236
2519
|
const state = environmentState(this);
|
|
@@ -2393,9 +2676,17 @@ const bamboocss = (options = {}) => {
|
|
|
2393
2676
|
writeBundle: {
|
|
2394
2677
|
order: "pre",
|
|
2395
2678
|
sequential: true,
|
|
2396
|
-
handler(outputOptions, bundle) {
|
|
2679
|
+
async handler(outputOptions, bundle) {
|
|
2680
|
+
const environment = environmentName(this);
|
|
2681
|
+
const outputDir = outputOptions.dir ?? (outputOptions.file ? (0, node_path.dirname)(outputOptions.file) : void 0);
|
|
2682
|
+
if (outputDir) staticSession.writtenOutputs.push({
|
|
2683
|
+
environment,
|
|
2684
|
+
dir: (0, node_path.resolve)(outputDir),
|
|
2685
|
+
files: Object.values(bundle).map((output) => output.fileName)
|
|
2686
|
+
});
|
|
2397
2687
|
const identity = outputIdentityByBundle.get(bundle) ?? outputIdentityByOptions.get(outputOptions);
|
|
2398
|
-
if (identity?.environment ===
|
|
2688
|
+
if (identity?.environment === environment) publishPreparedOutput(identity.environment, identity.outputToken, identity.outputSlot, true);
|
|
2689
|
+
await finalizeDeferredSheetsIfComplete();
|
|
2399
2690
|
}
|
|
2400
2691
|
}
|
|
2401
2692
|
};
|
|
@@ -2419,6 +2710,10 @@ const bamboocss = (options = {}) => {
|
|
|
2419
2710
|
session: staticSession,
|
|
2420
2711
|
pruneCss
|
|
2421
2712
|
}),
|
|
2713
|
+
bamboocssCssEarly({
|
|
2714
|
+
session: staticSession,
|
|
2715
|
+
pruneCss
|
|
2716
|
+
}),
|
|
2422
2717
|
compiler,
|
|
2423
2718
|
compilerSfc
|
|
2424
2719
|
];
|
package/dist/index.d.cts
CHANGED
|
@@ -62,6 +62,17 @@ interface BambooVitePluginOptions {
|
|
|
62
62
|
* @default true
|
|
63
63
|
*/
|
|
64
64
|
pruneCss?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Give each lazily loaded chunk a stylesheet of the utilities only it uses, and keep the
|
|
67
|
+
* rest in the entry sheet. Builds only, and only where Vite's own `build.cssCodeSplit` is on.
|
|
68
|
+
*
|
|
69
|
+
* An atom two chunks use stays in the entry sheet, so nothing is ever downloaded twice; a
|
|
70
|
+
* route that is the only user of a style downloads that style with the route. Precedence
|
|
71
|
+
* does not depend on which sheet a rule is in, since it lives in the cascade sublayers.
|
|
72
|
+
*
|
|
73
|
+
* @default true
|
|
74
|
+
*/
|
|
75
|
+
splitCss?: boolean;
|
|
65
76
|
}
|
|
66
77
|
/**
|
|
67
78
|
* Vite integration for Bamboo CSS.
|
package/dist/index.d.mts
CHANGED
|
@@ -64,6 +64,17 @@ interface BambooVitePluginOptions {
|
|
|
64
64
|
* @default true
|
|
65
65
|
*/
|
|
66
66
|
pruneCss?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Give each lazily loaded chunk a stylesheet of the utilities only it uses, and keep the
|
|
69
|
+
* rest in the entry sheet. Builds only, and only where Vite's own `build.cssCodeSplit` is on.
|
|
70
|
+
*
|
|
71
|
+
* An atom two chunks use stays in the entry sheet, so nothing is ever downloaded twice; a
|
|
72
|
+
* route that is the only user of a style downloads that style with the route. Precedence
|
|
73
|
+
* does not depend on which sheet a rule is in, since it lives in the cascade sublayers.
|
|
74
|
+
*
|
|
75
|
+
* @default true
|
|
76
|
+
*/
|
|
77
|
+
splitCss?: boolean;
|
|
67
78
|
}
|
|
68
79
|
/**
|
|
69
80
|
* Vite integration for Bamboo CSS.
|