@bamboocss/vite 1.53.1 → 1.54.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,9 @@
1
1
  import { t as bare } from "./class-name.mjs";
2
+ import { basename, join } from "node:path";
2
3
  import { toHash, truncateList } from "@bamboocss/shared";
4
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
5
  import remapping from "@ampproject/remapping";
6
+ import { GenMapping, addMapping, toEncodedMap } from "@jridgewell/gen-mapping";
4
7
  import MagicString from "magic-string";
5
8
  import postcss from "postcss";
6
9
  import selectorParser from "postcss-selector-parser";
@@ -170,43 +173,431 @@ const generatedCssSource = (output) => {
170
173
  const source = typeof output.source === "string" ? output.source : Buffer.from(output.source).toString();
171
174
  return source.includes("--made-with-bamboo") ? source : void 0;
172
175
  };
173
- /** Whether this bundle replaces a previously generated Bamboo stylesheet. */
174
- const containsGeneratedCssAsset = (bundle) => Object.values(bundle).some((output) => generatedCssSource(output) !== void 0);
175
176
  /**
176
- * Prune compiler-owned CSS, then give any sheet whose bytes changed a hash of those bytes.
177
- *
178
- * Rollup has already expanded `[hash]` when `generateBundle` runs. Mutating only `source`
179
- * would therefore leave two different reachable subsets under one CDN key. The extra final
180
- * hash is not cosmetic: it makes late graph reachability cache-safe.
181
- *
182
- * Renaming is therefore not a choice this takes. Pruned bytes under the unpruned sheet's name
183
- * is the one outcome that must never be reachable, and a sheet nothing was removed from keeps
184
- * its name because its bytes are unchanged — so "rename" is a consequence of "the bytes moved",
185
- * not a second option. `prune` is the only knob.
177
+ * Whether this bundle replaces a previously generated Bamboo stylesheet or, given the set of
178
+ * assets a pass already handled, whether any generated sheet is still waiting for one.
186
179
  */
