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