@bamboocss/vite 1.37.12 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +289 -70
- package/dist/index.d.cts +21 -10
- package/dist/index.d.mts +21 -10
- package/dist/index.mjs +289 -70
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -94,6 +94,7 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
|
|
|
94
94
|
if (!className || !prunable.has(className) || used.has(className)) return;
|
|
95
95
|
candidate.remove();
|
|
96
96
|
removedAny = true;
|
|
97
|
+
session.prunedClasses.add(className);
|
|
97
98
|
});
|
|
98
99
|
}).processSync(rule.selector);
|
|
99
100
|
} catch {
|
|
@@ -154,6 +155,56 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
|
|
|
154
155
|
return root.toString();
|
|
155
156
|
};
|
|
156
157
|
//#endregion
|
|
158
|
+
//#region src/static-session.ts
|
|
159
|
+
const createStaticCompilationSession = () => {
|
|
160
|
+
const session = {
|
|
161
|
+
utilityLayer: "utilities",
|
|
162
|
+
sourcemap: false,
|
|
163
|
+
cssLoaded: false,
|
|
164
|
+
transformedFiles: /* @__PURE__ */ new Set(),
|
|
165
|
+
extractedFiles: /* @__PURE__ */ new Set(),
|
|
166
|
+
prunableClasses: /* @__PURE__ */ new Set(),
|
|
167
|
+
viewTransitionClasses: /* @__PURE__ */ new Set(),
|
|
168
|
+
usedClasses: /* @__PURE__ */ new Set(),
|
|
169
|
+
expectedEnvironments: void 0,
|
|
170
|
+
startedEnvironments: /* @__PURE__ */ new Set(),
|
|
171
|
+
prunedClasses: /* @__PURE__ */ new Set(),
|
|
172
|
+
markClassUsed(className) {
|
|
173
|
+
for (const token of className.split(" ")) {
|
|
174
|
+
if (!token) continue;
|
|
175
|
+
session.usedClasses.add(token.includes("\\") ? token : (0, _bamboocss_shared.esc)(token));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
return session;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Environments this run intends to build that have not been compiled yet.
|
|
183
|
+
*
|
|
184
|
+
* Empty means everything the run will contribute has been contributed, which is the condition
|
|
185
|
+
* every whole-run judgement here waits for: pruning the stylesheet against reachability, and
|
|
186
|
+
* the two guards that ask whether the compiled modules and the extraction graph agree. Each of
|
|
187
|
+
* those is false about a build in progress and true only about a finished one.
|
|
188
|
+
*
|
|
189
|
+
* Empty is also the answer for a single-environment build, where nothing announced an
|
|
190
|
+
* environment list because there is only ever one — so that path is unchanged.
|
|
191
|
+
*
|
|
192
|
+
* An environment a run declares and then never builds leaves this permanently non-empty, and
|
|
193
|
+
* those judgements are skipped for the run. Every one of them errs towards shipping more CSS
|
|
194
|
+
* or asserting less, so that is the safe direction to be wrong in.
|
|
195
|
+
*/
|
|
196
|
+
const remainingEnvironments = (session) => [...session.expectedEnvironments ?? []].filter((name) => !session.startedEnvironments.has(name));
|
|
197
|
+
const resetStaticCompilationSession = (session) => {
|
|
198
|
+
session.cssLoaded = false;
|
|
199
|
+
session.transformedFiles.clear();
|
|
200
|
+
session.extractedFiles.clear();
|
|
201
|
+
session.prunableClasses.clear();
|
|
202
|
+
session.viewTransitionClasses.clear();
|
|
203
|
+
session.usedClasses.clear();
|
|
204
|
+
session.startedEnvironments.clear();
|
|
205
|
+
session.prunedClasses.clear();
|
|
206
|
+
};
|
|
207
|
+
//#endregion
|
|
157
208
|
//#region src/css.ts
|
|
158
209
|
/**
|
|
159
210
|
* What a project imports to get the stylesheet.
|
|
@@ -255,32 +306,37 @@ const replaceAssetReferences = (bundle, previous, next, sourcemap) => {
|
|
|
255
306
|
*/
|
|
256
307
|
const carriesGeneratedCss = (output) => output.type === "asset" && output.fileName.endsWith(".css");
|
|
257
308
|
/**
|
|
258
|
-
* Prune
|
|
309
|
+
* Prune compiler-owned CSS, then give any sheet whose bytes changed a hash of those bytes.
|
|
259
310
|
*
|
|
260
311
|
* Rollup has already expanded `[hash]` when `generateBundle` runs. Mutating only `source`
|
|
261
312
|
* would therefore leave two different reachable subsets under one CDN key. The extra final
|
|
262
313
|
* hash is not cosmetic: it makes late graph reachability cache-safe.
|
|
314
|
+
*
|
|
315
|
+
* Renaming is therefore not a choice this takes. Pruned bytes under the unpruned sheet's name
|
|
316
|
+
* is the one outcome that must never be reachable, and a sheet nothing was removed from keeps
|
|
317
|
+
* its name because its bytes are unchanged — so "rename" is a consequence of "the bytes moved",
|
|
318
|
+
* not a second option. `prune` is the only knob.
|
|
263
319
|
*/
|
|
264
320
|
const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
265
|
-
const {
|
|
321
|
+
const { prune = true, sourcemap = session.sourcemap } = options;
|
|
322
|
+
/** Assets in this bundle that carry the generated stylesheet, pruned or not. */
|
|
323
|
+
let sheets = 0;
|
|
266
324
|
for (const output of Object.values(bundle)) {
|
|
267
325
|
if (!carriesGeneratedCss(output)) continue;
|
|
268
326
|
const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString();
|
|
269
327
|
if (!source.includes("--made-with-bamboo")) continue;
|
|
328
|
+
sheets++;
|
|
329
|
+
if (!prune) continue;
|
|
270
330
|
const optimized = pruneStaticCss(source, session);
|
|
271
331
|
output.source = optimized;
|
|
272
332
|
if (optimized === source) continue;
|
|
273
|
-
if (!rename) {
|
|
274
|
-
output.source = source;
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
333
|
const nextName = output.fileName.replace(/\.css$/, `.b-${(0, _bamboocss_shared.toHash)(optimized)}.css`);
|
|
278
|
-
if (nextName === output.fileName) continue;
|
|
279
334
|
if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
|
|
280
335
|
const previous = output.fileName;
|
|
281
336
|
output.fileName = nextName;
|
|
282
|
-
replaceAssetReferences(bundle, previous, nextName,
|
|
337
|
+
replaceAssetReferences(bundle, previous, nextName, sourcemap);
|
|
283
338
|
}
|
|
339
|
+
return { sheets };
|
|
284
340
|
};
|
|
285
341
|
/**
|
|
286
342
|
* Serve bamboo's stylesheet as a virtual module, in dev and in build.
|
|
@@ -296,7 +352,7 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
|
296
352
|
* process just wrote, which is a race on any watch rebuild.
|
|
297
353
|
*/
|
|
298
354
|
const bamboocssCss = (options) => {
|
|
299
|
-
const { configPath, cwd, session,
|
|
355
|
+
const { configPath, cwd, session, pruneCss = true } = options;
|
|
300
356
|
/**
|
|
301
357
|
* Environments whose `load` served the virtual stylesheet.
|
|
302
358
|
*
|
|
@@ -356,9 +412,32 @@ const bamboocssCss = (options) => {
|
|
|
356
412
|
};
|
|
357
413
|
return {
|
|
358
414
|
name: "bamboocss:css",
|
|
415
|
+
/**
|
|
416
|
+
* One instance for every environment of a build, rather than one per environment.
|
|
417
|
+
*
|
|
418
|
+
* Vite re-reads the config file once per environment, so a project that lists this plugin
|
|
419
|
+
* in `vite.config.ts` — every project — got a *fresh* instance per environment, each with
|
|
420
|
+
* its own compilation session, context and ts-morph project. Nothing an environment
|
|
421
|
+
* established could then be seen by the next one, which is the premise the reachability
|
|
422
|
+
* accounting below is built on, and it also meant the whole config load and extraction
|
|
423
|
+
* happened once per environment.
|
|
424
|
+
*/
|
|
425
|
+
sharedDuringBuild: true,
|
|
359
426
|
configResolved(config) {
|
|
360
427
|
command = config.command;
|
|
361
428
|
session.sourcemap = config.build.sourcemap;
|
|
429
|
+
if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
|
|
430
|
+
},
|
|
431
|
+
/**
|
|
432
|
+
* The definitive environment list, for a run that reaches `builder.buildApp()` without
|
|
433
|
+
* configuring `builder` — the shape `vite build` itself takes, where exactly one
|
|
434
|
+
* environment is set up and pruning is therefore safe.
|
|
435
|
+
*/
|
|
436
|
+
buildApp: {
|
|
437
|
+
order: "pre",
|
|
438
|
+
async handler(builder) {
|
|
439
|
+
session.expectedEnvironments = new Set(Object.keys(builder.environments));
|
|
440
|
+
}
|
|
362
441
|
},
|
|
363
442
|
resolveId(id) {
|
|
364
443
|
const query = queryOf(id);
|
|
@@ -398,8 +477,34 @@ const bamboocssCss = (options) => {
|
|
|
398
477
|
generateBundle: {
|
|
399
478
|
order: "post",
|
|
400
479
|
handler(_, bundle) {
|
|
401
|
-
|
|
402
|
-
|
|
480
|
+
const environment = this.environment;
|
|
481
|
+
/**
|
|
482
|
+
* Every environment of this run has already had its modules compiled.
|
|
483
|
+
*
|
|
484
|
+
* Reachability is what pruning removes rules against, and it is only complete once
|
|
485
|
+
* nothing is left to contribute to it. The stylesheet is emitted and finalized by the
|
|
486
|
+
* environment that *imports* it, which in an SSR app is the client — and the client
|
|
487
|
+
* builds first, before the server environment has transformed a single module. Pruning
|
|
488
|
+
* there deletes every rule for a class only the server graph reaches, and the pages
|
|
489
|
+
* link the pruned copy: one project lost 39% of its atoms that way, presenting as
|
|
490
|
+
* rarely-used classes such as `md:{display:inline-block}` silently not applying.
|
|
491
|
+
*
|
|
492
|
+
* So the full extracted stylesheet ships instead, which is what `pruneCss: false` asks
|
|
493
|
+
* for by hand. Being the last environment is not the common case — frameworks build the
|
|
494
|
+
* client first — but it is the only one where the answer is knowable, and a framework
|
|
495
|
+
* that builds its server bundle first does get pruned output.
|
|
496
|
+
*/
|
|
497
|
+
const pending = remainingEnvironments(session);
|
|
498
|
+
const { sheets } = optimizeStaticCssAssets(bundle, session, {
|
|
499
|
+
prune: pruneCss && pending.length === 0,
|
|
500
|
+
sourcemap: environment?.config?.build?.sourcemap
|
|
501
|
+
});
|
|
502
|
+
if (sheets && !pruneCss) _bamboocss_logger.logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
|
|
503
|
+
else if (sheets && pending.length) _bamboocss_logger.logger.info("vite", `Reachability pruning skipped: the stylesheet is emitted by the ${JSON.stringify(environment?.name ?? "default")} environment, and ${(0, _bamboocss_shared.truncateList)(pending, {
|
|
504
|
+
unit: "environment",
|
|
505
|
+
separator: ", "
|
|
506
|
+
})} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
|
|
507
|
+
if (!servedEnvironments.has(environment?.name ?? "default")) return;
|
|
403
508
|
if (!session.transformedFiles.size) return;
|
|
404
509
|
if (!Object.values(bundle).some((output) => {
|
|
405
510
|
if (!carriesGeneratedCss(output)) return false;
|
|
@@ -412,6 +517,78 @@ const bamboocssCss = (options) => {
|
|
|
412
517
|
//#endregion
|
|
413
518
|
//#region src/fold-analysis.ts
|
|
414
519
|
/**
|
|
520
|
+
* Nodes that *compose* a value out of their children rather than computing one.
|
|
521
|
+
*
|
|
522
|
+
* The boundary of the walk below, and the whole of its precision. Climbing through these
|
|
523
|
+
* keeps a value's provenance: `'red.300'` inside `{ color: 'red.300' }` inside a default is
|
|
524
|
+
* still the default's. Anything else — a call, a function body, a JSX element — produces its
|
|
525
|
+
* value by being evaluated, so what is written inside it is an argument to that evaluation
|
|
526
|
+
* and not the enclosing default's value.
|
|
527
|
+
*
|
|
528
|
+
* Without the boundary, `({ cls = css({ color: 'red.300' }) }) => cls` is rejected: the call's
|
|
529
|
+
* own literal argument is syntactically inside a default, so an unbounded walk calls it one.
|
|
530
|
+
* That is correct code, and rejecting it fails the build.
|
|
531
|
+
*/
|
|
532
|
+
const composesValue = (node) => ts_morph.Node.isObjectLiteralExpression(node) || ts_morph.Node.isArrayLiteralExpression(node) || ts_morph.Node.isPropertyAssignment(node) || ts_morph.Node.isShorthandPropertyAssignment(node) || ts_morph.Node.isSpreadAssignment(node) || ts_morph.Node.isSpreadElement(node) || ts_morph.Node.isAsExpression(node) || ts_morph.Node.isParenthesizedExpression(node) || ts_morph.Node.isNonNullExpression(node) || ts_morph.Node.isTypeAssertion(node) || ts_morph.Node.isSatisfiesExpression(node);
|
|
533
|
+
/** Is `inner` written within `outer`? Positions rather than a walk, so it is O(1). */
|
|
534
|
+
const contains = (outer, inner) => outer.getSourceFile() === inner.getSourceFile() && outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
|
|
535
|
+
/**
|
|
536
|
+
* Did this value come from the `= …` of a destructuring binding?
|
|
537
|
+
*
|
|
538
|
+
* `const { tone = 'red.300' } = source` boxes as the literal `'red.300'`: the extractor's
|
|
539
|
+
* `maybeDefinitionValue` tests for an initializer first and returns the boxed default, never
|
|
540
|
+
* reaching the branch that would read `source`. So the default is reported as the value whether
|
|
541
|
+
* or not it is the one that applies.
|
|
542
|
+
*
|
|
543
|
+
* For extraction that is merely optimistic, and deliberately so: a CLI or PostCSS build ships a
|
|
544
|
+
* runtime `css()`, where the default genuinely does apply when the caller omits the key, and it
|
|
545
|
+
* needs a rule behind it. Folding is where the same resolution turns into a wrong answer,
|
|
546
|
+
* because the call is *replaced* by that value.
|
|
547
|
+
*
|
|
548
|
+
* Stops at the first non-composing parent, so a call written inside a default keeps its own
|
|
549
|
+
* provenance, and checks that the binding element was reached through its initializer, so
|
|
550
|
+
* `{ tone = X }`'s name node is not mistaken for its default.
|
|
551
|
+
*/
|
|
552
|
+
const isBindingElementDefault = (node) => {
|
|
553
|
+
if (node && ts_morph.Node.isCallExpression(node)) return false;
|
|
554
|
+
let current = node;
|
|
555
|
+
while (current) {
|
|
556
|
+
const parent = current.getParent();
|
|
557
|
+
if (!parent) return false;
|
|
558
|
+
if (ts_morph.Node.isBindingElement(parent)) return parent.getInitializer() === current;
|
|
559
|
+
if (!composesValue(parent)) return false;
|
|
560
|
+
current = parent;
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
};
|
|
564
|
+
/**
|
|
565
|
+
* The same question asked of a whole box, including how it was resolved.
|
|
566
|
+
*
|
|
567
|
+
* The node a box reports is not always the one its value came from — an empty `{}` default
|
|
568
|
+
* boxes against the call rather than against the `{}` — so the resolution stack is consulted
|
|
569
|
+
* too. A binding element reaches that stack by having been resolved *through*, which is the
|
|
570
|
+
* signal the extractor itself reads when one of these is a conditional's test.
|
|
571
|
+
*
|
|
572
|
+
* Only the entries that are binding elements are examined. Walking up from every other entry
|
|
573
|
+
* was tried and never once changed a verdict across the default spellings or the sandbox's own
|
|
574
|
+
* modules, while accounting for most of the parent hops this does — the stack carries nodes
|
|
575
|
+
* that were never resolved through, including a call's own arguments, so walking from them is
|
|
576
|
+
* both the expensive half and the one that reaches conclusions it has no basis for.
|
|
577
|
+
*
|
|
578
|
+
* A binding element without an initializer carries no default to mistrust: `const { tone } =
|
|
579
|
+
* source` either resolves from `source` or does not resolve at all.
|
|
580
|
+
*/
|
|
581
|
+
const isFromBindingDefault = (node) => {
|
|
582
|
+
const own = node.getNode?.();
|
|
583
|
+
if (isBindingElementDefault(own)) return true;
|
|
584
|
+
for (const entry of node.getStack?.() ?? []) {
|
|
585
|
+
if (!ts_morph.Node.isBindingElement(entry) || !entry.getInitializer()) continue;
|
|
586
|
+
if (own && contains(entry, own)) continue;
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
return false;
|
|
590
|
+
};
|
|
591
|
+
/**
|
|
415
592
|
* Statically resolvable means: every box in the tree carries a known value.
|
|
416
593
|
*
|
|
417
594
|
* `unresolvable` is the extractor saying it could not evaluate a node.
|
|
@@ -425,6 +602,7 @@ const isStaticBox = (node, seen = /* @__PURE__ */ new Set()) => {
|
|
|
425
602
|
seen.add(node);
|
|
426
603
|
if (_bamboocss_extractor.box.isUnresolvable(node) || _bamboocss_extractor.box.isConditional(node)) return false;
|
|
427
604
|
if (!("type" in node) || node.type == null) return false;
|
|
605
|
+
if (isFromBindingDefault(node)) return false;
|
|
428
606
|
if (_bamboocss_extractor.box.isLiteral(node) && node.value === void 0) {
|
|
429
607
|
const source = node.getNode?.();
|
|
430
608
|
return Boolean(source && ts_morph.Node.isIdentifier(source) && source.getText() === "undefined");
|
|
@@ -2784,40 +2962,50 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
|
|
|
2784
2962
|
};
|
|
2785
2963
|
};
|
|
2786
2964
|
//#endregion
|
|
2787
|
-
//#region src/static-session.ts
|
|
2788
|
-
const createStaticCompilationSession = () => {
|
|
2789
|
-
const session = {
|
|
2790
|
-
utilityLayer: "utilities",
|
|
2791
|
-
sourcemap: false,
|
|
2792
|
-
cssLoaded: false,
|
|
2793
|
-
transformedFiles: /* @__PURE__ */ new Set(),
|
|
2794
|
-
extractedFiles: /* @__PURE__ */ new Set(),
|
|
2795
|
-
prunableClasses: /* @__PURE__ */ new Set(),
|
|
2796
|
-
viewTransitionClasses: /* @__PURE__ */ new Set(),
|
|
2797
|
-
usedClasses: /* @__PURE__ */ new Set(),
|
|
2798
|
-
markClassUsed(className) {
|
|
2799
|
-
for (const token of className.split(" ")) {
|
|
2800
|
-
if (!token) continue;
|
|
2801
|
-
session.usedClasses.add(token.includes("\\") ? token : (0, _bamboocss_shared.esc)(token));
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
};
|
|
2805
|
-
return session;
|
|
2806
|
-
};
|
|
2807
|
-
const resetStaticCompilationSession = (session) => {
|
|
2808
|
-
session.cssLoaded = false;
|
|
2809
|
-
session.transformedFiles.clear();
|
|
2810
|
-
session.extractedFiles.clear();
|
|
2811
|
-
session.prunableClasses.clear();
|
|
2812
|
-
session.viewTransitionClasses.clear();
|
|
2813
|
-
session.usedClasses.clear();
|
|
2814
|
-
};
|
|
2815
|
-
//#endregion
|
|
2816
2965
|
//#region src/plugin.ts
|
|
2817
2966
|
const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
|
|
2818
2967
|
const NODE_MODULES = /node_modules/;
|
|
2968
|
+
/**
|
|
2969
|
+
* Queries that make Vite serve something other than the module's own source.
|
|
2970
|
+
*
|
|
2971
|
+
* `./theme.tsx?raw` is a module whose text is `export default "…"`, and `?url`, `?worker` and
|
|
2972
|
+
* `?sharedworker` are wrappers of the same kind. The query has to be stripped before the
|
|
2973
|
+
* extension is tested — otherwise nothing matches `.tsx` — and stripping it is what made these
|
|
2974
|
+
* look like the file itself. The transform then handed the wrapper's text to ts-morph *under
|
|
2975
|
+
* the real file's path*, overwriting the parsed module every fold reads for that path.
|
|
2976
|
+
*
|
|
2977
|
+
* That is not theoretical: a module folding `css(shared)` against a sibling the entry also
|
|
2978
|
+
* imported as `?raw` failed the build with "1 call(s) could not be compiled" — the compiler
|
|
2979
|
+
* had read `export default "…"` and found no `shared` to resolve. The advice it prints, to
|
|
2980
|
+
* make the value statically analyzable, is unfollowable, because the source already was.
|
|
2981
|
+
*
|
|
2982
|
+
* Whether it bites depends on which of the two ids Rollup transforms last, so the same project
|
|
2983
|
+
* can build and then stop building because an import moved.
|
|
2984
|
+
*
|
|
2985
|
+
* A deny list rather than an allow list of benign queries: dev ids carry `?t=` after an edit
|
|
2986
|
+
* and `?import` when a dynamic import is rewritten, and rejecting an unrecognised one of those
|
|
2987
|
+
* would silently stop folding a module rather than loudly refuse it.
|
|
2988
|
+
*
|
|
2989
|
+
* Exactly these four, matching Vite's own `SPECIAL_QUERY_RE`. The list was drafted wider —
|
|
2990
|
+
* `?inline`, `?no-inline`, `?worklet`, `?init` — and every one of those was wrong: Vite has no
|
|
2991
|
+
* `worklet` query at all, `?init` is `.wasm` only and that extension is already rejected below,
|
|
2992
|
+
* and `inline`/`no-inline` merely pick base64-versus-file for something that *already* matched
|
|
2993
|
+
* `raw`/`url`, so `./a.tsx?inline` is served as the module's own source. Rejecting an id that
|
|
2994
|
+
* carries real source is the expensive direction: the transform declines, its atoms never reach
|
|
2995
|
+
* the reachability set, pruning removes their rules, and the runtime still returns the class
|
|
2996
|
+
* names — unstyled elements, no error. Only names verified against Vite belong here.
|
|
2997
|
+
*
|
|
2998
|
+
* Note `?worker_file`, which is how dev serves a worker's *real* source, is deliberately absent
|
|
2999
|
+
* and must stay absent. It contains "worker" and is the obvious next entry; adding it would
|
|
3000
|
+
* stop folding every worker module in dev, silently, by the mechanism above.
|
|
3001
|
+
*
|
|
3002
|
+
* Tested against the whole id rather than a split-off query, so it cannot disagree with
|
|
3003
|
+
* `queryOf` in `css.ts` about where the query starts.
|
|
3004
|
+
*/
|
|
3005
|
+
const WRAPPED_MODULE_QUERY = /[?&](?:raw|url|worker|sharedworker)(?:&|=|$)/;
|
|
2819
3006
|
const shouldTransform = (id) => {
|
|
2820
3007
|
if (id.startsWith("\0")) return false;
|
|
3008
|
+
if (WRAPPED_MODULE_QUERY.test(id)) return false;
|
|
2821
3009
|
const [filePath] = id.split("?");
|
|
2822
3010
|
if (!filePath) return false;
|
|
2823
3011
|
if (NODE_MODULES.test(filePath)) return false;
|
|
@@ -2864,17 +3052,26 @@ const formatSkipped = (id, skipped) => {
|
|
|
2864
3052
|
* with no matching rule.
|
|
2865
3053
|
*/
|
|
2866
3054
|
const bamboocss = (options = {}) => {
|
|
2867
|
-
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates,
|
|
3055
|
+
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
|
|
2868
3056
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
3057
|
+
if ("renameCssAsset" in options) throw new Error("bamboocss: `renameCssAsset` has been replaced by `pruneCss`. Use `pruneCss: false` for what `renameCssAsset: false` did — it always disabled the pruning as well, since pruned bytes under the unpruned sheet's name is what lets a CDN serve a stale stylesheet. The new name says which of the two it is really about.");
|
|
3058
|
+
/**
|
|
3059
|
+
* What each file's transform found, for the summary. Keyed by file rather than summed as it
|
|
3060
|
+
* goes, because a build has more than one environment and they share most of their modules.
|
|
3061
|
+
*
|
|
3062
|
+
* Running totals double-counted every shared module once per environment — a two-environment
|
|
3063
|
+
* build of one shared file and one entry each reported "2/2 across 2/4 files" for three
|
|
3064
|
+
* source modules. Coverage is a property of the source, not of how many times a bundler
|
|
3065
|
+
* handed the same file over. It also grew without bound in dev, where every HMR
|
|
3066
|
+
* re-transform of a file counted as another file.
|
|
3067
|
+
*
|
|
3068
|
+
* A second pass over a file replaces its entry rather than adding to it. Both environments
|
|
3069
|
+
* are assumed to compute the same answer for the same module — true of this compiler, though
|
|
3070
|
+
* not something the plugin can enforce, since another `pre` plugin may hand each environment
|
|
3071
|
+
* different code. Where they disagree the last one wins, which is a cosmetic number either
|
|
3072
|
+
* way.
|
|
3073
|
+
*/
|
|
3074
|
+
const perFile = /* @__PURE__ */ new Map();
|
|
2878
3075
|
const staticSession = createStaticCompilationSession();
|
|
2879
3076
|
/**
|
|
2880
3077
|
* Indexed by file, because the only bulk operation on it is "forget this one's".
|
|
@@ -2937,26 +3134,24 @@ const bamboocss = (options = {}) => {
|
|
|
2937
3134
|
configPath,
|
|
2938
3135
|
cwd,
|
|
2939
3136
|
session: staticSession,
|
|
2940
|
-
|
|
3137
|
+
pruneCss
|
|
2941
3138
|
}), {
|
|
2942
3139
|
name: "bamboocss:compiler",
|
|
2943
3140
|
enforce: "pre",
|
|
3141
|
+
/** See the same declaration on the css plugin: one instance per build, not per environment. */
|
|
3142
|
+
sharedDuringBuild: true,
|
|
2944
3143
|
configResolved(config) {
|
|
2945
3144
|
command = config.command;
|
|
2946
3145
|
},
|
|
2947
3146
|
async buildStart() {
|
|
2948
3147
|
const environment = this.environment?.name ?? "default";
|
|
2949
|
-
if (
|
|
2950
|
-
|
|
2951
|
-
totals.folded = 0;
|
|
2952
|
-
totals.files = 0;
|
|
2953
|
-
totals.filesWithFolds = 0;
|
|
2954
|
-
totals.skipped.clear();
|
|
3148
|
+
if (staticSession.startedEnvironments.has(environment)) {
|
|
3149
|
+
perFile.clear();
|
|
2955
3150
|
survivorsByFile.clear();
|
|
2956
3151
|
recipeConfigCache.clear();
|
|
2957
3152
|
resetStaticCompilationSession(staticSession);
|
|
2958
3153
|
}
|
|
2959
|
-
|
|
3154
|
+
staticSession.startedEnvironments.add(environment);
|
|
2960
3155
|
try {
|
|
2961
3156
|
await ensureContext();
|
|
2962
3157
|
} catch (error) {
|
|
@@ -3026,8 +3221,10 @@ const bamboocss = (options = {}) => {
|
|
|
3026
3221
|
});
|
|
3027
3222
|
} catch (error) {
|
|
3028
3223
|
_bamboocss_logger.logger.caughtError("vite:transform", `Failed to compile ${filePath}`, error);
|
|
3029
|
-
|
|
3030
|
-
|
|
3224
|
+
perFile.set(filePath, {
|
|
3225
|
+
folded: 0,
|
|
3226
|
+
skipped: new Map([["compile-failed", 1]])
|
|
3227
|
+
});
|
|
3031
3228
|
addSurvivor({
|
|
3032
3229
|
file: filePath,
|
|
3033
3230
|
line: 1,
|
|
@@ -3037,10 +3234,15 @@ const bamboocss = (options = {}) => {
|
|
|
3037
3234
|
if (command === "serve") throw asError(error, `failed to compile ${filePath}`);
|
|
3038
3235
|
return null;
|
|
3039
3236
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3237
|
+
let skippedHere;
|
|
3238
|
+
for (const entry of result.skipped) {
|
|
3239
|
+
skippedHere ??= /* @__PURE__ */ new Map();
|
|
3240
|
+
skippedHere.set(entry.reason, (skippedHere.get(entry.reason) ?? 0) + 1);
|
|
3241
|
+
}
|
|
3242
|
+
perFile.set(filePath, {
|
|
3243
|
+
folded: result.folded.length,
|
|
3244
|
+
skipped: skippedHere
|
|
3245
|
+
});
|
|
3044
3246
|
if (result.folded.some((entry) => entry.kind === "class" || entry.kind === "slots")) staticSession.transformedFiles.add((0, node_path.resolve)(filePath));
|
|
3045
3247
|
for (const entry of result.folded) for (const className of entry.classNames) staticSession.markClassUsed(className);
|
|
3046
3248
|
for (const entry of result.skipped) {
|
|
@@ -3067,7 +3269,15 @@ const bamboocss = (options = {}) => {
|
|
|
3067
3269
|
buildEnd() {
|
|
3068
3270
|
const survivors = allSurvivors();
|
|
3069
3271
|
if (survivors.length) throw createSurvivorError(survivors);
|
|
3070
|
-
|
|
3272
|
+
const lost = [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(bare(className)));
|
|
3273
|
+
if (lost.length) {
|
|
3274
|
+
const environment = this.environment?.name ?? "default";
|
|
3275
|
+
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}`), {
|
|
3276
|
+
unit: "class",
|
|
3277
|
+
separator: "\n"
|
|
3278
|
+
})}\n\nThe stylesheet is finalized by the environment that imports it, so pruning it is only safe once every environment has been compiled. This build did not say how many there would be: it called \`builder.build(environment)\` directly. Run it through \`vite build\`, call \`builder.buildApp()\`, or set \`builder: {}\` in the Vite config so the environments are known before the first one builds. \`bamboocss({ pruneCss: false })\` also turns pruning off entirely.`);
|
|
3279
|
+
}
|
|
3280
|
+
if (typeof this.getModuleInfo === "function" && !remainingEnvironments(staticSession).length) {
|
|
3071
3281
|
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.`);
|
|
3072
3282
|
const outsideExtraction = [...staticSession.transformedFiles].filter((file) => !staticSession.extractedFiles.has(file));
|
|
3073
3283
|
if (outsideExtraction.length) throw new Error(`bamboocss: ${outsideExtraction.length} statically compiled module(s) are outside the CSS extraction graph:\n\n${(0, _bamboocss_shared.truncateList)(outsideExtraction.map((file) => ` ${file}`), {
|
|
@@ -3076,12 +3286,21 @@ const bamboocss = (options = {}) => {
|
|
|
3076
3286
|
})}\n\nAdd them to \`include\` in bamboo.config, or no CSS rule can back their emitted classes.`);
|
|
3077
3287
|
}
|
|
3078
3288
|
if (!reportSummary) return;
|
|
3079
|
-
|
|
3080
|
-
|
|
3289
|
+
if (command === "build" && remainingEnvironments(staticSession).length) return;
|
|
3290
|
+
let folded = 0;
|
|
3291
|
+
let filesWithFolds = 0;
|
|
3292
|
+
const skipped = /* @__PURE__ */ new Map();
|
|
3293
|
+
for (const entry of perFile.values()) {
|
|
3294
|
+
folded += entry.folded;
|
|
3295
|
+
if (entry.folded) filesWithFolds++;
|
|
3296
|
+
for (const [reason, count] of entry.skipped ?? []) skipped.set(reason, (skipped.get(reason) ?? 0) + count);
|
|
3297
|
+
}
|
|
3298
|
+
const declined = Array.from(skipped.values()).reduce((sum, count) => sum + count, 0);
|
|
3299
|
+
const total = folded + declined;
|
|
3081
3300
|
if (!total) return;
|
|
3082
|
-
const share = Math.round(
|
|
3083
|
-
const reasons = Array.from(
|
|
3084
|
-
_bamboocss_logger.logger.info("vite:transform", `Compiled ${
|
|
3301
|
+
const share = Math.round(folded / total * 100);
|
|
3302
|
+
const reasons = Array.from(skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
|
|
3303
|
+
_bamboocss_logger.logger.info("vite:transform", `Compiled ${folded}/${total} (${share}%) across ${filesWithFolds}/${perFile.size} files` + (reasons ? ` — declined: ${reasons}` : ""));
|
|
3085
3304
|
}
|
|
3086
3305
|
}];
|
|
3087
3306
|
};
|
package/dist/index.d.cts
CHANGED
|
@@ -37,21 +37,32 @@ interface BambooVitePluginOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
maxRecipeStates?: number;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Remove rules for atoms no compiled module can emit. Builds only; dev never prunes.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* a
|
|
42
|
+
* Off ships the whole extracted stylesheet: every rule the source graph produced, including
|
|
43
|
+
* ones nothing reaches. Larger, and never wrong *by pruning* — it also stands down the
|
|
44
|
+
* assertion that every compiled class has a rule, since that check exists to catch this pass
|
|
45
|
+
* removing too much. So this is a true escape hatch: it cannot fail a build over reachability.
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* The pruned sheet is also renamed to a hash of its own bytes, and that is not a separate
|
|
48
|
+
* setting because it cannot safely be one. Rollup and Rolldown expand `[hash]` before
|
|
49
|
+
* `generateBundle`, where pruning has to run, so the name Vite assigned describes the sheet
|
|
50
|
+
* as it was *before* pruning. Leaving that name on pruned bytes is how a stale stylesheet
|
|
51
|
+
* outlives a deploy — a change to reachability alone, which is what upgrading Bamboo is,
|
|
52
|
+
* leaves identical source CSS under an identical name with different content, and a CDN
|
|
53
|
+
* holding that key keeps serving the old one. So the bytes and the name move together or
|
|
54
|
+
* neither does.
|
|
55
|
+
*
|
|
56
|
+
* Reach for this if something downstream derives an artifact from the stylesheet's *content*
|
|
57
|
+
* during `generateBundle` before Bamboo runs — subresource integrity is the clear case, since
|
|
58
|
+
* an `integrity` attribute is a digest of the bytes and no amount of reference rewriting can
|
|
59
|
+
* carry it across an edit — or to rule pruning out while diagnosing a missing rule. Where the
|
|
60
|
+
* consumer can be moved after Bamboo instead (`order: 'post'`, `writeBundle`, `closeBundle`),
|
|
61
|
+
* do that and keep the pruning.
|
|
51
62
|
*
|
|
52
63
|
* @default true
|
|
53
64
|
*/
|
|
54
|
-
|
|
65
|
+
pruneCss?: boolean;
|
|
55
66
|
}
|
|
56
67
|
/**
|
|
57
68
|
* Vite integration for Bamboo CSS.
|
package/dist/index.d.mts
CHANGED
|
@@ -37,21 +37,32 @@ interface BambooVitePluginOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
maxRecipeStates?: number;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Remove rules for atoms no compiled module can emit. Builds only; dev never prunes.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* a
|
|
42
|
+
* Off ships the whole extracted stylesheet: every rule the source graph produced, including
|
|
43
|
+
* ones nothing reaches. Larger, and never wrong *by pruning* — it also stands down the
|
|
44
|
+
* assertion that every compiled class has a rule, since that check exists to catch this pass
|
|
45
|
+
* removing too much. So this is a true escape hatch: it cannot fail a build over reachability.
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* The pruned sheet is also renamed to a hash of its own bytes, and that is not a separate
|
|
48
|
+
* setting because it cannot safely be one. Rollup and Rolldown expand `[hash]` before
|
|
49
|
+
* `generateBundle`, where pruning has to run, so the name Vite assigned describes the sheet
|
|
50
|
+
* as it was *before* pruning. Leaving that name on pruned bytes is how a stale stylesheet
|
|
51
|
+
* outlives a deploy — a change to reachability alone, which is what upgrading Bamboo is,
|
|
52
|
+
* leaves identical source CSS under an identical name with different content, and a CDN
|
|
53
|
+
* holding that key keeps serving the old one. So the bytes and the name move together or
|
|
54
|
+
* neither does.
|
|
55
|
+
*
|
|
56
|
+
* Reach for this if something downstream derives an artifact from the stylesheet's *content*
|
|
57
|
+
* during `generateBundle` before Bamboo runs — subresource integrity is the clear case, since
|
|
58
|
+
* an `integrity` attribute is a digest of the bytes and no amount of reference rewriting can
|
|
59
|
+
* carry it across an edit — or to rule pruning out while diagnosing a missing rule. Where the
|
|
60
|
+
* consumer can be moved after Bamboo instead (`order: 'post'`, `writeBundle`, `closeBundle`),
|
|
61
|
+
* do that and keep the pruning.
|
|
51
62
|
*
|
|
52
63
|
* @default true
|
|
53
64
|
*/
|
|
54
|
-
|
|
65
|
+
pruneCss?: boolean;
|
|
55
66
|
}
|
|
56
67
|
/**
|
|
57
68
|
* Vite integration for Bamboo CSS.
|
package/dist/index.mjs
CHANGED
|
@@ -64,6 +64,7 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
|
|
|
64
64
|
if (!className || !prunable.has(className) || used.has(className)) return;
|
|
65
65
|
candidate.remove();
|
|
66
66
|
removedAny = true;
|
|
67
|
+
session.prunedClasses.add(className);
|
|
67
68
|
});
|
|
68
69
|
}).processSync(rule.selector);
|
|
69
70
|
} catch {
|
|
@@ -124,6 +125,56 @@ const pruneStaticCss = (css, session, { prune = true } = {}) => {
|
|
|
124
125
|
return root.toString();
|
|
125
126
|
};
|
|
126
127
|
//#endregion
|
|
128
|
+
//#region src/static-session.ts
|
|
129
|
+
const createStaticCompilationSession = () => {
|
|
130
|
+
const session = {
|
|
131
|
+
utilityLayer: "utilities",
|
|
132
|
+
sourcemap: false,
|
|
133
|
+
cssLoaded: false,
|
|
134
|
+
transformedFiles: /* @__PURE__ */ new Set(),
|
|
135
|
+
extractedFiles: /* @__PURE__ */ new Set(),
|
|
136
|
+
prunableClasses: /* @__PURE__ */ new Set(),
|
|
137
|
+
viewTransitionClasses: /* @__PURE__ */ new Set(),
|
|
138
|
+
usedClasses: /* @__PURE__ */ new Set(),
|
|
139
|
+
expectedEnvironments: void 0,
|
|
140
|
+
startedEnvironments: /* @__PURE__ */ new Set(),
|
|
141
|
+
prunedClasses: /* @__PURE__ */ new Set(),
|
|
142
|
+
markClassUsed(className) {
|
|
143
|
+
for (const token of className.split(" ")) {
|
|
144
|
+
if (!token) continue;
|
|
145
|
+
session.usedClasses.add(token.includes("\\") ? token : esc(token));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return session;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Environments this run intends to build that have not been compiled yet.
|
|
153
|
+
*
|
|
154
|
+
* Empty means everything the run will contribute has been contributed, which is the condition
|
|
155
|
+
* every whole-run judgement here waits for: pruning the stylesheet against reachability, and
|
|
156
|
+
* the two guards that ask whether the compiled modules and the extraction graph agree. Each of
|
|
157
|
+
* those is false about a build in progress and true only about a finished one.
|
|
158
|
+
*
|
|
159
|
+
* Empty is also the answer for a single-environment build, where nothing announced an
|
|
160
|
+
* environment list because there is only ever one — so that path is unchanged.
|
|
161
|
+
*
|
|
162
|
+
* An environment a run declares and then never builds leaves this permanently non-empty, and
|
|
163
|
+
* those judgements are skipped for the run. Every one of them errs towards shipping more CSS
|
|
164
|
+
* or asserting less, so that is the safe direction to be wrong in.
|
|
165
|
+
*/
|
|
166
|
+
const remainingEnvironments = (session) => [...session.expectedEnvironments ?? []].filter((name) => !session.startedEnvironments.has(name));
|
|
167
|
+
const resetStaticCompilationSession = (session) => {
|
|
168
|
+
session.cssLoaded = false;
|
|
169
|
+
session.transformedFiles.clear();
|
|
170
|
+
session.extractedFiles.clear();
|
|
171
|
+
session.prunableClasses.clear();
|
|
172
|
+
session.viewTransitionClasses.clear();
|
|
173
|
+
session.usedClasses.clear();
|
|
174
|
+
session.startedEnvironments.clear();
|
|
175
|
+
session.prunedClasses.clear();
|
|
176
|
+
};
|
|
177
|
+
//#endregion
|
|
127
178
|
//#region src/css.ts
|
|
128
179
|
/**
|
|
129
180
|
* What a project imports to get the stylesheet.
|
|
@@ -225,32 +276,37 @@ const replaceAssetReferences = (bundle, previous, next, sourcemap) => {
|
|
|
225
276
|
*/
|
|
226
277
|
const carriesGeneratedCss = (output) => output.type === "asset" && output.fileName.endsWith(".css");
|
|
227
278
|
/**
|
|
228
|
-
* Prune
|
|
279
|
+
* Prune compiler-owned CSS, then give any sheet whose bytes changed a hash of those bytes.
|
|
229
280
|
*
|
|
230
281
|
* Rollup has already expanded `[hash]` when `generateBundle` runs. Mutating only `source`
|
|
231
282
|
* would therefore leave two different reachable subsets under one CDN key. The extra final
|
|
232
283
|
* hash is not cosmetic: it makes late graph reachability cache-safe.
|
|
284
|
+
*
|
|
285
|
+
* Renaming is therefore not a choice this takes. Pruned bytes under the unpruned sheet's name
|
|
286
|
+
* is the one outcome that must never be reachable, and a sheet nothing was removed from keeps
|
|
287
|
+
* its name because its bytes are unchanged — so "rename" is a consequence of "the bytes moved",
|
|
288
|
+
* not a second option. `prune` is the only knob.
|
|
233
289
|
*/
|
|
234
290
|
const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
235
|
-
const {
|
|
291
|
+
const { prune = true, sourcemap = session.sourcemap } = options;
|
|
292
|
+
/** Assets in this bundle that carry the generated stylesheet, pruned or not. */
|
|
293
|
+
let sheets = 0;
|
|
236
294
|
for (const output of Object.values(bundle)) {
|
|
237
295
|
if (!carriesGeneratedCss(output)) continue;
|
|
238
296
|
const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString();
|
|
239
297
|
if (!source.includes("--made-with-bamboo")) continue;
|
|
298
|
+
sheets++;
|
|
299
|
+
if (!prune) continue;
|
|
240
300
|
const optimized = pruneStaticCss(source, session);
|
|
241
301
|
output.source = optimized;
|
|
242
302
|
if (optimized === source) continue;
|
|
243
|
-
if (!rename) {
|
|
244
|
-
output.source = source;
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
303
|
const nextName = output.fileName.replace(/\.css$/, `.b-${toHash(optimized)}.css`);
|
|
248
|
-
if (nextName === output.fileName) continue;
|
|
249
304
|
if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
|
|
250
305
|
const previous = output.fileName;
|
|
251
306
|
output.fileName = nextName;
|
|
252
|
-
replaceAssetReferences(bundle, previous, nextName,
|
|
307
|
+
replaceAssetReferences(bundle, previous, nextName, sourcemap);
|
|
253
308
|
}
|
|
309
|
+
return { sheets };
|
|
254
310
|
};
|
|
255
311
|
/**
|
|
256
312
|
* Serve bamboo's stylesheet as a virtual module, in dev and in build.
|
|
@@ -266,7 +322,7 @@ const optimizeStaticCssAssets = (bundle, session, options = {}) => {
|
|
|
266
322
|
* process just wrote, which is a race on any watch rebuild.
|
|
267
323
|
*/
|
|
268
324
|
const bamboocssCss = (options) => {
|
|
269
|
-
const { configPath, cwd, session,
|
|
325
|
+
const { configPath, cwd, session, pruneCss = true } = options;
|
|
270
326
|
/**
|
|
271
327
|
* Environments whose `load` served the virtual stylesheet.
|
|
272
328
|
*
|
|
@@ -326,9 +382,32 @@ const bamboocssCss = (options) => {
|
|
|
326
382
|
};
|
|
327
383
|
return {
|
|
328
384
|
name: "bamboocss:css",
|
|
385
|
+
/**
|
|
386
|
+
* One instance for every environment of a build, rather than one per environment.
|
|
387
|
+
*
|
|
388
|
+
* Vite re-reads the config file once per environment, so a project that lists this plugin
|
|
389
|
+
* in `vite.config.ts` — every project — got a *fresh* instance per environment, each with
|
|
390
|
+
* its own compilation session, context and ts-morph project. Nothing an environment
|
|
391
|
+
* established could then be seen by the next one, which is the premise the reachability
|
|
392
|
+
* accounting below is built on, and it also meant the whole config load and extraction
|
|
393
|
+
* happened once per environment.
|
|
394
|
+
*/
|
|
395
|
+
sharedDuringBuild: true,
|
|
329
396
|
configResolved(config) {
|
|
330
397
|
command = config.command;
|
|
331
398
|
session.sourcemap = config.build.sourcemap;
|
|
399
|
+
if (config.builder && config.environments) session.expectedEnvironments = new Set(Object.keys(config.environments));
|
|
400
|
+
},
|
|
401
|
+
/**
|
|
402
|
+
* The definitive environment list, for a run that reaches `builder.buildApp()` without
|
|
403
|
+
* configuring `builder` — the shape `vite build` itself takes, where exactly one
|
|
404
|
+
* environment is set up and pruning is therefore safe.
|
|
405
|
+
*/
|
|
406
|
+
buildApp: {
|
|
407
|
+
order: "pre",
|
|
408
|
+
async handler(builder) {
|
|
409
|
+
session.expectedEnvironments = new Set(Object.keys(builder.environments));
|
|
410
|
+
}
|
|
332
411
|
},
|
|
333
412
|
resolveId(id) {
|
|
334
413
|
const query = queryOf(id);
|
|
@@ -368,8 +447,34 @@ const bamboocssCss = (options) => {
|
|
|
368
447
|
generateBundle: {
|
|
369
448
|
order: "post",
|
|
370
449
|
handler(_, bundle) {
|
|
371
|
-
|
|
372
|
-
|
|
450
|
+
const environment = this.environment;
|
|
451
|
+
/**
|
|
452
|
+
* Every environment of this run has already had its modules compiled.
|
|
453
|
+
*
|
|
454
|
+
* Reachability is what pruning removes rules against, and it is only complete once
|
|
455
|
+
* nothing is left to contribute to it. The stylesheet is emitted and finalized by the
|
|
456
|
+
* environment that *imports* it, which in an SSR app is the client — and the client
|
|
457
|
+
* builds first, before the server environment has transformed a single module. Pruning
|
|
458
|
+
* there deletes every rule for a class only the server graph reaches, and the pages
|
|
459
|
+
* link the pruned copy: one project lost 39% of its atoms that way, presenting as
|
|
460
|
+
* rarely-used classes such as `md:{display:inline-block}` silently not applying.
|
|
461
|
+
*
|
|
462
|
+
* So the full extracted stylesheet ships instead, which is what `pruneCss: false` asks
|
|
463
|
+
* for by hand. Being the last environment is not the common case — frameworks build the
|
|
464
|
+
* client first — but it is the only one where the answer is knowable, and a framework
|
|
465
|
+
* that builds its server bundle first does get pruned output.
|
|
466
|
+
*/
|
|
467
|
+
const pending = remainingEnvironments(session);
|
|
468
|
+
const { sheets } = optimizeStaticCssAssets(bundle, session, {
|
|
469
|
+
prune: pruneCss && pending.length === 0,
|
|
470
|
+
sourcemap: environment?.config?.build?.sourcemap
|
|
471
|
+
});
|
|
472
|
+
if (sheets && !pruneCss) logger.info("vite", "Reachability pruning is off (`pruneCss: false`). The full extracted stylesheet ships.");
|
|
473
|
+
else if (sheets && pending.length) logger.info("vite", `Reachability pruning skipped: the stylesheet is emitted by the ${JSON.stringify(environment?.name ?? "default")} environment, and ${truncateList(pending, {
|
|
474
|
+
unit: "environment",
|
|
475
|
+
separator: ", "
|
|
476
|
+
})} ${pending.length === 1 ? "has" : "have"} not been compiled in this run. The full extracted stylesheet ships — nothing is missing from it.`);
|
|
477
|
+
if (!servedEnvironments.has(environment?.name ?? "default")) return;
|
|
373
478
|
if (!session.transformedFiles.size) return;
|
|
374
479
|
if (!Object.values(bundle).some((output) => {
|
|
375
480
|
if (!carriesGeneratedCss(output)) return false;
|
|
@@ -382,6 +487,78 @@ const bamboocssCss = (options) => {
|
|
|
382
487
|
//#endregion
|
|
383
488
|
//#region src/fold-analysis.ts
|
|
384
489
|
/**
|
|
490
|
+
* Nodes that *compose* a value out of their children rather than computing one.
|
|
491
|
+
*
|
|
492
|
+
* The boundary of the walk below, and the whole of its precision. Climbing through these
|
|
493
|
+
* keeps a value's provenance: `'red.300'` inside `{ color: 'red.300' }` inside a default is
|
|
494
|
+
* still the default's. Anything else — a call, a function body, a JSX element — produces its
|
|
495
|
+
* value by being evaluated, so what is written inside it is an argument to that evaluation
|
|
496
|
+
* and not the enclosing default's value.
|
|
497
|
+
*
|
|
498
|
+
* Without the boundary, `({ cls = css({ color: 'red.300' }) }) => cls` is rejected: the call's
|
|
499
|
+
* own literal argument is syntactically inside a default, so an unbounded walk calls it one.
|
|
500
|
+
* That is correct code, and rejecting it fails the build.
|
|
501
|
+
*/
|
|
502
|
+
const composesValue = (node) => Node.isObjectLiteralExpression(node) || Node.isArrayLiteralExpression(node) || Node.isPropertyAssignment(node) || Node.isShorthandPropertyAssignment(node) || Node.isSpreadAssignment(node) || Node.isSpreadElement(node) || Node.isAsExpression(node) || Node.isParenthesizedExpression(node) || Node.isNonNullExpression(node) || Node.isTypeAssertion(node) || Node.isSatisfiesExpression(node);
|
|
503
|
+
/** Is `inner` written within `outer`? Positions rather than a walk, so it is O(1). */
|
|
504
|
+
const contains = (outer, inner) => outer.getSourceFile() === inner.getSourceFile() && outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
|
|
505
|
+
/**
|
|
506
|
+
* Did this value come from the `= …` of a destructuring binding?
|
|
507
|
+
*
|
|
508
|
+
* `const { tone = 'red.300' } = source` boxes as the literal `'red.300'`: the extractor's
|
|
509
|
+
* `maybeDefinitionValue` tests for an initializer first and returns the boxed default, never
|
|
510
|
+
* reaching the branch that would read `source`. So the default is reported as the value whether
|
|
511
|
+
* or not it is the one that applies.
|
|
512
|
+
*
|
|
513
|
+
* For extraction that is merely optimistic, and deliberately so: a CLI or PostCSS build ships a
|
|
514
|
+
* runtime `css()`, where the default genuinely does apply when the caller omits the key, and it
|
|
515
|
+
* needs a rule behind it. Folding is where the same resolution turns into a wrong answer,
|
|
516
|
+
* because the call is *replaced* by that value.
|
|
517
|
+
*
|
|
518
|
+
* Stops at the first non-composing parent, so a call written inside a default keeps its own
|
|
519
|
+
* provenance, and checks that the binding element was reached through its initializer, so
|
|
520
|
+
* `{ tone = X }`'s name node is not mistaken for its default.
|
|
521
|
+
*/
|
|
522
|
+
const isBindingElementDefault = (node) => {
|
|
523
|
+
if (node && Node.isCallExpression(node)) return false;
|
|
524
|
+
let current = node;
|
|
525
|
+
while (current) {
|
|
526
|
+
const parent = current.getParent();
|
|
527
|
+
if (!parent) return false;
|
|
528
|
+
if (Node.isBindingElement(parent)) return parent.getInitializer() === current;
|
|
529
|
+
if (!composesValue(parent)) return false;
|
|
530
|
+
current = parent;
|
|
531
|
+
}
|
|
532
|
+
return false;
|
|
533
|
+
};
|
|
534
|
+
/**
|
|
535
|
+
* The same question asked of a whole box, including how it was resolved.
|
|
536
|
+
*
|
|
537
|
+
* The node a box reports is not always the one its value came from — an empty `{}` default
|
|
538
|
+
* boxes against the call rather than against the `{}` — so the resolution stack is consulted
|
|
539
|
+
* too. A binding element reaches that stack by having been resolved *through*, which is the
|
|
540
|
+
* signal the extractor itself reads when one of these is a conditional's test.
|
|
541
|
+
*
|
|
542
|
+
* Only the entries that are binding elements are examined. Walking up from every other entry
|
|
543
|
+
* was tried and never once changed a verdict across the default spellings or the sandbox's own
|
|
544
|
+
* modules, while accounting for most of the parent hops this does — the stack carries nodes
|
|
545
|
+
* that were never resolved through, including a call's own arguments, so walking from them is
|
|
546
|
+
* both the expensive half and the one that reaches conclusions it has no basis for.
|
|
547
|
+
*
|
|
548
|
+
* A binding element without an initializer carries no default to mistrust: `const { tone } =
|
|
549
|
+
* source` either resolves from `source` or does not resolve at all.
|
|
550
|
+
*/
|
|
551
|
+
const isFromBindingDefault = (node) => {
|
|
552
|
+
const own = node.getNode?.();
|
|
553
|
+
if (isBindingElementDefault(own)) return true;
|
|
554
|
+
for (const entry of node.getStack?.() ?? []) {
|
|
555
|
+
if (!Node.isBindingElement(entry) || !entry.getInitializer()) continue;
|
|
556
|
+
if (own && contains(entry, own)) continue;
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
return false;
|
|
560
|
+
};
|
|
561
|
+
/**
|
|
385
562
|
* Statically resolvable means: every box in the tree carries a known value.
|
|
386
563
|
*
|
|
387
564
|
* `unresolvable` is the extractor saying it could not evaluate a node.
|
|
@@ -395,6 +572,7 @@ const isStaticBox = (node, seen = /* @__PURE__ */ new Set()) => {
|
|
|
395
572
|
seen.add(node);
|
|
396
573
|
if (box.isUnresolvable(node) || box.isConditional(node)) return false;
|
|
397
574
|
if (!("type" in node) || node.type == null) return false;
|
|
575
|
+
if (isFromBindingDefault(node)) return false;
|
|
398
576
|
if (box.isLiteral(node) && node.value === void 0) {
|
|
399
577
|
const source = node.getNode?.();
|
|
400
578
|
return Boolean(source && Node.isIdentifier(source) && source.getText() === "undefined");
|
|
@@ -2754,40 +2932,50 @@ const createStaticStyleSetCompiler = (ctx, runtimeCss, allocateClassString = (cl
|
|
|
2754
2932
|
};
|
|
2755
2933
|
};
|
|
2756
2934
|
//#endregion
|
|
2757
|
-
//#region src/static-session.ts
|
|
2758
|
-
const createStaticCompilationSession = () => {
|
|
2759
|
-
const session = {
|
|
2760
|
-
utilityLayer: "utilities",
|
|
2761
|
-
sourcemap: false,
|
|
2762
|
-
cssLoaded: false,
|
|
2763
|
-
transformedFiles: /* @__PURE__ */ new Set(),
|
|
2764
|
-
extractedFiles: /* @__PURE__ */ new Set(),
|
|
2765
|
-
prunableClasses: /* @__PURE__ */ new Set(),
|
|
2766
|
-
viewTransitionClasses: /* @__PURE__ */ new Set(),
|
|
2767
|
-
usedClasses: /* @__PURE__ */ new Set(),
|
|
2768
|
-
markClassUsed(className) {
|
|
2769
|
-
for (const token of className.split(" ")) {
|
|
2770
|
-
if (!token) continue;
|
|
2771
|
-
session.usedClasses.add(token.includes("\\") ? token : esc(token));
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
|
-
};
|
|
2775
|
-
return session;
|
|
2776
|
-
};
|
|
2777
|
-
const resetStaticCompilationSession = (session) => {
|
|
2778
|
-
session.cssLoaded = false;
|
|
2779
|
-
session.transformedFiles.clear();
|
|
2780
|
-
session.extractedFiles.clear();
|
|
2781
|
-
session.prunableClasses.clear();
|
|
2782
|
-
session.viewTransitionClasses.clear();
|
|
2783
|
-
session.usedClasses.clear();
|
|
2784
|
-
};
|
|
2785
|
-
//#endregion
|
|
2786
2935
|
//#region src/plugin.ts
|
|
2787
2936
|
const DEFAULT_EXTENSIONS = /\.(?:[cm]?[jt]sx?)$/;
|
|
2788
2937
|
const NODE_MODULES = /node_modules/;
|
|
2938
|
+
/**
|
|
2939
|
+
* Queries that make Vite serve something other than the module's own source.
|
|
2940
|
+
*
|
|
2941
|
+
* `./theme.tsx?raw` is a module whose text is `export default "…"`, and `?url`, `?worker` and
|
|
2942
|
+
* `?sharedworker` are wrappers of the same kind. The query has to be stripped before the
|
|
2943
|
+
* extension is tested — otherwise nothing matches `.tsx` — and stripping it is what made these
|
|
2944
|
+
* look like the file itself. The transform then handed the wrapper's text to ts-morph *under
|
|
2945
|
+
* the real file's path*, overwriting the parsed module every fold reads for that path.
|
|
2946
|
+
*
|
|
2947
|
+
* That is not theoretical: a module folding `css(shared)` against a sibling the entry also
|
|
2948
|
+
* imported as `?raw` failed the build with "1 call(s) could not be compiled" — the compiler
|
|
2949
|
+
* had read `export default "…"` and found no `shared` to resolve. The advice it prints, to
|
|
2950
|
+
* make the value statically analyzable, is unfollowable, because the source already was.
|
|
2951
|
+
*
|
|
2952
|
+
* Whether it bites depends on which of the two ids Rollup transforms last, so the same project
|
|
2953
|
+
* can build and then stop building because an import moved.
|
|
2954
|
+
*
|
|
2955
|
+
* A deny list rather than an allow list of benign queries: dev ids carry `?t=` after an edit
|
|
2956
|
+
* and `?import` when a dynamic import is rewritten, and rejecting an unrecognised one of those
|
|
2957
|
+
* would silently stop folding a module rather than loudly refuse it.
|
|
2958
|
+
*
|
|
2959
|
+
* Exactly these four, matching Vite's own `SPECIAL_QUERY_RE`. The list was drafted wider —
|
|
2960
|
+
* `?inline`, `?no-inline`, `?worklet`, `?init` — and every one of those was wrong: Vite has no
|
|
2961
|
+
* `worklet` query at all, `?init` is `.wasm` only and that extension is already rejected below,
|
|
2962
|
+
* and `inline`/`no-inline` merely pick base64-versus-file for something that *already* matched
|
|
2963
|
+
* `raw`/`url`, so `./a.tsx?inline` is served as the module's own source. Rejecting an id that
|
|
2964
|
+
* carries real source is the expensive direction: the transform declines, its atoms never reach
|
|
2965
|
+
* the reachability set, pruning removes their rules, and the runtime still returns the class
|
|
2966
|
+
* names — unstyled elements, no error. Only names verified against Vite belong here.
|
|
2967
|
+
*
|
|
2968
|
+
* Note `?worker_file`, which is how dev serves a worker's *real* source, is deliberately absent
|
|
2969
|
+
* and must stay absent. It contains "worker" and is the obvious next entry; adding it would
|
|
2970
|
+
* stop folding every worker module in dev, silently, by the mechanism above.
|
|
2971
|
+
*
|
|
2972
|
+
* Tested against the whole id rather than a split-off query, so it cannot disagree with
|
|
2973
|
+
* `queryOf` in `css.ts` about where the query starts.
|
|
2974
|
+
*/
|
|
2975
|
+
const WRAPPED_MODULE_QUERY = /[?&](?:raw|url|worker|sharedworker)(?:&|=|$)/;
|
|
2789
2976
|
const shouldTransform = (id) => {
|
|
2790
2977
|
if (id.startsWith("\0")) return false;
|
|
2978
|
+
if (WRAPPED_MODULE_QUERY.test(id)) return false;
|
|
2791
2979
|
const [filePath] = id.split("?");
|
|
2792
2980
|
if (!filePath) return false;
|
|
2793
2981
|
if (NODE_MODULES.test(filePath)) return false;
|
|
@@ -2834,17 +3022,26 @@ const formatSkipped = (id, skipped) => {
|
|
|
2834
3022
|
* with no matching rule.
|
|
2835
3023
|
*/
|
|
2836
3024
|
const bamboocss = (options = {}) => {
|
|
2837
|
-
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates,
|
|
3025
|
+
const { configPath, cwd, reportSkipped = false, reportSummary = true, maxRecipeStates, pruneCss = true } = options;
|
|
2838
3026
|
if (maxRecipeStates !== void 0 && (!Number.isSafeInteger(maxRecipeStates) || maxRecipeStates < 1)) throw new Error("bamboocss: `maxRecipeStates` must be a positive safe integer.");
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
3027
|
+
if ("renameCssAsset" in options) throw new Error("bamboocss: `renameCssAsset` has been replaced by `pruneCss`. Use `pruneCss: false` for what `renameCssAsset: false` did — it always disabled the pruning as well, since pruned bytes under the unpruned sheet's name is what lets a CDN serve a stale stylesheet. The new name says which of the two it is really about.");
|
|
3028
|
+
/**
|
|
3029
|
+
* What each file's transform found, for the summary. Keyed by file rather than summed as it
|
|
3030
|
+
* goes, because a build has more than one environment and they share most of their modules.
|
|
3031
|
+
*
|
|
3032
|
+
* Running totals double-counted every shared module once per environment — a two-environment
|
|
3033
|
+
* build of one shared file and one entry each reported "2/2 across 2/4 files" for three
|
|
3034
|
+
* source modules. Coverage is a property of the source, not of how many times a bundler
|
|
3035
|
+
* handed the same file over. It also grew without bound in dev, where every HMR
|
|
3036
|
+
* re-transform of a file counted as another file.
|
|
3037
|
+
*
|
|
3038
|
+
* A second pass over a file replaces its entry rather than adding to it. Both environments
|
|
3039
|
+
* are assumed to compute the same answer for the same module — true of this compiler, though
|
|
3040
|
+
* not something the plugin can enforce, since another `pre` plugin may hand each environment
|
|
3041
|
+
* different code. Where they disagree the last one wins, which is a cosmetic number either
|
|
3042
|
+
* way.
|
|
3043
|
+
*/
|
|
3044
|
+
const perFile = /* @__PURE__ */ new Map();
|
|
2848
3045
|
const staticSession = createStaticCompilationSession();
|
|
2849
3046
|
/**
|
|
2850
3047
|
* Indexed by file, because the only bulk operation on it is "forget this one's".
|
|
@@ -2907,26 +3104,24 @@ const bamboocss = (options = {}) => {
|
|
|
2907
3104
|
configPath,
|
|
2908
3105
|
cwd,
|
|
2909
3106
|
session: staticSession,
|
|
2910
|
-
|
|
3107
|
+
pruneCss
|
|
2911
3108
|
}), {
|
|
2912
3109
|
name: "bamboocss:compiler",
|
|
2913
3110
|
enforce: "pre",
|
|
3111
|
+
/** See the same declaration on the css plugin: one instance per build, not per environment. */
|
|
3112
|
+
sharedDuringBuild: true,
|
|
2914
3113
|
configResolved(config) {
|
|
2915
3114
|
command = config.command;
|
|
2916
3115
|
},
|
|
2917
3116
|
async buildStart() {
|
|
2918
3117
|
const environment = this.environment?.name ?? "default";
|
|
2919
|
-
if (
|
|
2920
|
-
|
|
2921
|
-
totals.folded = 0;
|
|
2922
|
-
totals.files = 0;
|
|
2923
|
-
totals.filesWithFolds = 0;
|
|
2924
|
-
totals.skipped.clear();
|
|
3118
|
+
if (staticSession.startedEnvironments.has(environment)) {
|
|
3119
|
+
perFile.clear();
|
|
2925
3120
|
survivorsByFile.clear();
|
|
2926
3121
|
recipeConfigCache.clear();
|
|
2927
3122
|
resetStaticCompilationSession(staticSession);
|
|
2928
3123
|
}
|
|
2929
|
-
|
|
3124
|
+
staticSession.startedEnvironments.add(environment);
|
|
2930
3125
|
try {
|
|
2931
3126
|
await ensureContext();
|
|
2932
3127
|
} catch (error) {
|
|
@@ -2996,8 +3191,10 @@ const bamboocss = (options = {}) => {
|
|
|
2996
3191
|
});
|
|
2997
3192
|
} catch (error) {
|
|
2998
3193
|
logger.caughtError("vite:transform", `Failed to compile ${filePath}`, error);
|
|
2999
|
-
|
|
3000
|
-
|
|
3194
|
+
perFile.set(filePath, {
|
|
3195
|
+
folded: 0,
|
|
3196
|
+
skipped: new Map([["compile-failed", 1]])
|
|
3197
|
+
});
|
|
3001
3198
|
addSurvivor({
|
|
3002
3199
|
file: filePath,
|
|
3003
3200
|
line: 1,
|
|
@@ -3007,10 +3204,15 @@ const bamboocss = (options = {}) => {
|
|
|
3007
3204
|
if (command === "serve") throw asError(error, `failed to compile ${filePath}`);
|
|
3008
3205
|
return null;
|
|
3009
3206
|
}
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3207
|
+
let skippedHere;
|
|
3208
|
+
for (const entry of result.skipped) {
|
|
3209
|
+
skippedHere ??= /* @__PURE__ */ new Map();
|
|
3210
|
+
skippedHere.set(entry.reason, (skippedHere.get(entry.reason) ?? 0) + 1);
|
|
3211
|
+
}
|
|
3212
|
+
perFile.set(filePath, {
|
|
3213
|
+
folded: result.folded.length,
|
|
3214
|
+
skipped: skippedHere
|
|
3215
|
+
});
|
|
3014
3216
|
if (result.folded.some((entry) => entry.kind === "class" || entry.kind === "slots")) staticSession.transformedFiles.add(resolve(filePath));
|
|
3015
3217
|
for (const entry of result.folded) for (const className of entry.classNames) staticSession.markClassUsed(className);
|
|
3016
3218
|
for (const entry of result.skipped) {
|
|
@@ -3037,7 +3239,15 @@ const bamboocss = (options = {}) => {
|
|
|
3037
3239
|
buildEnd() {
|
|
3038
3240
|
const survivors = allSurvivors();
|
|
3039
3241
|
if (survivors.length) throw createSurvivorError(survivors);
|
|
3040
|
-
|
|
3242
|
+
const lost = [...staticSession.usedClasses].filter((className) => staticSession.prunedClasses.has(bare(className)));
|
|
3243
|
+
if (lost.length) {
|
|
3244
|
+
const environment = this.environment?.name ?? "default";
|
|
3245
|
+
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}`), {
|
|
3246
|
+
unit: "class",
|
|
3247
|
+
separator: "\n"
|
|
3248
|
+
})}\n\nThe stylesheet is finalized by the environment that imports it, so pruning it is only safe once every environment has been compiled. This build did not say how many there would be: it called \`builder.build(environment)\` directly. Run it through \`vite build\`, call \`builder.buildApp()\`, or set \`builder: {}\` in the Vite config so the environments are known before the first one builds. \`bamboocss({ pruneCss: false })\` also turns pruning off entirely.`);
|
|
3249
|
+
}
|
|
3250
|
+
if (typeof this.getModuleInfo === "function" && !remainingEnvironments(staticSession).length) {
|
|
3041
3251
|
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.`);
|
|
3042
3252
|
const outsideExtraction = [...staticSession.transformedFiles].filter((file) => !staticSession.extractedFiles.has(file));
|
|
3043
3253
|
if (outsideExtraction.length) throw new Error(`bamboocss: ${outsideExtraction.length} statically compiled module(s) are outside the CSS extraction graph:\n\n${truncateList(outsideExtraction.map((file) => ` ${file}`), {
|
|
@@ -3046,12 +3256,21 @@ const bamboocss = (options = {}) => {
|
|
|
3046
3256
|
})}\n\nAdd them to \`include\` in bamboo.config, or no CSS rule can back their emitted classes.`);
|
|
3047
3257
|
}
|
|
3048
3258
|
if (!reportSummary) return;
|
|
3049
|
-
|
|
3050
|
-
|
|
3259
|
+
if (command === "build" && remainingEnvironments(staticSession).length) return;
|
|
3260
|
+
let folded = 0;
|
|
3261
|
+
let filesWithFolds = 0;
|
|
3262
|
+
const skipped = /* @__PURE__ */ new Map();
|
|
3263
|
+
for (const entry of perFile.values()) {
|
|
3264
|
+
folded += entry.folded;
|
|
3265
|
+
if (entry.folded) filesWithFolds++;
|
|
3266
|
+
for (const [reason, count] of entry.skipped ?? []) skipped.set(reason, (skipped.get(reason) ?? 0) + count);
|
|
3267
|
+
}
|
|
3268
|
+
const declined = Array.from(skipped.values()).reduce((sum, count) => sum + count, 0);
|
|
3269
|
+
const total = folded + declined;
|
|
3051
3270
|
if (!total) return;
|
|
3052
|
-
const share = Math.round(
|
|
3053
|
-
const reasons = Array.from(
|
|
3054
|
-
logger.info("vite:transform", `Compiled ${
|
|
3271
|
+
const share = Math.round(folded / total * 100);
|
|
3272
|
+
const reasons = Array.from(skipped.entries()).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason}=${count}`).join(" ");
|
|
3273
|
+
logger.info("vite:transform", `Compiled ${folded}/${total} (${share}%) across ${filesWithFolds}/${perFile.size} files` + (reasons ? ` — declined: ${reasons}` : ""));
|
|
3055
3274
|
}
|
|
3056
3275
|
}];
|
|
3057
3276
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/vite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.0",
|
|
4
4
|
"description": "Vite integration for Bamboo CSS",
|
|
5
5
|
"homepage": "https://bamboocss.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,18 +40,18 @@
|
|
|
40
40
|
"postcss": "8.5.26",
|
|
41
41
|
"postcss-selector-parser": "7.1.5",
|
|
42
42
|
"ts-morph": "28.0.0",
|
|
43
|
-
"@bamboocss/config": "1.
|
|
44
|
-
"@bamboocss/core": "1.
|
|
45
|
-
"@bamboocss/
|
|
46
|
-
"@bamboocss/
|
|
47
|
-
"@bamboocss/
|
|
48
|
-
"@bamboocss/shared": "1.
|
|
49
|
-
"@bamboocss/types": "1.
|
|
43
|
+
"@bamboocss/config": "1.38.0",
|
|
44
|
+
"@bamboocss/core": "1.38.0",
|
|
45
|
+
"@bamboocss/logger": "1.38.0",
|
|
46
|
+
"@bamboocss/node": "1.38.0",
|
|
47
|
+
"@bamboocss/extractor": "1.38.0",
|
|
48
|
+
"@bamboocss/shared": "1.38.0",
|
|
49
|
+
"@bamboocss/types": "1.38.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@jridgewell/trace-mapping": "^0.3.31",
|
|
53
53
|
"vite": "7.2.6",
|
|
54
|
-
"@bamboocss/fixture": "1.
|
|
54
|
+
"@bamboocss/fixture": "1.38.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vite": ">=5"
|