@animus-ui/vite-plugin 0.1.49 → 0.1.50

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.
@@ -0,0 +1,27 @@
1
+ import type { PluginContext } from './context';
2
+ import type { Rolldown } from 'vite';
3
+ /** What a build that removes unbundled components does to its slices. */
4
+ export interface BundleRemoval {
5
+ /** Emits `file` without the removed components' rules, beside it under a
6
+ * name its new content hashes, and returns that name; `null` keeps it. */
7
+ replace(file: Rolldown.OutputAsset): string | null;
8
+ /** Points Vite's manifests and chunk metadata at each replacement where
9
+ * they named the file replaced, then reports. */
10
+ finish(): void;
11
+ }
12
+ /**
13
+ * The removal of every component no output file of this client build
14
+ * includes, or `null` where the build keeps every component: it bundles
15
+ * another environment beside the client (`otherEnvironments`), whose output
16
+ * may render what the client's does not; it leaves a module external; it
17
+ * carries data the scan cannot read as text; or an output file other than
18
+ * Vite's `manifests` names a stylesheet slice, which a replacement would
19
+ * leave naming a deleted file. Every output file but the slices
20
+ * (`sliceIds`) and media is evidence, data URLs decoded.
21
+ */
22
+ export declare function bundleRemoval(ctx: PluginContext, bundle: Rolldown.OutputBundle, sliceIds: readonly string[], context: {
23
+ otherEnvironments: readonly string[];
24
+ manifests: ReadonlySet<string>;
25
+ emit: (asset: Rolldown.EmittedAsset) => void;
26
+ }): BundleRemoval | null;
27
+ //# sourceMappingURL=bundle-removal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-removal.d.ts","sourceRoot":"","sources":["../src/bundle-removal.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAErC,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B;+EAC2E;IAC3E,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,WAAW,GAAG,MAAM,GAAG,IAAI,CAAC;IACnD;sDACkD;IAClD,MAAM,IAAI,IAAI,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,QAAQ,CAAC,YAAY,EAC7B,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,OAAO,EAAE;IACP,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,YAAY,KAAK,IAAI,CAAC;CAC9C,GACA,aAAa,GAAG,IAAI,CAiHtB"}
@@ -21,6 +21,9 @@ export declare function injectHeadTags(html: string, tags: HtmlTagDescriptor[]):
21
21
  * the font preloads, then the development bridge.
22
22
  */
23
23
  export declare function buildIndexHtmlTags(ctx: PluginContext, html?: string): HtmlTagDescriptor[];
24
+ /** The slice `file` holds, by its id, or a path to it from the root, as
25
+ * Vite versions spell it. */
26
+ export declare function sliceIdOf(file: Rolldown.OutputAsset | Rolldown.OutputChunk, sliceIds: readonly string[]): string | undefined;
24
27
  /**
25
28
  * Puts a split build's slices where the single stylesheet would have been.
26
29
  * The stylesheet module held only a marker rule, so it kept its place in
@@ -31,5 +34,7 @@ export declare function buildIndexHtmlTags(ctx: PluginContext, html?: string): H
31
34
  * lazy chunk that names it. The build fails when no page links the
32
35
  * carrier: its stylesheet loads with a lazy chunk, or no page is built.
33
36
  */
34
- export declare function placeStylesheetSlices(ctx: PluginContext, bundle: Rolldown.OutputBundle, sliceIds: readonly string[], emitAsset: (name: string, source: string) => string): void;
37
+ export declare function placeStylesheetSlices(ctx: PluginContext, bundle: Rolldown.OutputBundle, sliceIds: readonly string[], emitAsset: (name: string, source: string) => string,
38
+ /** The file emitted in a slice's place, or `null` to ship Vite's own. */
39
+ replace?: (file: Rolldown.OutputAsset) => string | null): void;
35
40
  //# sourceMappingURL=index-html.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index-html.d.ts","sourceRoot":"","sources":["../src/index-html.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AA0CxD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,MAAM,EAAE,CAeV;AAsED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,iBAAiB,EAAE,GACxB,MAAM,GAAG,iBAAiB,EAAE,CAU9B;AA2BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,aAAa,EAClB,IAAI,SAAK,GACR,iBAAiB,EAAE,CAuCrB;AAsCD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,QAAQ,CAAC,YAAY,EAC7B,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,MAAM,GAClD,IAAI,CAuFN"}