180
+ const containsGeneratedCssAsset = (bundle, handled, handledNames) => Object.values(bundle).some((output) => generatedCssSource(output) !== void 0 && !handled?.has(output) && !handledNames?.has(output.fileName));
187
181
  const optimizeStaticCssAssets = (bundle, session, options = {}) => {
188
- const { environment, prune = true, requiredClasses, sourcemap = session.sourcemap } = options;
182
+ const { environment, prune = true, requiredClasses, sourcemap = session.sourcemap, handled, handledNames, split } = options;
189
183
  /** Assets in this bundle that carry the generated stylesheet, pruned or not. */
190
184
  let sheets = 0;
185
+ /** Their file names, after any rename below. */
186
+ const names = [];
187
+ const results = [];
191
188
  for (const output of Object.values(bundle)) {
192
189
  const source = generatedCssSource(output);
193
190
  if (source === void 0) continue;
191
+ if (handled?.has(output) || handledNames?.has(output.fileName)) continue;
192
+ handled?.add(output);
193
+ handledNames?.add(output.fileName);
194
194
  sheets++;
195
- const optimized = pruneStaticCss(source, session, {
195
+ names.push(output.fileName);
196
+ const original = output.fileName;
197
+ const pruned = pruneStaticCss(source, session, {
196
198
  environment,
197
199
  prune,
198
200
  requiredClasses
199
201
  });
200
- if (!prune) continue;
202
+ if (!prune) {
203
+ results.push({
204
+ original,
205
+ fileName: original,
206
+ source,
207
+ optimized: source,
208
+ asset: output,
209
+ moved: /* @__PURE__ */ new Set()
210
+ });
211
+ continue;
212
+ }
213
+ let optimized = pruned;
214
+ let moved = /* @__PURE__ */ new Set();
215
+ if (split?.ownership.size) {
216
+ const divided = splitStaticCss(pruned, session, split.ownership);
217
+ optimized = divided.css;
218
+ moved = divided.moved;
219
+ for (const [chunk, css] of divided.chunks) split.emit(chunk, css);
220
+ }
201
221
  output.source = optimized;
202
- if (optimized === source) continue;
222
+ if (optimized === source) {
223
+ results.push({
224
+ original,
225
+ fileName: original,
226
+ source,
227
+ optimized,
228
+ asset: output,
229
+ moved
230
+ });
231
+ continue;
232
+ }
233
+ names.pop();
203
234
  const nextName = output.fileName.replace(/\.css$/, `.b-${toHash(optimized)}.css`);
204
235
  if (bundle[nextName] && bundle[nextName] !== output) throw new Error(`bamboocss: final CSS asset name collision at ${JSON.stringify(nextName)}.`);
205
236
  const previous = output.fileName;
206
237
  output.fileName = nextName;
238
+ names.push(nextName);
239
+ handledNames?.add(nextName);
240
+ results.push({
241
+ original,
242
+ fileName: nextName,
243
+ source,
244
+ optimized,
245
+ asset: output,
246
+ moved
247
+ });
207
248
  replaceAssetReferences(bundle, previous, nextName, sourcemap);
249
+ try {
250
+ bundle[nextName] = output;
251
+ if (bundle[nextName] === output && nextName !== previous) delete bundle[previous];
252
+ } catch {}
253
+ }
254
+ return {
255
+ sheets,
256
+ names,
257
+ results
258
+ };
259
+ };
260
+ /** Text outputs a stylesheet's name can appear in. Fonts and images are skipped unread. */
261
+ const REFERENCING_EXTENSIONS = /\.(?:[cm]?js|html?|css|json|txt|map|webmanifest|svg|xml)$/i;
262
+ const CHUNK_EXTENSIONS = /\.[cm]?js$/i;
263
+ /**
264
+ * Rewrite one written file the way `replaceChunkReference` rewrites a chunk in memory.
265
+ *
266
+ * A chunk with a sourcemap — a sibling `.map`, or one inlined — has its mappings carried
267
+ * across the edit: the replacement changes the length of a line, and every mapping after it
268
+ * on that line would otherwise be off by the difference.
269
+ */
270
+ const replaceFileReference = (path, previous, next) => {
271
+ const code = readFileSync(path, "utf8");
272
+ if (!code.includes(previous)) return false;
273
+ if (!CHUNK_EXTENSIONS.test(path)) {
274
+ writeFileSync(path, code.replaceAll(previous, next));
275
+ return true;
276
+ }
277
+ const magic = new MagicString(code);
278
+ let index = code.indexOf(previous);
279
+ while (index !== -1) {
280
+ magic.overwrite(index, index + previous.length, next);
281
+ index = code.indexOf(previous, index + previous.length);
282
+ }
283
+ let rewritten = magic.toString();
284
+ const inlineMap = code.match(INLINE_SOURCE_MAP)?.[0];
285
+ const mapPath = `${path}.map`;
286
+ const rawMap = inlineMap ? JSON.parse(Buffer.from(inlineMap.slice(inlineMap.indexOf("base64,") + 7), "base64").toString()) : existsSync(mapPath) ? JSON.parse(readFileSync(mapPath, "utf8")) : void 0;
287
+ if (rawMap) {
288
+ const combined = remapping([magic.generateMap({
289
+ source: basename(path),
290
+ hires: "boundary"
291
+ }), rawMap], () => null);
292
+ if (rawMap.file) combined.file = rawMap.file;
293
+ if (rawMap.debugId) combined.debugId = rawMap.debugId;
294
+ if (inlineMap) rewritten = rewritten.replace(INLINE_SOURCE_MAP, "") + `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from(combined.toString()).toString("base64")}`;
295
+ else writeFileSync(mapPath, combined.toString());
296
+ }
297
+ writeFileSync(path, rewritten);
298
+ return true;
299
+ };
300
+ /**
301
+ * Prune every deferred sheet against the whole run's reachability, on disk.
302
+ *
303
+ * From the *unpruned* source, not from the file: the file was pruned against a partial run and
304
+ * may lack a rule a later environment reaches, and pruning cannot put a rule back. When the
305
+ * final bytes are the provisional bytes — the common case, since a later environment usually
306
+ * reaches nothing new — nothing on disk moves and every recorded name stays right. When they
307
+ * differ, the same contract as `optimizeStaticCssAssets` applies: the bytes and the name move
308
+ * together, every written reference moves with them across environments, and a copy of the
309
+ * provisional sheet a framework wrote into another output is replaced the same way.
310
+ */
311
+ const finalizeDeferredSheets = (sheets, session, options) => {
312
+ const { requiredClasses, prune = true, outputs, bundle, sourcemap = session.sourcemap } = options;
313
+ const finalized = [];
314
+ for (const sheet of sheets) {
315
+ if (!existsSync(join(sheet.dir, sheet.fileName))) continue;
316
+ let final = pruneStaticCss(sheet.source, session, {
317
+ prune,
318
+ requiredClasses
319
+ });
320
+ if (sheet.moved?.size) final = splitStaticCss(final, session, new Map([...sheet.moved].map((className) => [className, ""]))).css;
321
+ if (!prune || final === sheet.provisional) {
322
+ finalized.push({
323
+ ...sheet,
324
+ before: sheet.provisional.length,
325
+ after: sheet.provisional.length
326
+ });
327
+ continue;
328
+ }
329
+ const nextName = sheet.originalFileName.replace(/\.css$/, `.b-${toHash(final)}.css`);
330
+ const directories = new Set([sheet.dir]);
331
+ for (const output of outputs) directories.add(output.dir);
332
+ for (const dir of directories) {
333
+ const copy = join(dir, sheet.fileName);
334
+ if (!existsSync(copy)) continue;
335
+ if (dir !== sheet.dir && readFileSync(copy, "utf8") !== sheet.provisional) continue;
336
+ writeFileSync(join(dir, nextName), final);
337
+ rmSync(copy, { force: true });
338
+ }
339
+ const seen = /* @__PURE__ */ new Set();
340
+ for (const output of outputs) for (const file of output.files) {
341
+ if (!REFERENCING_EXTENSIONS.test(file)) continue;
342
+ const target = join(output.dir, file);
343
+ if (seen.has(target) || file === sheet.fileName) continue;
344
+ seen.add(target);
345
+ if (!existsSync(target)) continue;
346
+ replaceFileReference(target, sheet.fileName, nextName);
347
+ }
348
+ if (sheet.asset) {
349
+ sheet.asset.source = final;
350
+ sheet.asset.fileName = nextName;
351
+ if (sheet.bundle) {
352
+ try {
353
+ sheet.bundle[nextName] = sheet.asset;
354
+ if (sheet.bundle[nextName] === sheet.asset) delete sheet.bundle[sheet.fileName];
355
+ } catch {}
356
+ replaceAssetReferences(sheet.bundle, sheet.fileName, nextName, sheet.sourcemap);
357
+ }
358
+ }
359
+ if (bundle) replaceAssetReferences(bundle, sheet.fileName, nextName, sourcemap);
360
+ finalized.push({
361
+ ...sheet,
362
+ renamed: nextName,
363
+ before: sheet.provisional.length,
364
+ after: final.length
365
+ });
366
+ }
367
+ return finalized;
368
+ };
369
+ /** The at-rules a rule sits under, outermost first, as name and params. */
370
+ const ancestryOf = (rule) => {
371
+ const chain = [];
372
+ let parent = rule.parent;
373
+ while (parent && parent.type !== "root") {
374
+ if (parent.type === "atrule") {
375
+ const atRule = parent;
376
+ chain.unshift({
377
+ name: atRule.name,
378
+ params: atRule.params
379
+ });
380
+ }
381
+ parent = parent.parent;
382
+ }
383
+ return chain;
384
+ };
385
+ /** The container in `root` under the same at-rule chain, created where absent. */
386
+ const containerFor = (root, chain) => {
387
+ let container = root;
388
+ for (const { name, params } of chain) {
389
+ let next = container.nodes?.find((node) => node.type === "atrule" && node.name === name && node.params === params);
390
+ if (!next) {
391
+ next = postcss.atRule({
392
+ name,
393
+ params,
394
+ nodes: []
395
+ });
396
+ container.append(next);
397
+ }
398
+ container = next;
399
+ }
400
+ return container;
401
+ };
402
+ /**
403
+ * Move every utility rule a lazily loaded chunk owns into a sheet of that chunk's own.
404
+ *
405
+ * Reads the finished entry sheet — pruned, sublayered, minified or not — and walks its
406
+ * utility rules. A selector naming exactly one class that `ownership` assigns to a chunk is
407
+ * moved to that chunk's sheet under the same at-rule chain: the utilities layer, its sublayer,
408
+ * any media or container query. A rule with several selectors is split per selector, since a
409
+ * merged rule's members can belong to different chunks. Whatever nothing owns stays.
410
+ *
411
+ * Each chunk sheet opens with the entry's sublayer order statement, so whichever sheet the
412
+ * document happens to parse first establishes the same order. That is what makes the split
413
+ * safe at all: precedence lives in the sublayers, not in where a rule sits.
414
+ */
415
+ const splitStaticCss = (css, session, ownership) => {
416
+ const chunks = /* @__PURE__ */ new Map();
417
+ const moved = /* @__PURE__ */ new Set();
418
+ if (!ownership.size || !css.includes("--made-with-bamboo")) return {
419
+ css,
420
+ chunks,
421
+ moved
422
+ };
423
+ const root = postcss.parse(css);
424
+ const roots = /* @__PURE__ */ new Map();
425
+ const rootFor = (chunk) => {
426
+ let chunkRoot = roots.get(chunk);
427
+ if (!chunkRoot) {
428
+ chunkRoot = postcss.root();
429
+ roots.set(chunk, chunkRoot);
430
+ }
431
+ return chunkRoot;
432
+ };
433
+ const isUtilityRule = (rule) => {
434
+ let parent = rule.parent;
435
+ while (parent) {
436
+ if (parent.type === "atrule") {
437
+ const atRule = parent;
438
+ if (atRule.name === "layer" && atRule.params === session.utilityLayer) return true;
439
+ }
440
+ parent = parent.parent;
441
+ }
442
+ return false;
443
+ };
444
+ let order;
445
+ root.walkAtRules("layer", (atRule) => {
446
+ if (order || atRule.nodes) return;
447
+ const parent = atRule.parent;
448
+ if (parent?.type === "atrule" && parent.params === session.utilityLayer) order = atRule;
449
+ });
450
+ root.walkRules((rule) => {
451
+ if (!isUtilityRule(rule)) return;
452
+ const kept = [];
453
+ const taken = /* @__PURE__ */ new Map();
454
+ for (const selector of rule.selectors) {
455
+ let owner;
456
+ try {
457
+ selectorParser((selectors) => {
458
+ const classes = /* @__PURE__ */ new Set();
459
+ selectors.walkClasses((classNode) => {
460
+ classes.add(bare(classNode.toString().slice(1)));
461
+ });
462
+ if (classes.size !== 1) return;
463
+ const [className] = classes;
464
+ owner = ownership.get(className);
465
+ if (owner !== void 0) moved.add(className);
466
+ }).processSync(selector);
467
+ } catch {}
468
+ if (owner === void 0) kept.push(selector);
469
+ else (taken.get(owner) ?? taken.set(owner, []).get(owner)).push(selector);
470
+ }
471
+ if (!taken.size) return;
472
+ const chain = ancestryOf(rule);
473
+ for (const [chunk, selectors] of taken) containerFor(rootFor(chunk), chain).append(rule.clone({ selectors }));
474
+ if (kept.length) rule.selectors = kept;
475
+ else rule.remove();
476
+ });
477
+ let removed = true;
478
+ while (removed) {
479
+ removed = false;
480
+ root.walkAtRules((atRule) => {
481
+ if (atRule.nodes?.length !== 0) return;
482
+ atRule.remove();
483
+ removed = true;
484
+ });
208
485
  }
209
- return { sheets };
486
+ for (const [chunk, chunkRoot] of roots) {
487
+ if (order) chunkRoot.nodes.find((node) => node.type === "atrule" && node.name === "layer" && node.params === session.utilityLayer)?.prepend(order.clone());
488
+ chunks.set(chunk, chunkRoot.toString());
489
+ }
490
+ return {
491
+ css: root.toString(),
492
+ chunks,
493
+ moved
494
+ };
495
+ };
496
+ /** A class token of a selector, escapes and all: `.hover\\:c_red600` up to its unescaped `:`. */
497
+ const CLASS_TOKEN = /\.((?:\\.|[^\\\s.:>+~[\](),])+)/g;
498
+ /**
499
+ * The top-level comma-separated parts of a selector list as written, each with its offset.
500
+ *
501
+ * `rule.selectors` splits the same way but loses where each part sits, and a merged rule's
502
+ * selectors sit on separate lines in the unminified sheet a dev server serves.
503
+ */
504
+ const selectorParts = (raw) => {
505
+ const parts = [];
506
+ let depth = 0;
507
+ let start = 0;
508
+ for (let index = 0; index < raw.length; index++) {
509
+ const char = raw[index];
510
+ if (char === "\\") index++;
511
+ else if (char === "(" || char === "[") depth++;
512
+ else if (char === ")" || char === "]") depth--;
513
+ else if (char === "," && depth === 0) {
514
+ parts.push({
515
+ text: raw.slice(start, index),
516
+ offset: start
517
+ });
518
+ start = index + 1;
519
+ }
520
+ }
521
+ parts.push({
522
+ text: raw.slice(start),
523
+ offset: start
524
+ });
525
+ return parts.map((part) => {
526
+ const leading = part.text.length - part.text.trimStart().length;
527
+ return {
528
+ text: part.text.trim(),
529
+ offset: part.offset + leading
530
+ };
531
+ });
532
+ };
533
+ /** `start` advanced over `text` up to `offset`, in 1-based lines and columns. */
534
+ const positionAt = (text, offset, start) => {
535
+ let { line, column } = start;
536
+ for (let index = 0; index < offset; index++) if (text[index] === "\n") {
537
+ line++;
538
+ column = 1;
539
+ } else column++;
540
+ return {
541
+ line,
542
+ column
543
+ };
544
+ };
545
+ /**
546
+ * A source map from each rule of the served stylesheet to the call site its atom was first
547
+ * encoded at, for DevTools to name the file and line a rule came from.
548
+ *
549
+ * Read off the finished sheet rather than threaded through its generation: rules enter the
550
+ * postcss tree as strings, which strips their positions, and the optimizer re-parses the text
551
+ * twice more. One parse here costs the same as one of those and needs no cooperation from
552
+ * either. A rule maps at its selector, and a merged rule maps each of its selectors, since
553
+ * the optimizer folds rules from different call sites into one. `sourcesContent` is left for
554
+ * Vite to fill from disk, which it does for a dev stylesheet's map before inlining it.
555
+ */
556
+ const cssSourceMap = (css, origins) => {
557
+ const byBareClass = /* @__PURE__ */ new Map();
558
+ for (const [className, origin] of origins) {
559
+ const name = bare(className);
560
+ if (!byBareClass.has(name)) byBareClass.set(name, origin);
561
+ }
562
+ if (!byBareClass.size) return void 0;
563
+ const originOf = (selector) => {
564
+ for (const match of selector.matchAll(CLASS_TOKEN)) {
565
+ const origin = byBareClass.get(bare(match[1]));
566
+ if (origin) return origin;
567
+ }
568
+ };
569
+ const map = new GenMapping();
570
+ let mapped = 0;
571
+ postcss.parse(css).walkRules((rule) => {
572
+ const start = rule.source?.start;
573
+ if (!start) return;
574
+ const raw = rule.raws.selector?.raw ?? rule.selector;
575
+ for (const part of selectorParts(raw)) {
576
+ const origin = originOf(part.text);
577
+ if (!origin) continue;
578
+ const generated = positionAt(raw, part.offset, start);
579
+ addMapping(map, {
580
+ generated: {
581
+ line: generated.line,
582
+ column: generated.column - 1
583
+ },
584
+ source: origin.filePath,
585
+ original: {
586
+ line: origin.line,
587
+ column: origin.column - 1
588
+ }
589
+ });
590
+ mapped++;
591
+ }
592
+ });
593
+ if (!mapped) return void 0;
594
+ const encoded = toEncodedMap(map);
595
+ return {
596
+ version: 3,
597
+ mappings: encoded.mappings,
598
+ names: [...encoded.names],
599
+ sources: encoded.sources.map((source) => source ?? "")
600
+ };
210
601
  };
211
602
  //#endregion
212
- export { containsGeneratedCssAsset, optimizeStaticCssAssets, pruneStaticCss };
603
+ export { containsGeneratedCssAsset, cssSourceMap, finalizeDeferredSheets, optimizeStaticCssAssets, pruneStaticCss, splitStaticCss };
@@ -1,7 +1,7 @@
1
1
  const require_chunk = require("./chunk.cjs");
2
+ let node_path = require("node:path");
2
3
  let _bamboocss_shared = require("@bamboocss/shared");
3
4
  let node_crypto = require("node:crypto");
4
- let node_path = require("node:path");
5
5
  let magic_string = require("magic-string");
6
6
  magic_string = require_chunk.__toESM(magic_string);
7
7
  let _bamboocss_config_ts_path = require("@bamboocss/config/ts-path");
@@ -1,6 +1,6 @@
1
+ import { dirname, relative, resolve } from "node:path";
1
2
  import { compact, createCssUncached, createMergeCss, memo, viewTransitionClassName } from "@bamboocss/shared";
2
3
  import { createHash } from "node:crypto";
3
- import { dirname, relative, resolve } from "node:path";
4
4
  import MagicString from "magic-string";
5
5
  import { resolveTsPathPattern } from "@bamboocss/config/ts-path";
6
6
  import { box, maybeBoxNode } from "@bamboocss/extractor";