1
+ {"version":3,"file":"index-html.d.ts","sourceRoot":"","sources":["../src/index-html.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AA0CxD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,MAAM,EAAE,CAeV;AAsED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,iBAAiB,EAAE,GACxB,MAAM,GAAG,iBAAiB,EAAE,CAU9B;AA2BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,aAAa,EAClB,IAAI,SAAK,GACR,iBAAiB,EAAE,CAuCrB;AAsCD;8BAC8B;AAC9B,wBAAgB,SAAS,CACvB,IAAI,EAAE,QAAQ,CAAC,WAAW,GAAG,QAAQ,CAAC,WAAW,EACjD,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,MAAM,GAAG,SAAS,CAKpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,QAAQ,CAAC,YAAY,EAC7B,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,MAAM;AACnD,yEAAyE;AACzE,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,WAAW,KAAK,MAAM,GAAG,IAAI,GACtD,IAAI,CA2FN"}
package/dist/index.cjs CHANGED
@@ -8,6 +8,7 @@ let vite = require("vite");
8
8
  let fs = require("fs");
9
9
  let _animus_ui_extract_session = require("@animus-ui/extract/session");
10
10
  let child_process = require("child_process");
11
+ let node_path = require("node:path");
11
12
  let crypto = require("crypto");
12
13
  //#region src/asset-copy-dirs.ts
13
14
  /**
@@ -182,6 +183,397 @@ async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
182
183
  if (ctx.options.verify) ctx.runSelfVerify();
183
184
  }
184
185
  //#endregion
186
+ //#region src/constants.ts
187
+ /**
188
+ * The namespace every virtual module here lives under; the `transform` guard
189
+ * matches on this same string so a rename cannot strand it.
190
+ */
191
+ const VIRTUAL_PREFIX = "virtual:animus/";
192
+ const VIRTUAL_CSS_ID = `${VIRTUAL_PREFIX}styles.css`;
193
+ const RESOLVED_CSS_ID = `\0${VIRTUAL_CSS_ID}`;
194
+ /** A slice of the stylesheet, which `stylesheets: 'split'` builds emit as
195
+ * a file of its own. */
196
+ const sliceStylesheetId = (slice) => `${VIRTUAL_PREFIX}styles.${slice}.css`;
197
+ const RESOLVED_SLICE_IDS = new Map(_animus_ui_extract_pipeline.STYLESHEET_SLICES.map((slice) => [`\0${sliceStylesheetId(slice)}`, slice]));
198
+ /**
199
+ * What the stylesheet module holds in a split build: one rule that keeps
200
+ * the stylesheet's place in Vite's CSS, where the page's links to the
201
+ * slices then go. Its own property, so no minifier merges or drops it.
202
+ */
203
+ const SPLIT_STYLESHEET_MARKER = ".animus-split-stylesheet{--animus-split-stylesheet:1}";
204
+ /** The marker as a built stylesheet spells it, minified or not. */
205
+ const SPLIT_STYLESHEET_MARKER_RE = /\.animus-split-stylesheet\s*\{\s*--animus-split-stylesheet\s*:\s*1\s*;?\s*\}\n?/;
206
+ const VIRTUAL_COMPONENTS_ID = `${VIRTUAL_PREFIX}components.js`;
207
+ const RESOLVED_COMPONENTS_ID = `\0${VIRTUAL_COMPONENTS_ID}`;
208
+ const VIRTUAL_BRIDGE_ID = `${VIRTUAL_PREFIX}hmr-bridge.js`;
209
+ const RESOLVED_BRIDGE_ID = `\0${VIRTUAL_BRIDGE_ID}`;
210
+ /**
211
+ * Vite reserves `/@id/` for module ids that are not file paths and strips the
212
+ * prefix before `resolveId`, so the unprefixed specifier travels here.
213
+ */
214
+ const BRIDGE_SCRIPT_SRC = `/@id/${VIRTUAL_BRIDGE_ID}`;
215
+ const VIRTUAL_SYSTEM_PROPS_ID = `${VIRTUAL_PREFIX}system-props`;
216
+ const RESOLVED_SYSTEM_PROPS_ID = `\0${VIRTUAL_SYSTEM_PROPS_ID}`;
217
+ /** The faces this build preloads, with the URL each file is preloaded at:
218
+ * the module every host serves under this id. */
219
+ const VIRTUAL_FONT_PRELOADS_ID = _animus_ui_extract_session.FONT_PRELOADS_MODULE_ID;
220
+ const RESOLVED_FONT_PRELOADS_ID = `\0${VIRTUAL_FONT_PRELOADS_ID}`;
221
+ //#endregion
222
+ //#region src/index-html.ts
223
+ const LINK_TAG_RE = /<link\b[^>]*>/gi;
224
+ /** Marks the preloads Animus writes, so another preload of the same file
225
+ * can be told from Animus's own. */
226
+ const ANIMUS_PRELOAD_MARKER = "data-animus-preload";
227
+ const MARKER_RE = /\sdata-animus-preload(?=[\s=/>])/i;
228
+ const REL_RE = /\srel\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
229
+ const HREF_RE = /\shref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
230
+ function attribute(tag, re) {
231
+ const match = re.exec(tag);
232
+ return match ? match[1] ?? match[2] ?? match[3] : void 0;
233
+ }
234
+ /** Each `<link rel="preload">` in `html`, with its href. */
235
+ function* preloadLinks(html) {
236
+ for (const [tag] of html.matchAll(LINK_TAG_RE)) {
237
+ const rel = attribute(tag, REL_RE)?.toLowerCase().split(/\s+/) ?? [];
238
+ const href = attribute(tag, HREF_RE);
239
+ if (rel.includes("preload") && href !== void 0) yield [tag, href];
240
+ }
241
+ }
242
+ const ENTITY_RE = /&(?:#x([0-9a-f]+)|#(\d+)|(amp|lt|gt|quot|apos));/gi;
243
+ const NAMED_ENTITIES = /* @__PURE__ */ new Map([
244
+ ["amp", "&"],
245
+ ["lt", "<"],
246
+ ["gt", ">"],
247
+ ["quot", "\""],
248
+ ["apos", "'"]
249
+ ]);
250
+ /** An attribute value as the browser reads it: character references decoded. */
251
+ function attributeText(value) {
252
+ return value.replace(ENTITY_RE, (_, hex, decimal, named) => named === void 0 ? String.fromCodePoint(Number.parseInt(hex ?? decimal, hex ? 16 : 10)) : NAMED_ENTITIES.get(named.toLowerCase()));
253
+ }
254
+ /**
255
+ * The hrefs a finished document preloads both through Animus's tag and
256
+ * through another, which the page wrote or a hook running after Animus's
257
+ * added: the browser may then fetch the file twice. Each href is compared
258
+ * as the browser resolves it against `pageUrl`, the URL the page is served
259
+ * at, and reported as Animus wrote it.
260
+ */
261
+ function duplicatedFontPreloads(html, pageUrl) {
262
+ const resolved = (href) => {
263
+ try {
264
+ return new URL(attributeText(href), pageUrl).href;
265
+ } catch {
266
+ return href;
267
+ }
268
+ };
269
+ const ours = /* @__PURE__ */ new Map();
270
+ const others = /* @__PURE__ */ new Set();
271
+ for (const [tag, href] of preloadLinks(html)) if (MARKER_RE.test(tag)) ours.set(resolved(href), href);
272
+ else others.add(resolved(href));
273
+ return [...ours].filter(([url]) => others.has(url)).map(([, href]) => href);
274
+ }
275
+ /** The hrefs the document already preloads. */
276
+ function preloadedHrefs(html) {
277
+ return new Set([...preloadLinks(html)].map(([, href]) => href));
278
+ }
279
+ /** The head elements Animus's tags go after (the charset and viewport
280
+ * metas), the stylesheets they go before, and what holds neither. */
281
+ const HEAD_SCAN_RE = /<!--[\s\S]*?-->|<script\b[\s\S]*?<\/script\s*>|<\/head\s*>|<style\b|<(?:meta|link)\b[^>]*>/gi;
282
+ /** Where Animus's head tags go: after the head's charset and viewport
283
+ * metas, so they cannot push the charset past the 1024 bytes a browser
284
+ * sniffs for it, and before its first stylesheet; -1 without a `<head>`. */
285
+ function headTagOffset(html) {
286
+ const open = /<head\b[^>]*>/i.exec(html);
287
+ if (open === null) return -1;
288
+ const start = open.index + open[0].length;
289
+ let offset = start;
290
+ for (const match of html.slice(start).matchAll(HEAD_SCAN_RE)) {
291
+ const [element] = match;
292
+ const lower = element.toLowerCase();
293
+ if (lower.startsWith("</head") || lower.startsWith("<style")) break;
294
+ if (lower.startsWith("<link")) {
295
+ if ((attribute(element, REL_RE)?.toLowerCase().split(/\s+/))?.includes("stylesheet")) break;
296
+ } else if (lower.startsWith("<meta") && (/\scharset\s*=/i.test(element) || /\shttp-equiv\s*=\s*["']?content-type\b/i.test(element) || /\sname\s*=\s*["']?viewport\b/i.test(element))) offset = start + match.index + element.length;
297
+ }
298
+ return offset;
299
+ }
300
+ /** As `escape-html` escapes, which Vite applies to attribute values. */
301
+ function escapeHtml(value) {
302
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
303
+ }
304
+ /** A tag as Vite serializes it, `children` as written, except that an
305
+ * undefined attribute is left out rather than written "undefined". */
306
+ function serializeTag({ tag, attrs = {}, children }) {
307
+ const serialized = Object.entries(attrs).map(([key, value]) => {
308
+ if (value === true) return ` ${key}`;
309
+ if (value === false || value === void 0) return "";
310
+ return ` ${key}="${escapeHtml(value)}"`;
311
+ }).join("");
312
+ if (tag === "link" || tag === "meta" || tag === "base") return `<${tag}${serialized}>`;
313
+ return `<${tag}${serialized}>${Array.isArray(children) ? "" : children ?? ""}</${tag}>`;
314
+ }
315
+ /**
316
+ * The hook's result: the tags written after the charset and viewport metas,
317
+ * each on its own line indented as the element it follows, so the charset
318
+ * stays in the first 1024 bytes however long the tags are; as `head-prepend`
319
+ * tags for a document without a `<head>`.
320
+ */
321
+ function injectHeadTags(html, tags) {
322
+ const offset = headTagOffset(html);
323
+ if (offset === -1) return tags;
324
+ if (tags.length === 0) return html;
325
+ const lineStart = html.lastIndexOf("\n", offset - 1) + 1;
326
+ const indent = /^[ \t]*/.exec(html.slice(lineStart))[0];
327
+ const inserted = tags.map((tag) => `\n${indent}${serializeTag(tag)}`).join("");
328
+ return html.slice(0, offset) + inserted + html.slice(offset);
329
+ }
330
+ /** The preloads for the faces that ask for one, each marked as Animus's;
331
+ * one the document already preloads is left out. */
332
+ function fontPreloadTags(ctx, html) {
333
+ if (!ctx.served?.fontPreloads.length) return [];
334
+ const existing = preloadedHrefs(html);
335
+ return ctx.fontPreloads.filter(({ href }) => !existing.has(href)).map(({ href, type, fetchpriority }) => {
336
+ const attrs = {
337
+ rel: "preload",
338
+ href,
339
+ as: "font"
340
+ };
341
+ if (type !== void 0) attrs.type = type;
342
+ if (fetchpriority !== void 0) attrs.fetchpriority = fetchpriority;
343
+ attrs.crossorigin = true;
344
+ attrs[ANIMUS_PRELOAD_MARKER] = true;
345
+ return {
346
+ tag: "link",
347
+ attrs,
348
+ injectTo: "head-prepend"
349
+ };
350
+ });
351
+ }
352
+ /**
353
+ * Animus's head tags in document order: the bootstrap script, so it runs
354
+ * before first paint and ahead of every stylesheet, the layer declaration,
355
+ * the font preloads, then the development bridge.
356
+ */
357
+ function buildIndexHtmlTags(ctx, html = "") {
358
+ const tags = [];
359
+ const bootstrap = ctx.options.appearanceBootstrap;
360
+ if (bootstrap?.code) tags.push({
361
+ tag: "script",
362
+ attrs: { "data-animus-bootstrap": "" },
363
+ children: bootstrap.code,
364
+ injectTo: "head-prepend"
365
+ });
366
+ if (ctx.layerDeclaration) tags.push({
367
+ tag: "style",
368
+ attrs: { "data-animus-layers": "" },
369
+ children: ctx.layerDeclaration,
370
+ injectTo: "head-prepend"
371
+ });
372
+ tags.push(...fontPreloadTags(ctx, html));
373
+ if (!ctx.isProd) tags.push({
374
+ tag: "script",
375
+ attrs: {
376
+ type: "module",
377
+ src: BRIDGE_SCRIPT_SRC,
378
+ "data-animus-bridge": ""
379
+ },
380
+ injectTo: "head-prepend"
381
+ });
382
+ return tags;
383
+ }
384
+ /**
385
+ * A built file's URL in a page, as Vite writes its own links: the host's
386
+ * `renderBuiltUrl` first, then relative to the page under a relative base,
387
+ * else under the base.
388
+ */
389
+ function builtUrl(ctx, page, fileName) {
390
+ let relative = ctx.base === "" || ctx.base === "./";
391
+ const rendered = ctx.renderBuiltUrl?.(fileName, {
392
+ hostId: page,
393
+ hostType: "html",
394
+ type: "asset",
395
+ ssr: false
396
+ });
397
+ if (rendered instanceof Object) {
398
+ if (rendered.runtime !== void 0) throw new Error(`[animus-extract] renderBuiltUrl returned { runtime } for ${fileName}, which a page's <link> cannot load`);
399
+ if (rendered.relative !== void 0) relative = rendered.relative;
400
+ } else if (rendered) return rendered;
401
+ if (relative) return `${path.posix.join(path.posix.relative(page, "").slice(0, -2), "./")}${fileName}`;
402
+ return `${decodeURI(ctx.base).replace(/\/$/, "")}/${fileName}`;
403
+ }
404
+ /** `tag`, a stylesheet link, linking `href` instead, with `attributes`. */
405
+ function linkingInstead(tag, href, attributes = "") {
406
+ return tag.replace(HREF_RE, ` href="${escapeHtml(href)}"`).replace(/\s*\/?>$/, `${attributes}>`);
407
+ }
408
+ /** The slice `file` holds, by its id, or a path to it from the root, as
409
+ * Vite versions spell it. */
410
+ function sliceIdOf(file, sliceIds) {
411
+ if (file.type !== "asset") return void 0;
412
+ return sliceIds.find((slice) => file.originalFileNames.some((name) => name.endsWith(slice)));
413
+ }
414
+ /**
415
+ * Puts a split build's slices where the single stylesheet would have been.
416
+ * The stylesheet module held only a marker rule, so it kept its place in
417
+ * Vite's CSS: the file holding the marker is cut there, and each page's
418
+ * link to it becomes the file's CSS before the marker, the slices in
419
+ * order, then its CSS after the marker, in a file of its own when CSS
420
+ * precedes the marker too. A carrier left empty is kept, unlinked, for a
421
+ * lazy chunk that names it. The build fails when no page links the
422
+ * carrier: its stylesheet loads with a lazy chunk, or no page is built.
423
+ */
424
+ function placeStylesheetSlices(ctx, bundle, sliceIds, emitAsset, replace) {
425
+ const fileOf = /* @__PURE__ */ new Map();
426
+ const pages = [];
427
+ const carriers = [];
428
+ for (const file of Object.values(bundle)) {
429
+ if (file.type !== "asset") continue;
430
+ if (file.fileName.endsWith(".html")) pages.push(file);
431
+ if (file.fileName.endsWith(".css") && SPLIT_STYLESHEET_MARKER_RE.test(String(file.source))) carriers.push(file);
432
+ const id = sliceIdOf(file, sliceIds);
433
+ if (id === void 0) continue;
434
+ const replacement = replace?.(file);
435
+ if (replacement === void 0 || replacement === null) {
436
+ fileOf.set(id, file.fileName);
437
+ continue;
438
+ }
439
+ delete bundle[file.fileName];
440
+ fileOf.set(id, replacement);
441
+ }
442
+ const slices = sliceIds.map((id) => {
443
+ const fileName = fileOf.get(id);
444
+ if (fileName === void 0) throw new Error(`[animus-extract] stylesheets: 'split' emitted ${id}, and the bundle holds no file for it, so the stylesheet would load without it.`);
445
+ return fileName;
446
+ });
447
+ if (carriers.length === 0) {
448
+ for (const fileName of slices) delete bundle[fileName];
449
+ return;
450
+ }
451
+ if (carriers.length > 1) throw new Error(`[animus-extract] stylesheets: 'split' found Animus's stylesheet in ${carriers.map((file) => file.fileName).join(" and ")}, and places it once.`);
452
+ const carrier = carriers[0];
453
+ const text = String(carrier.source);
454
+ const marker = SPLIT_STYLESHEET_MARKER_RE.exec(text);
455
+ const before = text.slice(0, marker.index);
456
+ const after = text.slice(marker.index + marker[0].length);
457
+ const precedes = before.trim() !== "";
458
+ const follows = after.trim() !== "";
459
+ carrier.source = precedes ? before : after;
460
+ const rest = precedes && follows ? emitAsset(carrier.names[0] ?? "index.css", after) : null;
461
+ const sequence = [
462
+ ...precedes ? [null] : [],
463
+ ...slices,
464
+ ...follows ? [rest] : []
465
+ ];
466
+ let linked = false;
467
+ for (const page of pages) {
468
+ const html = Buffer.from(page.source).toString("utf8");
469
+ const carrierUrl = builtUrl(ctx, page.fileName, carrier.fileName);
470
+ page.source = html.replace(LINK_TAG_RE, (tag, offset) => {
471
+ const rel = attribute(tag, REL_RE)?.toLowerCase().split(/\s+/) ?? [];
472
+ const href = attribute(tag, HREF_RE);
473
+ if (!rel.includes("stylesheet") || href === void 0) return tag;
474
+ if (attributeText(href) !== carrierUrl) return tag;
475
+ linked = true;
476
+ const lineStart = html.lastIndexOf("\n", offset - 1) + 1;
477
+ const indent = /^[ \t]*/.exec(html.slice(lineStart))[0];
478
+ return sequence.map((fileName) => fileName === null ? tag : linkingInstead(tag, builtUrl(ctx, page.fileName, fileName), fileName === rest ? "" : " data-animus-stylesheet")).join(`\n${indent}`);
479
+ });
480
+ }
481
+ if (!linked) throw new Error(`[animus-extract] stylesheets: 'split' links the stylesheet's files where a page links ${carrier.fileName}, which holds Animus's stylesheet, and no page links it: it loads with a lazy chunk, or the build has no HTML page. Import Animus's components from the page's entry, or leave \`stylesheets\` unset for one sheet.`);
482
+ }
483
+ //#endregion
484
+ //#region src/bundle-removal.ts
485
+ /**
486
+ * The removal of every component no output file of this client build
487
+ * includes, or `null` where the build keeps every component: it bundles
488
+ * another environment beside the client (`otherEnvironments`), whose output
489
+ * may render what the client's does not; it leaves a module external; it
490
+ * carries data the scan cannot read as text; or an output file other than
491
+ * Vite's `manifests` names a stylesheet slice, which a replacement would
492
+ * leave naming a deleted file. Every output file but the slices
493
+ * (`sliceIds`) and media is evidence, data URLs decoded.
494
+ */
495
+ function bundleRemoval(ctx, bundle, sliceIds, context) {
496
+ const manifest = ctx.storedManifest;
497
+ if (!manifest) return null;
498
+ const keep = (why) => {
499
+ ctx.info(`kept every component's CSS: ${why}`);
500
+ return null;
501
+ };
502
+ const blocker = removalBlocker(bundle, context.otherEnvironments);
503
+ if (blocker !== null) return keep(blocker);
504
+ const slices = new Set(Object.values(bundle).filter((file) => sliceIdOf(file, sliceIds) !== void 0).map((file) => file.fileName));
505
+ const { texts, unreadable } = (0, _animus_ui_extract_pipeline.outputEvidence)(Object.values(bundle).map((file) => file.type === "chunk" ? {
506
+ fileName: file.fileName,
507
+ code: file.code
508
+ } : {
509
+ fileName: file.fileName,
510
+ bytes: Buffer.from(file.source)
511
+ }), slices);
512
+ if (unreadable !== null) return keep(`${unreadable} holds data the scan cannot read as text, which may name a class`);
513
+ for (const { fileName, text } of texts) for (const slice of slices) if (occurrences(text, node_path.posix.basename(slice)) > (context.manifests.has(fileName) ? occurrences(text, JSON.stringify(slice).slice(1, -1)) : 0)) return keep(`${fileName} names ${slice}, which a replacement would leave it naming`);
514
+ const components = manifest.components;
515
+ const removed = (0, _animus_ui_extract_pipeline.unbundledComponents)(components, (0, _animus_ui_extract_pipeline.shippedClasses)(texts.map(({ text }) => text), Object.values(components).map((component) => component.class_name)), new Set(Object.keys(ctx.options.staticCss?.components ?? {})));
516
+ const classes = new Set(removed.values());
517
+ let characters = 0;
518
+ const renamed = /* @__PURE__ */ new Map();
519
+ return {
520
+ replace(file) {
521
+ const css = Buffer.from(file.source).toString("utf8");
522
+ let pruned;
523
+ try {
524
+ pruned = (0, _animus_ui_extract_pipeline.removeComponentRules)(css, classes, {
525
+ targets: ctx.lcssTargets,
526
+ keepDirSelector: ctx.options.keepDirSelector
527
+ });
528
+ } catch (error) {
529
+ ctx.warn(`[animus-extract] removeUnbundledComponents kept ${file.fileName} whole: Lightning CSS could not read it (${error instanceof Error ? error.message : String(error)})`);
530
+ return null;
531
+ }
532
+ if (pruned === null) return null;
533
+ characters += css.length - pruned.length;
534
+ const extension = node_path.posix.extname(file.fileName);
535
+ const fileName = `${file.fileName.slice(0, -extension.length)}-${(0, _animus_ui_extract_pipeline.contentHash)(pruned).slice(0, 8)}${extension}`;
536
+ context.emit({
537
+ type: "asset",
538
+ fileName,
539
+ name: file.names[0],
540
+ originalFileName: file.originalFileNames[0],
541
+ source: pruned
542
+ });
543
+ renamed.set(file.fileName, fileName);
544
+ return fileName;
545
+ },
546
+ finish() {
547
+ for (const file of Object.values(bundle)) {
548
+ const imported = file.viteMetadata?.importedCss;
549
+ for (const [before, after] of renamed) if (imported?.delete(before)) imported.add(after);
550
+ if (file.type !== "asset" || !context.manifests.has(file.fileName)) continue;
551
+ let text = Buffer.from(file.source).toString("utf8");
552
+ for (const [before, after] of renamed) text = text.replaceAll(JSON.stringify(before).slice(1, -1), JSON.stringify(after).slice(1, -1));
553
+ file.source = text;
554
+ }
555
+ if (ctx.tracing) for (const [id, className] of removed) ctx.trace(`removed ${id} (${className}): no output file includes it`);
556
+ ctx.info(`removed the CSS of ${removed.size} of ${Object.keys(components).length} components no output file includes (${characters} characters of the stylesheet)`);
557
+ }
558
+ };
559
+ }
560
+ /** How many times `needle` occurs in `text`. */
561
+ function occurrences(text, needle) {
562
+ let count = 0;
563
+ for (let at = text.indexOf(needle); at !== -1; at = text.indexOf(needle, at + 1)) count += 1;
564
+ return count;
565
+ }
566
+ /** Why a client build keeps every component, or `null`. */
567
+ function removalBlocker(bundle, otherEnvironments) {
568
+ if (otherEnvironments.length > 0) return `this build also bundles ${otherEnvironments.join(", ")}, whose output may render what the client's does not`;
569
+ for (const file of Object.values(bundle)) {
570
+ if (file.type !== "chunk") continue;
571
+ const external = [...file.imports, ...file.dynamicImports].find((imported) => !(imported in bundle));
572
+ if (external !== void 0) return `the build leaves ${external} external, which may render components no output file includes`;
573
+ }
574
+ return null;
575
+ }
576
+ //#endregion
185
577
  //#region src/config.ts
186
578
  function applyResolvedConfig(ctx, config) {
187
579
  ctx.isProd = config.command === "build";
@@ -218,51 +610,15 @@ function applyResolvedConfig(ctx, config) {
218
610
  pattern: entry.find,
219
611
  target: entry.replacement,
220
612
  kind: "prefix"
221
- });
222
- }
223
- const built = (0, _animus_ui_extract_pipeline.buildPathAliasesJson)(pairs, ctx.rootDir);
224
- if (built) {
225
- ctx.pathAliasesJson = built.json;
226
- ctx.log(`Path aliases forwarded: ${built.count} entries`);
227
- }
228
- ctx.createSession();
229
- }
230
- //#endregion
231
- //#region src/constants.ts
232
- /**
233
- * The namespace every virtual module here lives under; the `transform` guard
234
- * matches on this same string so a rename cannot strand it.
235
- */
236
- const VIRTUAL_PREFIX = "virtual:animus/";
237
- const VIRTUAL_CSS_ID = `${VIRTUAL_PREFIX}styles.css`;
238
- const RESOLVED_CSS_ID = `\0${VIRTUAL_CSS_ID}`;
239
- /** A slice of the stylesheet, which `stylesheets: 'split'` builds emit as
240
- * a file of its own. */
241
- const sliceStylesheetId = (slice) => `${VIRTUAL_PREFIX}styles.${slice}.css`;
242
- const RESOLVED_SLICE_IDS = new Map(_animus_ui_extract_pipeline.STYLESHEET_SLICES.map((slice) => [`\0${sliceStylesheetId(slice)}`, slice]));
243
- /**
244
- * What the stylesheet module holds in a split build: one rule that keeps
245
- * the stylesheet's place in Vite's CSS, where the page's links to the
246
- * slices then go. Its own property, so no minifier merges or drops it.
247
- */
248
- const SPLIT_STYLESHEET_MARKER = ".animus-split-stylesheet{--animus-split-stylesheet:1}";
249
- /** The marker as a built stylesheet spells it, minified or not. */
250
- const SPLIT_STYLESHEET_MARKER_RE = /\.animus-split-stylesheet\s*\{\s*--animus-split-stylesheet\s*:\s*1\s*;?\s*\}\n?/;
251
- const VIRTUAL_COMPONENTS_ID = `${VIRTUAL_PREFIX}components.js`;
252
- const RESOLVED_COMPONENTS_ID = `\0${VIRTUAL_COMPONENTS_ID}`;
253
- const VIRTUAL_BRIDGE_ID = `${VIRTUAL_PREFIX}hmr-bridge.js`;
254
- const RESOLVED_BRIDGE_ID = `\0${VIRTUAL_BRIDGE_ID}`;
255
- /**
256
- * Vite reserves `/@id/` for module ids that are not file paths and strips the
257
- * prefix before `resolveId`, so the unprefixed specifier travels here.
258
- */
259
- const BRIDGE_SCRIPT_SRC = `/@id/${VIRTUAL_BRIDGE_ID}`;
260
- const VIRTUAL_SYSTEM_PROPS_ID = `${VIRTUAL_PREFIX}system-props`;
261
- const RESOLVED_SYSTEM_PROPS_ID = `\0${VIRTUAL_SYSTEM_PROPS_ID}`;
262
- /** The faces this build preloads, with the URL each file is preloaded at:
263
- * the module every host serves under this id. */
264
- const VIRTUAL_FONT_PRELOADS_ID = _animus_ui_extract_session.FONT_PRELOADS_MODULE_ID;
265
- const RESOLVED_FONT_PRELOADS_ID = `\0${VIRTUAL_FONT_PRELOADS_ID}`;
613
+ });
614
+ }
615
+ const built = (0, _animus_ui_extract_pipeline.buildPathAliasesJson)(pairs, ctx.rootDir);
616
+ if (built) {
617
+ ctx.pathAliasesJson = built.json;
618
+ ctx.log(`Path aliases forwarded: ${built.count} entries`);
619
+ }
620
+ ctx.createSession();
621
+ }
266
622
  //#endregion
267
623
  //#region src/hot-update-events.ts
268
624
  /**
@@ -1368,255 +1724,6 @@ function invalidateStaleModules(ctx, environment, modules, analyzed) {
1368
1724
  }
1369
1725
  }
1370
1726
  //#endregion
1371
- //#region src/index-html.ts
1372
- const LINK_TAG_RE = /<link\b[^>]*>/gi;
1373
- /** Marks the preloads Animus writes, so another preload of the same file
1374
- * can be told from Animus's own. */
1375
- const ANIMUS_PRELOAD_MARKER = "data-animus-preload";
1376
- const MARKER_RE = /\sdata-animus-preload(?=[\s=/>])/i;
1377
- const REL_RE = /\srel\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
1378
- const HREF_RE = /\shref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
1379
- function attribute(tag, re) {
1380
- const match = re.exec(tag);
1381
- return match ? match[1] ?? match[2] ?? match[3] : void 0;
1382
- }
1383
- /** Each `<link rel="preload">` in `html`, with its href. */
1384
- function* preloadLinks(html) {
1385
- for (const [tag] of html.matchAll(LINK_TAG_RE)) {
1386
- const rel = attribute(tag, REL_RE)?.toLowerCase().split(/\s+/) ?? [];
1387
- const href = attribute(tag, HREF_RE);
1388
- if (rel.includes("preload") && href !== void 0) yield [tag, href];
1389
- }
1390
- }
1391
- const ENTITY_RE = /&(?:#x([0-9a-f]+)|#(\d+)|(amp|lt|gt|quot|apos));/gi;
1392
- const NAMED_ENTITIES = /* @__PURE__ */ new Map([
1393
- ["amp", "&"],
1394
- ["lt", "<"],
1395
- ["gt", ">"],
1396
- ["quot", "\""],
1397
- ["apos", "'"]
1398
- ]);
1399
- /** An attribute value as the browser reads it: character references decoded. */
1400
- function attributeText(value) {
1401
- return value.replace(ENTITY_RE, (_, hex, decimal, named) => named === void 0 ? String.fromCodePoint(Number.parseInt(hex ?? decimal, hex ? 16 : 10)) : NAMED_ENTITIES.get(named.toLowerCase()));
1402
- }
1403
- /**
1404
- * The hrefs a finished document preloads both through Animus's tag and
1405
- * through another, which the page wrote or a hook running after Animus's
1406
- * added: the browser may then fetch the file twice. Each href is compared
1407
- * as the browser resolves it against `pageUrl`, the URL the page is served
1408
- * at, and reported as Animus wrote it.
1409
- */
1410
- function duplicatedFontPreloads(html, pageUrl) {
1411
- const resolved = (href) => {
1412
- try {
1413
- return new URL(attributeText(href), pageUrl).href;
1414
- } catch {
1415
- return href;
1416
- }
1417
- };
1418
- const ours = /* @__PURE__ */ new Map();
1419
- const others = /* @__PURE__ */ new Set();
1420
- for (const [tag, href] of preloadLinks(html)) if (MARKER_RE.test(tag)) ours.set(resolved(href), href);
1421
- else others.add(resolved(href));
1422
- return [...ours].filter(([url]) => others.has(url)).map(([, href]) => href);
1423
- }
1424
- /** The hrefs the document already preloads. */
1425
- function preloadedHrefs(html) {
1426
- return new Set([...preloadLinks(html)].map(([, href]) => href));
1427
- }
1428
- /** The head elements Animus's tags go after (the charset and viewport
1429
- * metas), the stylesheets they go before, and what holds neither. */
1430
- const HEAD_SCAN_RE = /<!--[\s\S]*?-->|<script\b[\s\S]*?<\/script\s*>|<\/head\s*>|<style\b|<(?:meta|link)\b[^>]*>/gi;
1431
- /** Where Animus's head tags go: after the head's charset and viewport
1432
- * metas, so they cannot push the charset past the 1024 bytes a browser
1433
- * sniffs for it, and before its first stylesheet; -1 without a `<head>`. */
1434
- function headTagOffset(html) {
1435
- const open = /<head\b[^>]*>/i.exec(html);
1436
- if (open === null) return -1;
1437
- const start = open.index + open[0].length;
1438
- let offset = start;
1439
- for (const match of html.slice(start).matchAll(HEAD_SCAN_RE)) {
1440
- const [element] = match;
1441
- const lower = element.toLowerCase();
1442
- if (lower.startsWith("</head") || lower.startsWith("<style")) break;
1443
- if (lower.startsWith("<link")) {
1444
- if ((attribute(element, REL_RE)?.toLowerCase().split(/\s+/))?.includes("stylesheet")) break;
1445
- } else if (lower.startsWith("<meta") && (/\scharset\s*=/i.test(element) || /\shttp-equiv\s*=\s*["']?content-type\b/i.test(element) || /\sname\s*=\s*["']?viewport\b/i.test(element))) offset = start + match.index + element.length;
1446
- }
1447
- return offset;
1448
- }
1449
- /** As `escape-html` escapes, which Vite applies to attribute values. */
1450
- function escapeHtml(value) {
1451
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1452
- }
1453
- /** A tag as Vite serializes it, `children` as written, except that an
1454
- * undefined attribute is left out rather than written "undefined". */
1455
- function serializeTag({ tag, attrs = {}, children }) {
1456
- const serialized = Object.entries(attrs).map(([key, value]) => {
1457
- if (value === true) return ` ${key}`;
1458
- if (value === false || value === void 0) return "";
1459
- return ` ${key}="${escapeHtml(value)}"`;
1460
- }).join("");
1461
- if (tag === "link" || tag === "meta" || tag === "base") return `<${tag}${serialized}>`;
1462
- return `<${tag}${serialized}>${Array.isArray(children) ? "" : children ?? ""}</${tag}>`;
1463
- }
1464
- /**
1465
- * The hook's result: the tags written after the charset and viewport metas,
1466
- * each on its own line indented as the element it follows, so the charset
1467
- * stays in the first 1024 bytes however long the tags are; as `head-prepend`
1468
- * tags for a document without a `<head>`.
1469
- */
1470
- function injectHeadTags(html, tags) {
1471
- const offset = headTagOffset(html);
1472
- if (offset === -1) return tags;
1473
- if (tags.length === 0) return html;
1474
- const lineStart = html.lastIndexOf("\n", offset - 1) + 1;
1475
- const indent = /^[ \t]*/.exec(html.slice(lineStart))[0];
1476
- const inserted = tags.map((tag) => `\n${indent}${serializeTag(tag)}`).join("");
1477
- return html.slice(0, offset) + inserted + html.slice(offset);
1478
- }
1479
- /** The preloads for the faces that ask for one, each marked as Animus's;
1480
- * one the document already preloads is left out. */
1481
- function fontPreloadTags(ctx, html) {
1482
- if (!ctx.served?.fontPreloads.length) return [];
1483
- const existing = preloadedHrefs(html);
1484
- return ctx.fontPreloads.filter(({ href }) => !existing.has(href)).map(({ href, type, fetchpriority }) => {
1485
- const attrs = {
1486
- rel: "preload",
1487
- href,
1488
- as: "font"
1489
- };
1490
- if (type !== void 0) attrs.type = type;
1491
- if (fetchpriority !== void 0) attrs.fetchpriority = fetchpriority;
1492
- attrs.crossorigin = true;
1493
- attrs[ANIMUS_PRELOAD_MARKER] = true;
1494
- return {
1495
- tag: "link",
1496
- attrs,
1497
- injectTo: "head-prepend"
1498
- };
1499
- });
1500
- }
1501
- /**
1502
- * Animus's head tags in document order: the bootstrap script, so it runs
1503
- * before first paint and ahead of every stylesheet, the layer declaration,
1504
- * the font preloads, then the development bridge.
1505
- */
1506
- function buildIndexHtmlTags(ctx, html = "") {
1507
- const tags = [];
1508
- const bootstrap = ctx.options.appearanceBootstrap;
1509
- if (bootstrap?.code) tags.push({
1510
- tag: "script",
1511
- attrs: { "data-animus-bootstrap": "" },
1512
- children: bootstrap.code,
1513
- injectTo: "head-prepend"
1514
- });
1515
- if (ctx.layerDeclaration) tags.push({
1516
- tag: "style",
1517
- attrs: { "data-animus-layers": "" },
1518
- children: ctx.layerDeclaration,
1519
- injectTo: "head-prepend"
1520
- });
1521
- tags.push(...fontPreloadTags(ctx, html));
1522
- if (!ctx.isProd) tags.push({
1523
- tag: "script",
1524
- attrs: {
1525
- type: "module",
1526
- src: BRIDGE_SCRIPT_SRC,
1527
- "data-animus-bridge": ""
1528
- },
1529
- injectTo: "head-prepend"
1530
- });
1531
- return tags;
1532
- }
1533
- /**
1534
- * A built file's URL in a page, as Vite writes its own links: the host's
1535
- * `renderBuiltUrl` first, then relative to the page under a relative base,
1536
- * else under the base.
1537
- */
1538
- function builtUrl(ctx, page, fileName) {
1539
- let relative = ctx.base === "" || ctx.base === "./";
1540
- const rendered = ctx.renderBuiltUrl?.(fileName, {
1541
- hostId: page,
1542
- hostType: "html",
1543
- type: "asset",
1544
- ssr: false
1545
- });
1546
- if (rendered instanceof Object) {
1547
- if (rendered.runtime !== void 0) throw new Error(`[animus-extract] renderBuiltUrl returned { runtime } for ${fileName}, which a page's <link> cannot load`);
1548
- if (rendered.relative !== void 0) relative = rendered.relative;
1549
- } else if (rendered) return rendered;
1550
- if (relative) return `${path.posix.join(path.posix.relative(page, "").slice(0, -2), "./")}${fileName}`;
1551
- return `${decodeURI(ctx.base).replace(/\/$/, "")}/${fileName}`;
1552
- }
1553
- /** `tag`, a stylesheet link, linking `href` instead, with `attributes`. */
1554
- function linkingInstead(tag, href, attributes = "") {
1555
- return tag.replace(HREF_RE, ` href="${escapeHtml(href)}"`).replace(/\s*\/?>$/, `${attributes}>`);
1556
- }
1557
- /**
1558
- * Puts a split build's slices where the single stylesheet would have been.
1559
- * The stylesheet module held only a marker rule, so it kept its place in
1560
- * Vite's CSS: the file holding the marker is cut there, and each page's
1561
- * link to it becomes the file's CSS before the marker, the slices in
1562
- * order, then its CSS after the marker, in a file of its own when CSS
1563
- * precedes the marker too. A carrier left empty is kept, unlinked, for a
1564
- * lazy chunk that names it. The build fails when no page links the
1565
- * carrier: its stylesheet loads with a lazy chunk, or no page is built.
1566
- */
1567
- function placeStylesheetSlices(ctx, bundle, sliceIds, emitAsset) {
1568
- const fileOf = /* @__PURE__ */ new Map();
1569
- const pages = [];
1570
- const carriers = [];
1571
- for (const file of Object.values(bundle)) {
1572
- if (file.type !== "asset") continue;
1573
- if (file.fileName.endsWith(".html")) pages.push(file);
1574
- if (file.fileName.endsWith(".css") && SPLIT_STYLESHEET_MARKER_RE.test(String(file.source))) carriers.push(file);
1575
- const id = sliceIds.find((slice) => file.originalFileNames.some((name) => name.endsWith(slice)));
1576
- if (id !== void 0) fileOf.set(id, file.fileName);
1577
- }
1578
- const slices = sliceIds.map((id) => {
1579
- const fileName = fileOf.get(id);
1580
- if (fileName === void 0) throw new Error(`[animus-extract] stylesheets: 'split' emitted ${id}, and the bundle holds no file for it, so the stylesheet would load without it.`);
1581
- return fileName;
1582
- });
1583
- if (carriers.length === 0) {
1584
- for (const fileName of slices) delete bundle[fileName];
1585
- return;
1586
- }
1587
- if (carriers.length > 1) throw new Error(`[animus-extract] stylesheets: 'split' found Animus's stylesheet in ${carriers.map((file) => file.fileName).join(" and ")}, and places it once.`);
1588
- const carrier = carriers[0];
1589
- const text = String(carrier.source);
1590
- const marker = SPLIT_STYLESHEET_MARKER_RE.exec(text);
1591
- const before = text.slice(0, marker.index);
1592
- const after = text.slice(marker.index + marker[0].length);
1593
- const precedes = before.trim() !== "";
1594
- const follows = after.trim() !== "";
1595
- carrier.source = precedes ? before : after;
1596
- const rest = precedes && follows ? emitAsset(carrier.names[0] ?? "index.css", after) : null;
1597
- const sequence = [
1598
- ...precedes ? [null] : [],
1599
- ...slices,
1600
- ...follows ? [rest] : []
1601
- ];
1602
- let linked = false;
1603
- for (const page of pages) {
1604
- const html = Buffer.from(page.source).toString("utf8");
1605
- const carrierUrl = builtUrl(ctx, page.fileName, carrier.fileName);
1606
- page.source = html.replace(LINK_TAG_RE, (tag, offset) => {
1607
- const rel = attribute(tag, REL_RE)?.toLowerCase().split(/\s+/) ?? [];
1608
- const href = attribute(tag, HREF_RE);
1609
- if (!rel.includes("stylesheet") || href === void 0) return tag;
1610
- if (attributeText(href) !== carrierUrl) return tag;
1611
- linked = true;
1612
- const lineStart = html.lastIndexOf("\n", offset - 1) + 1;
1613
- const indent = /^[ \t]*/.exec(html.slice(lineStart))[0];
1614
- return sequence.map((fileName) => fileName === null ? tag : linkingInstead(tag, builtUrl(ctx, page.fileName, fileName), fileName === rest ? "" : " data-animus-stylesheet")).join(`\n${indent}`);
1615
- });
1616
- }
1617
- if (!linked) throw new Error(`[animus-extract] stylesheets: 'split' links the stylesheet's files where a page links ${carrier.fileName}, which holds Animus's stylesheet, and no page links it: it loads with a lazy chunk, or the build has no HTML page. Import Animus's components from the page's entry, or leave \`stylesheets\` unset for one sheet.`);
1618
- }
1619
- //#endregion
1620
1727
  //#region src/quarantine-plugin.ts
1621
1728
  /**
1622
1729
  * layergate's Vite plugin for the `quarantine` option, loaded on first use:
@@ -1836,7 +1943,8 @@ function animusExtract(options) {
1836
1943
  (0, _animus_ui_extract_pipeline.assertKnownOptionKeys)(options, [
1837
1944
  "verify",
1838
1945
  "appearanceBootstrap",
1839
- "quarantine"
1946
+ "quarantine",
1947
+ "removeUnbundledComponents"
1840
1948
  ], [{
1841
1949
  key: "root",
1842
1950
  reason: "the Vite driver derives its root from the resolved Vite config"
@@ -1844,6 +1952,8 @@ function animusExtract(options) {
1844
1952
  onUnknownKey: "warn",
1845
1953
  warn: (message) => console.warn(`[animus-extract] ${message}`)
1846
1954
  });
1955
+ const removes = options.removeUnbundledComponents === true;
1956
+ if (removes && options.stylesheets !== "split") throw new _animus_ui_extract_pipeline.AnimusConfigError("removeUnbundledComponents needs stylesheets: 'split': it removes CSS from the stylesheet's own files, which only a split build emits after its scripts.");
1847
1957
  const vendor = options.quarantine ? quarantinePlugin(options.quarantine, options.layers) : null;
1848
1958
  const ctx = new PluginContext(vendor ? {
1849
1959
  ...options,
@@ -1855,6 +1965,8 @@ function animusExtract(options) {
1855
1965
  const layergate = await vendor?.plugin();
1856
1966
  return layergate ? await layergate.stylesheetHead.call(context, RESOLVED_CSS_ID) + sheet : sheet;
1857
1967
  }
1968
+ let otherEnvironments = [];
1969
+ let manifests = /* @__PURE__ */ new Set();
1858
1970
  let slices = /* @__PURE__ */ new Map();
1859
1971
  let linkedSlices = [];
1860
1972
  return {
@@ -1888,6 +2000,9 @@ function animusExtract(options) {
1888
2000
  },
1889
2001
  async configResolved(config) {
1890
2002
  applyResolvedConfig(ctx, config);
2003
+ otherEnvironments = config.builder ? Object.keys(config.environments).filter((name) => name !== "client") : [];
2004
+ const { manifest, ssrManifest } = config.build;
2005
+ manifests = /* @__PURE__ */ new Set([...manifest === true ? [".vite/manifest.json"] : manifest ? [manifest] : [], ...ssrManifest === true ? [".vite/ssr-manifest.json"] : ssrManifest ? [ssrManifest] : []]);
1891
2006
  (await vendor?.plugin())?.configResolved.call(this, config);
1892
2007
  },
1893
2008
  async buildEnd() {
@@ -1957,11 +2072,20 @@ function animusExtract(options) {
1957
2072
  generateBundle: {
1958
2073
  order: "post",
1959
2074
  handler(_output, bundle) {
1960
- if (linkedSlices.length > 0 && isClient(this)) placeStylesheetSlices(ctx, bundle, linkedSlices, (name, source) => this.getFileName(this.emitFile({
2075
+ const emitAsset = (name, source) => this.getFileName(this.emitFile({
1961
2076
  type: "asset",
1962
2077
  name,
1963
2078
  source
1964
- })));
2079
+ }));
2080
+ if (linkedSlices.length > 0 && isClient(this)) {
2081
+ const removal = removes ? bundleRemoval(ctx, bundle, linkedSlices, {
2082
+ otherEnvironments,
2083
+ manifests,
2084
+ emit: (asset) => this.emitFile(asset)
2085
+ }) : null;
2086
+ placeStylesheetSlices(ctx, bundle, linkedSlices, emitAsset, removal ? (file) => removal.replace(file) : void 0);
2087
+ removal?.finish();
2088
+ }
1965
2089
  if (ctx.servedDirSelector && !ctx.warnedLoweredDir && isClient(this)) {
1966
2090
  const sheets = Object.values(bundle).flatMap((file) => file.type === "asset" && file.fileName.endsWith(".css") ? [file.source] : []);
1967
2091
  if (sheets.length > 0 && sheets.every((source) => !Buffer.from(source).toString("utf8").includes(":dir("))) {
package/dist/index.d.ts CHANGED
@@ -44,6 +44,16 @@ export interface AnimusExtractOptions {
44
44
  * hash. Needs a page Vite builds from HTML to link the stylesheet; the
45
45
  * dev server keeps one sheet. */
46
46
  stylesheets?: StylesheetDelivery;
47
+ /** With `stylesheets: 'split'`, removes from the stylesheet's files the
48
+ * CSS of every component whose definition no output file includes, so it
49
+ * can never render; a component `staticCss` declares, and a parent of
50
+ * one kept, stay. A build that bundles another environment beside the
51
+ * client, or leaves a module external, keeps every component. Scripts a
52
+ * plugin emits after Animus's `generateBundle` are not seen, and a
53
+ * stylesheet shared with code outside the build (micro-frontends, module
54
+ * federation) is unsupported with this on. Off by default, and in
55
+ * development. */
56
+ removeUnbundledComponents?: boolean;
47
57
  /**
48
58
  * Substrings, or globs when `*`/`?` present. Replaces the defaults `dist`,
49
59
  * `.test.`, `.spec.`; `node_modules`, `.next`, `.animus` always apply.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EACV,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAEhB,eAAe,EACf,kBAAkB,EAEnB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAe,MAAM,EAAwB,MAAM,MAAM,CAAC;AAEtE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,YAAY,EACV,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,6BAA6B,CAAC;AACrC;sEACsE;AACtE,YAAY,EACV,WAAW,EACX,iBAAiB,GAClB,MAAM,6BAA6B,CAAC;AA+CrC,MAAM,WAAW,oBAAoB;IACnC,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf;;;0DAGsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC7C;;;;yBAIqB;IACrB,YAAY,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAClC;;;;;;;sCAOkC;IAClC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;+CAE2C;IAC3C,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC5B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B;;;;yDAIqD;IACrD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;0DAMsD;IACtD,eAAe,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC;4DACwD;IACxD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sEAAsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IACpC;;;;;0DAKsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;gBAEY;IACZ,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,IAAI,CAAC;IACd;;;OAGG;IACH,mBAAmB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,uBAAuB,CAAC;CACtC;AAuBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CA8RnE;eAEc,aAAa"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EACV,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAEhB,eAAe,EACf,kBAAkB,EAEnB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAe,MAAM,EAAwB,MAAM,MAAM,CAAC;AAEtE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,YAAY,EACV,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,6BAA6B,CAAC;AACrC;sEACsE;AACtE,YAAY,EACV,WAAW,EACX,iBAAiB,GAClB,MAAM,6BAA6B,CAAC;AA+CrC,MAAM,WAAW,oBAAoB;IACnC,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf;;;0DAGsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC7C;;;;yBAIqB;IACrB,YAAY,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAClC;;;;;;;sCAOkC;IAClC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC;;;;;;;;uBAQmB;IACnB,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;+CAE2C;IAC3C,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC5B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B;;;;yDAIqD;IACrD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;0DAMsD;IACtD,eAAe,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC;4DACwD;IACxD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sEAAsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,GAAG,YAAY,CAAC;IACpC;;;;;0DAKsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;gBAEY;IACZ,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,IAAI,CAAC;IACd;;;OAGG;IACH,mBAAmB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,uBAAuB,CAAC;CACtC;AAuBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CA8UnE;eAEc,aAAa"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animus-ui/vite-plugin",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "description": "Animus static CSS extraction Vite plugin",
5
5
  "keywords": [
6
6
  "animus",
@@ -43,16 +43,16 @@
43
43
  "compile": "bun check"
44
44
  },
45
45
  "dependencies": {
46
- "@animus-ui/extract": "0.1.49",
46
+ "@animus-ui/extract": "0.1.50",
47
47
  "browserslist": "^4.28.8",
48
48
  "lightningcss": "^1.33.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@animus-ui/assertions": "0.1.49",
52
- "@animus-ui/layergate": "0.1.49"
51
+ "@animus-ui/assertions": "0.1.50",
52
+ "@animus-ui/layergate": "0.1.50"
53
53
  },
54
54
  "peerDependencies": {
55
- "@animus-ui/layergate": "0.1.49",
55
+ "@animus-ui/layergate": "0.1.50",
56
56
  "vite": ">=8 <9"
57
57
  },
58
58
  "peerDependenciesMeta": {