@stealthscale/vite-plugin-product 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,12 +1,246 @@
1
- import { dirname, join, posix, relative, resolve, sep } from "node:path";
2
- import { normalizePath } from "vite";
3
- import { lineOf, resolveProduct } from "@stealthscale/sdk-core";
4
- import { dependencies, importer, literal, manifestAt, owning, quoted, text } from "@stealthscale/vite-plugin-base";
1
+ import { createRequire } from "node:module";
2
+ import { build, normalizePath, parseSync } from "vite";
3
+ import { PROVIDED_FILE, RELEASE_FILE, lineOf, releaseProblems, resolveProduct } from "@stealthscale/sdk-core";
4
+ import { dependencies, exportTarget, importer, literal, manifestAt, owning, packageAt, quoted, text } from "@stealthscale/vite-plugin-base";
5
5
  import { LOCALES, cataloguesOf } from "@stealthscale/vite-plugin-i18n";
6
+ import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
6
7
  import { validateHotkey } from "@tanstack/hotkeys";
7
8
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
9
+ import { createHash } from "node:crypto";
10
+ //#region src/page.ts
11
+ /**
12
+ * Writes the document and the entry of a page the plugin serves at `index.html`: a product's page
13
+ * or a plugin's standalone page.
14
+ */
15
+ /**
16
+ * The page's document, from the project root.
17
+ */
18
+ const DOCUMENT = "index.html";
19
+ /**
20
+ * The language of a document whose page states none.
21
+ */
22
+ const LANGUAGE = "en-US";
23
+ /**
24
+ * Returns the path of the page's document for a specifier that names it, and undefined for any
25
+ * other specifier.
26
+ *
27
+ * @param root - The project root.
28
+ * @param id - The specifier: `index.html`, `/index.html` or the document's path name the document.
29
+ */
30
+ function documentIdOf(root, id) {
31
+ const document = normalizePath(join(root, DOCUMENT));
32
+ return id === DOCUMENT || id === `/${DOCUMENT}` || id === document ? document : void 0;
33
+ }
34
+ /**
35
+ * Returns the specifier of a module by its path from the project root, such as `/src/glyphs.tsx`.
36
+ */
37
+ function rootedOf(path) {
38
+ return posix.join("/", normalizePath(path));
39
+ }
40
+ /**
41
+ * Returns the source of a page's document: its language and title, an element with the id `root`,
42
+ * and the entry.
43
+ */
44
+ function documentSource({ entry, language, title }) {
45
+ return [
46
+ "<!doctype html>",
47
+ `<html lang=${quoted(language)}>`,
48
+ " <head>",
49
+ " <meta charset=\"utf-8\" />",
50
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />",
51
+ ` <title>${title}</title>`,
52
+ " </head>",
53
+ " <body>",
54
+ " <div id=\"root\"></div>",
55
+ ` <script type="module" src="/@id/${entry}"><\/script>`,
56
+ " </body>",
57
+ "</html>",
58
+ ""
59
+ ].join("\n");
60
+ }
61
+ /**
62
+ * Returns the source of a page's entry: the stylesheet and the static imports, every other module
63
+ * through `import()`, and a call of the render function with the members.
64
+ *
65
+ * @remarks
66
+ * The entry imports the stylesheet and modules without components statically, and the page's
67
+ * other modules through `import()`. The bundled dev server installs the React refresh runtime in
68
+ * the entry's own body, and a chunk the entry imports statically runs before that body, so a
69
+ * component in a chunk the bundler shares between the entry and a lazy module would run before
70
+ * the runtime and throw. Behind `import()`, every component runs after it. A module the entry
71
+ * alone imports statically builds into the entry's chunk, so it adds no chunk of its own. The
72
+ * entry awaits nothing at its top level, because a module with a top-level await makes the
73
+ * bundler move every module it shares with a lazy module into a chunk of its own.
74
+ * @param render - The name of the render function, which the first module exports.
75
+ * @param modules - The modules the entry imports through `import()`, the render function's first.
76
+ * @param members - The members of the object the render function receives.
77
+ * @param statics - The modules without components the entry imports statically.
78
+ */
79
+ function entrySource(render, modules, members, statics = []) {
80
+ return [
81
+ "import \"@stealthscale/theme/styles.css\";",
82
+ ...statics.map(({ from, names }) => `import ${names} from ${quoted(from)};`),
83
+ "",
84
+ "async function start() {",
85
+ ` const [${modules.map(({ names }) => names).join(", ")}] = await Promise.all([`,
86
+ ...modules.map(({ from }) => ` import(${quoted(from)}),`),
87
+ " ]);",
88
+ "",
89
+ ` await ${render}({ ${members.join(", ")} });`,
90
+ "}",
91
+ "",
92
+ "void start();",
93
+ ""
94
+ ].join("\n");
95
+ }
96
+ //#endregion
97
+ //#region src/app.ts
98
+ /**
99
+ * The specifier of the page's entry.
100
+ */
101
+ const ENTRY$1 = "virtual:product-app";
102
+ /**
103
+ * The resolved id of the page's entry, behind the NUL that marks it as generated.
104
+ */
105
+ const RESOLVED_ENTRY$1 = `\0${ENTRY$1}`;
106
+ /**
107
+ * The document's title while no product resolved.
108
+ */
109
+ const UNTITLED = "Product";
110
+ /**
111
+ * The address a dev server's fallback rewrites every address without a file to.
112
+ */
113
+ const INDEX = "/index.html";
114
+ /**
115
+ * Returns the router's base path for the build's base: the base where it is a path other than the
116
+ * site's root, and undefined for any other base.
117
+ *
118
+ * @remarks
119
+ * A relative base and a URL state where the files are served from, and the address of the page
120
+ * is unknown to the build, so they give no base path.
121
+ */
122
+ function basepathOf(base) {
123
+ return base.startsWith("/") && base !== "/" ? base : void 0;
124
+ }
125
+ /**
126
+ * Returns the source of the page's document, in the product's first locale and titled with its id.
127
+ *
128
+ * @remarks
129
+ * `HostProvider` replaces the title with the product's name once the product renders.
130
+ */
131
+ function documentOf$1(build) {
132
+ return documentSource({
133
+ entry: ENTRY$1,
134
+ language: build.product?.locales?.[0] ?? "en-US",
135
+ title: build.product?.productId ?? UNTITLED
136
+ });
137
+ }
138
+ /**
139
+ * Returns the source of the page's entry, which renders the product with `renderProduct` over
140
+ * `virtual:product`, `virtual:i18n` and `virtual:stealth-themes`.
141
+ *
142
+ * @remarks
143
+ * The entry imports `virtual:stealth-themes` statically. The module is a list of names and
144
+ * renders nothing, and a module behind `import()` would build into a chunk of its own. A page
145
+ * that loads releases renders with `renderDeployed` from `@stealthscale/sdk-host/releases` and
146
+ * passes the definition, so the loader and the resolver build into no other product's page.
147
+ */
148
+ function entryOf$2(page, build) {
149
+ const basepath = basepathOf(build.base);
150
+ const { glyphs, runtime = false } = page;
151
+ const render = runtime ? "renderDeployed" : "renderProduct";
152
+ return entrySource(render, [
153
+ {
154
+ from: runtime ? "@stealthscale/sdk-host/releases" : "@stealthscale/sdk-host/app",
155
+ names: `{ ${render} }`
156
+ },
157
+ {
158
+ from: "virtual:i18n",
159
+ names: "{ catalogues }"
160
+ },
161
+ {
162
+ from: "virtual:product",
163
+ names: runtime ? "{ definition, product }" : "{ product }"
164
+ },
165
+ ...glyphs === void 0 ? [] : [{
166
+ from: rootedOf(glyphs),
167
+ names: "{ glyphs }"
168
+ }]
169
+ ], [
170
+ ...basepath === void 0 ? [] : [`basepath: ${quoted(basepath)}`],
171
+ "catalogues",
172
+ ...runtime ? ["definition"] : [],
173
+ ...glyphs === void 0 ? [] : ["glyphs"],
174
+ "product",
175
+ "themes"
176
+ ], [{
177
+ from: "virtual:stealth-themes",
178
+ names: "{ themes }"
179
+ }]);
180
+ }
181
+ /**
182
+ * Returns the id the page's document or its entry resolves to, and undefined for any other
183
+ * specifier.
184
+ *
185
+ * @param root - The project root.
186
+ * @param id - The specifier.
187
+ */
188
+ function appIdOf(root, id) {
189
+ if (id === ENTRY$1 || id === `/@id/${ENTRY$1}`) return RESOLVED_ENTRY$1;
190
+ return documentIdOf(root, id);
191
+ }
192
+ /**
193
+ * Returns the source of the page's document or its entry, and undefined for any other id.
194
+ *
195
+ * @param root - The project root.
196
+ * @param page - The page's glyphs, and whether the page loads releases at run time.
197
+ * @param build - The build's base and the resolved product.
198
+ * @param id - The resolved id.
199
+ */
200
+ function appSourceOf(root, page, build, id) {
201
+ if (id === RESOLVED_ENTRY$1) return entryOf$2(page, build);
202
+ return documentIdOf(root, id) === void 0 ? void 0 : documentOf$1(build);
203
+ }
204
+ /**
205
+ * Returns the hook that adds the middleware serving the page's document, and undefined under a
206
+ * server that bundles, whose bundle contains the document.
207
+ *
208
+ * @remarks
209
+ * A server that serves one module per file reads `index.html` from disk and finds none. Vite runs
210
+ * the middleware after its fallback has rewritten every address without a file to `/index.html`,
211
+ * and before its own document middleware. The middleware reads the build when a page asks, so it
212
+ * serves the product's latest locale and id.
213
+ * @param server - The dev server.
214
+ * @param build - The build's base and the resolved product.
215
+ */
216
+ function documentHookOf(server, build) {
217
+ /**
218
+ * Sends the page's document after every plugin's transforms of a document, and passes a failed
219
+ * transform to the next middleware.
220
+ */
221
+ const sent = async (request, response, next) => {
222
+ try {
223
+ const html = await server.transformIndexHtml(INDEX, documentOf$1(build), request.originalUrl);
224
+ response.setHeader("Content-Type", "text/html");
225
+ response.end(html);
226
+ } catch (error) {
227
+ next(error);
228
+ }
229
+ };
230
+ return server.config.experimental.bundledDev === true ? void 0 : () => {
231
+ server.middlewares.use((request, response, next) => {
232
+ if (request.url === INDEX) sent(request, response, next);
233
+ else next();
234
+ });
235
+ };
236
+ }
237
+ //#endregion
8
238
  //#region src/catalogues.ts
9
239
  /**
240
+ * Directory of the build's output the catalogues are written to.
241
+ */
242
+ const CATALOGUES = ".product";
243
+ /**
10
244
  * Returns true where a value is an object whose members can be read by name.
11
245
  */
12
246
  function isRecord$1(value) {
@@ -129,47 +363,238 @@ function cataloguesFor(product, api) {
129
363
  }
130
364
  };
131
365
  }
366
+ /**
367
+ * Returns the catalogue files of a client build, and none under a dev server or for another
368
+ * environment.
369
+ *
370
+ * @param state - The command, the resolved product and the catalogue plugin's api.
371
+ * @param consumer - The consumer of the environment whose bundle is written.
372
+ * @throws {@link Error} When a catalogue cannot be read for a description.
373
+ */
374
+ function catalogueFiles(state, consumer) {
375
+ const { command, product, words } = state;
376
+ if (consumer !== "client" || command !== "build" || product === void 0 || words === void 0) return [];
377
+ return Object.entries(cataloguesFor(product, words)).map(([name, catalogue]) => ({
378
+ fileName: `${CATALOGUES}/${name}.json`,
379
+ source: `${JSON.stringify(catalogue, null, 2)}\n`,
380
+ type: "asset"
381
+ }));
382
+ }
383
+ //#endregion
384
+ //#region src/module.ts
385
+ /**
386
+ * The specifier an application imports the product from.
387
+ */
388
+ const ID = "virtual:product";
389
+ /**
390
+ * The resolved id of the module, behind the NUL that marks it as generated.
391
+ */
392
+ const RESOLVED = `\0${ID}`;
393
+ /**
394
+ * Returns the source of `virtual:product`.
395
+ *
396
+ * @remarks
397
+ * The module imports the definition. The page receives each manifest from it, keyed by plugin id,
398
+ * with its lazy importers and its contract's search validators, and the definition's importers of
399
+ * its own frame and of its sources as `code`. Every member the build resolved is a literal in the
400
+ * module. The module exports the definition as well, which a page that loads releases resolves
401
+ * again with the releases' plugins.
402
+ * @param definition - The definition module's path from the project root, such as
403
+ * `/src/product.ts`.
404
+ * @param product - The resolved product.
405
+ * @throws {@link Error} When a member of the product cannot be written as source, naming its path.
406
+ */
407
+ function moduleOf(definition, product) {
408
+ return [
409
+ `import definition from ${quoted(definition)};`,
410
+ "",
411
+ `const resolved = ${literal(product, "product")};`,
412
+ "",
413
+ "export const product = {",
414
+ " ...resolved,",
415
+ " code: {",
416
+ " frame: typeof definition.frame === \"function\" ? definition.frame : undefined,",
417
+ " sources: definition.sources,",
418
+ " },",
419
+ " manifests: Object.fromEntries(",
420
+ " definition.plugins.map(({ manifest }) => [manifest.contract.pluginId, manifest]),",
421
+ " ),",
422
+ "};",
423
+ "",
424
+ "export { definition };",
425
+ ""
426
+ ].join("\n");
427
+ }
132
428
  //#endregion
133
429
  //#region src/chunks.ts
134
430
  /**
135
- * Names the chunk each lazy module of an installed plugin's web package builds into: one chunk per
136
- * plugin, which every page, extension, command and settings section of the plugin shares.
431
+ * Names the chunk each module of a product builds into: one chunk per plugin, which every page,
432
+ * extension, command and settings section of the plugin shares, and under a build one chunk for the
433
+ * other modules outside the application that the startup or a plugin imports statically.
137
434
  *
138
435
  * @remarks
139
436
  * The product imports each web package's main entry statically, through its definition. The main
140
437
  * entry and every module it imports statically build into the chunks the entry loads, because in
141
- * the plugin's chunk they would make the first load fetch the whole plugin.
438
+ * the plugin's chunk they would make the first load fetch the whole plugin. Groups that place a
439
+ * module by its path alone make rolldown 1.2 build a module the groups leave into the chunk of a
440
+ * lazy module that imports it, and export it from there to the chunks that need it. `import()` of
441
+ * that lazy module then resolves to a namespace with the other module's exports as well. A module
442
+ * outside every web package that one plugin's lazy modules alone import goes into the plugin's
443
+ * chunk. Every other module outside the application that the startup or a plugin's modules import
444
+ * statically goes into the `dependencies` chunk, so a lazy module's chunk contains the lazy
445
+ * module alone. A module that only another `import()` loads, such as a page the application
446
+ * loads on demand, keeps its own chunk, so it does not load with the startup.
142
447
  */
143
448
  /**
144
- * Returns true where an entry of the build imports a module statically, directly or through other
145
- * modules.
146
- *
147
- * @remarks
148
- * The walk follows each module's static importers back towards the entries. A dynamic import
149
- * ends the walk, because a module behind one loads after the entry.
449
+ * Name of the chunk that a module outside the application builds into where the startup or a
450
+ * plugin imports it statically and the other groups leave it.
150
451
  */
151
- function initial(id, chunking) {
452
+ const DEPENDENCIES = "dependencies";
453
+ /**
454
+ * Priority of the plugin chunks' group: above the shared chunk's 3, below the vendor chunk's 5.
455
+ */
456
+ const PRIORITY = 4;
457
+ /**
458
+ * Priority of the dependencies' group: below every other group's, so it claims only the modules
459
+ * they leave.
460
+ */
461
+ const LAST = 1;
462
+ /**
463
+ * Matches a path that starts with or contains a `node_modules` directory.
464
+ */
465
+ const INSTALLED = /(?:^|[\\/])node_modules[\\/]/u;
466
+ /**
467
+ * Returns the id of the plugin whose web package contains a module, or undefined for a module
468
+ * outside every web package.
469
+ */
470
+ function ownerOf(id, packages) {
471
+ return Object.entries(packages).find(([, { directory }]) => id.startsWith(`${directory}${sep}`))?.[0];
472
+ }
473
+ /**
474
+ * Returns true where the walk over a module's static importers meets a module the test accepts,
475
+ * the module itself included.
476
+ */
477
+ function leadsTo(id, chunking, ends) {
152
478
  const seen = /* @__PURE__ */ new Set();
153
479
  const queue = [id];
154
480
  for (let next = queue.shift(); next !== void 0; next = queue.shift()) {
155
481
  const info = seen.has(next) ? null : chunking.getModuleInfo(next);
156
482
  seen.add(next);
157
- if (info?.isEntry === true) return true;
483
+ if (ends(next, info)) return true;
158
484
  queue.push(...info?.importers ?? []);
159
485
  }
160
486
  return false;
161
487
  }
162
488
  /**
489
+ * Returns true for an entry of the build and for `virtual:product`.
490
+ */
491
+ function startup(id, info) {
492
+ return id === RESOLVED || info?.isEntry === true;
493
+ }
494
+ /**
495
+ * Returns true where an entry of the build or `virtual:product` imports a module statically,
496
+ * directly or through other modules.
497
+ *
498
+ * @remarks
499
+ * The walk follows each module's static importers back towards the entries, and ends at
500
+ * `virtual:product`, because every module the product imports statically loads with it. The
501
+ * standalone page's entry imports the product through `import()`, so no entry imports the
502
+ * definition statically there. A dynamic import ends the walk elsewhere, because a module behind
503
+ * one loads after the entry.
504
+ */
505
+ function initial(id, chunking) {
506
+ return leadsTo(id, chunking, startup);
507
+ }
508
+ /**
509
+ * Returns the one plugin whose web package's modules import a module outside every web package,
510
+ * directly or through other modules.
511
+ *
512
+ * @remarks
513
+ * Returns undefined where an entry or `virtual:product` imports the module statically, where the
514
+ * walk meets a module outside every web package that no module imports statically, such as a lazy
515
+ * page of the application, or a module the build does not know, and where the modules of two
516
+ * plugins import it.
517
+ */
518
+ function soleOwnerOf(id, chunking, packages) {
519
+ const owners = /* @__PURE__ */ new Set();
520
+ const seen = /* @__PURE__ */ new Set([id]);
521
+ const queue = [id];
522
+ for (let next = queue.shift(); next !== void 0; next = queue.shift()) {
523
+ const info = chunking.getModuleInfo(next);
524
+ const owner = ownerOf(next, packages);
525
+ if (next === RESOLVED || info === null || info.isEntry) return void 0;
526
+ if (owner === void 0 && info.importers.length === 0) return void 0;
527
+ if (owner !== void 0) owners.add(owner);
528
+ for (const importer of info.importers.filter((one) => !seen.has(one))) {
529
+ seen.add(importer);
530
+ queue.push(importer);
531
+ }
532
+ }
533
+ const [sole] = owners;
534
+ return owners.size === 1 ? sole : void 0;
535
+ }
536
+ /**
163
537
  * Returns the chunk a module builds into: `plugin-<id>` for a module of an installed plugin's web
164
- * package that no entry imports statically, and null for every other module.
538
+ * package that no entry imports statically, and for a module outside every web package that only
539
+ * that plugin's lazy modules import, and null for every other module.
165
540
  *
166
541
  * @param id - The module's id, its absolute path.
167
542
  * @param chunking - The context rolldown passes a group's name function.
168
543
  * @param packages - Each installed plugin's web package, by plugin id.
169
544
  */
170
545
  function pluginChunkOf(id, chunking, packages) {
171
- const owner = Object.entries(packages).find(([, { directory }]) => id.startsWith(`${directory}${sep}`));
172
- return owner === void 0 || initial(id, chunking) ? null : `plugin-${owner[0]}`;
546
+ const owner = ownerOf(id, packages);
547
+ if (owner !== void 0) return initial(id, chunking) ? null : `plugin-${owner}`;
548
+ const sole = soleOwnerOf(id, chunking, packages);
549
+ return sole === void 0 ? null : `plugin-${sole}`;
550
+ }
551
+ /**
552
+ * Returns the chunk a module builds into where the groups with a higher priority leave it:
553
+ * `dependencies` for a module outside the application that the startup or a plugin's modules import
554
+ * statically, and null for every other module.
555
+ *
556
+ * @remarks
557
+ * A module of the application is a file under the project root and outside its `node_modules`. A
558
+ * virtual module's id is not an absolute path. The walk over the module's static importers ends
559
+ * at an entry, at `virtual:product` and at a module of an installed plugin's web package. A
560
+ * module it does not meet is one that only another `import()` loads, such as a page the
561
+ * application loads on demand, which keeps its own chunk.
562
+ * @param id - The module's id, its absolute path for a file.
563
+ * @param chunking - The context rolldown passes a group's name function.
564
+ * @param root - The project root.
565
+ * @param packages - Each installed plugin's web package, by plugin id.
566
+ */
567
+ function dependencyChunkOf(id, chunking, root, packages) {
568
+ if (!isAbsolute(id)) return null;
569
+ const path = relative(root, id);
570
+ if (!(path.startsWith(`..${sep}`) || isAbsolute(path)) && !INSTALLED.test(path)) return null;
571
+ return leadsTo(id, chunking, (next, info) => startup(next, info) || ownerOf(next, packages) !== void 0) ? DEPENDENCIES : null;
572
+ }
573
+ /**
574
+ * Returns the configuration whose groups build each installed plugin's lazy modules into a chunk
575
+ * of its own, and under a build every other module outside the application that the startup or a
576
+ * plugin imports statically into the `dependencies` chunk.
577
+ *
578
+ * @remarks
579
+ * Both groups read the web packages the composition found, and the dependencies' group reads the
580
+ * project root. Both are read when the bundler places a module, after the composition. A dev
581
+ * server takes no dependencies' group: `import()` of a lazy module resolves to the module's own
582
+ * exports there without it, and `vite-config` leaves out its library and shared groups there too,
583
+ * whose chunks would run components before the React refresh runtime is installed.
584
+ * @param placing - The web packages and the project root, which each group reads when the bundler
585
+ * places a module.
586
+ * @param command - The command Vite runs under, `build` or `serve`.
587
+ */
588
+ function chunked(placing, command) {
589
+ return { build: { rolldownOptions: { output: { codeSplitting: { groups: [{
590
+ debugName: "plugins",
591
+ name: (id, chunking) => pluginChunkOf(id, chunking, placing.packages),
592
+ priority: PRIORITY
593
+ }, ...command === "build" ? [{
594
+ debugName: DEPENDENCIES,
595
+ name: (id, chunking) => dependencyChunkOf(id, chunking, placing.root, placing.packages),
596
+ priority: LAST
597
+ }] : []] } } } } };
173
598
  }
174
599
  //#endregion
175
600
  //#region src/discover.ts
@@ -381,43 +806,6 @@ async function compose(composing) {
381
806
  };
382
807
  }
383
808
  //#endregion
384
- //#region src/module.ts
385
- /**
386
- * The specifier an application imports the product from.
387
- */
388
- const ID = "virtual:product";
389
- /**
390
- * The resolved id of the module, behind the NUL that marks it as generated.
391
- */
392
- const RESOLVED = `\0${ID}`;
393
- /**
394
- * Returns the source of `virtual:product`.
395
- *
396
- * @remarks
397
- * The module imports the definition. The page receives each manifest from it, keyed by plugin id,
398
- * with its lazy importers and its contract's search validators. Every member the build resolved
399
- * is a literal in the module, and the page runs no check of its own.
400
- * @param definition - The definition module's path from the project root, such as
401
- * `/src/product.ts`.
402
- * @param product - The resolved product.
403
- * @throws {@link Error} When a member of the product cannot be written as source, naming its path.
404
- */
405
- function moduleOf(definition, product) {
406
- return [
407
- `import definition from ${quoted(definition)};`,
408
- "",
409
- `const resolved = ${literal(product, "product")};`,
410
- "",
411
- "export const product = {",
412
- " ...resolved,",
413
- " manifests: Object.fromEntries(",
414
- " definition.plugins.map(({ manifest }) => [manifest.contract.pluginId, manifest]),",
415
- " ),",
416
- "};",
417
- ""
418
- ].join("\n");
419
- }
420
- //#endregion
421
809
  //#region src/standalone.ts
422
810
  /**
423
811
  * Writes the standalone page of a plugin: the definition of the product that runs the plugin, the
@@ -434,11 +822,7 @@ const PRODUCT = "virtual:standalone-product";
434
822
  /**
435
823
  * Path of the manifest module where the page states none.
436
824
  */
437
- const MANIFEST = "src/manifest.ts";
438
- /**
439
- * The page's document, from the project root.
440
- */
441
- const DOCUMENT = "index.html";
825
+ const MANIFEST$1 = "src/manifest.ts";
442
826
  /**
443
827
  * The specifier of the page's entry.
444
828
  */
@@ -460,10 +844,6 @@ const SPECIFIED = /* @__PURE__ */ new Map([
460
844
  [PRODUCT, RESOLVED_PRODUCT]
461
845
  ]);
462
846
  /**
463
- * The language of the document where the page states no locale.
464
- */
465
- const LANGUAGE = "en-US";
466
- /**
467
847
  * Returns the source of a definition that passes the plugin's manifest module and each contract
468
848
  * package beside it to `standaloneFrom`.
469
849
  *
@@ -479,7 +859,7 @@ function sourceOf(from, page) {
479
859
  ...beside.map((name, index) => `import * as beside${String(index)} from ${quoted(name)};`),
480
860
  "",
481
861
  "export default standaloneFrom(",
482
- ` { from: ${quoted(page.manifest ?? MANIFEST)}, module: plugin },`,
862
+ ` { from: ${quoted(page.manifest ?? MANIFEST$1)}, module: plugin },`,
483
863
  " [",
484
864
  ...beside.map((name, index) => ` { from: ${quoted(name)}, module: beside${String(index)} },`),
485
865
  " ],",
@@ -497,10 +877,22 @@ function sourceOf(from, page) {
497
877
  * @param page - The manifest module and the contract packages beside the plugin.
498
878
  */
499
879
  function definitionOf(root, page) {
500
- const manifest = join(root, page.manifest ?? MANIFEST);
880
+ const manifest = join(root, page.manifest ?? MANIFEST$1);
501
881
  return sourceOf(normalizePath(relative(dirname(join(root, GENERATED)), manifest)), page);
502
882
  }
503
883
  /**
884
+ * Returns the specifier `virtual:product` imports the definition by: `virtual:standalone-product`
885
+ * for a definition the plugin generates, and the definition's path from the project root for any
886
+ * other.
887
+ *
888
+ * @param root - The project root.
889
+ * @param definition - The definition's path from the project root.
890
+ * @param generated - Whether the plugin generates the definition of a standalone page.
891
+ */
892
+ function importedOf(root, definition, generated) {
893
+ return generated ? PRODUCT : `/${normalizePath(relative(root, resolve(root, definition)))}`;
894
+ }
895
+ /**
504
896
  * Writes the generated definition under the project root, and leaves a file with the same source
505
897
  * untouched.
506
898
  *
@@ -518,38 +910,19 @@ function generate(root, page) {
518
910
  writeFileSync(file, source);
519
911
  }
520
912
  /**
521
- * Returns the source of the page's document: an element with the id `root`, and the entry.
913
+ * Returns the source of the page's document, in the language of the page's first locale.
522
914
  */
523
915
  function documentOf(page) {
524
916
  const [language = LANGUAGE] = page.locales ?? [];
525
- return [
526
- "<!doctype html>",
527
- `<html lang=${quoted(language)}>`,
528
- " <head>",
529
- " <meta charset=\"utf-8\" />",
530
- " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />",
531
- " <title>Standalone page</title>",
532
- " </head>",
533
- " <body>",
534
- " <div id=\"root\"></div>",
535
- ` <script type="module" src="/@id/${ENTRY}"><\/script>`,
536
- " </body>",
537
- "</html>",
538
- ""
539
- ].join("\n");
917
+ return documentSource({
918
+ entry: ENTRY,
919
+ language,
920
+ title: "Standalone page"
921
+ });
540
922
  }
541
923
  /**
542
924
  * Returns the source of the page's entry, which renders the page over `virtual:product` and
543
925
  * `virtual:i18n`.
544
- *
545
- * @remarks
546
- * The entry imports the stylesheet alone, and the page's modules through `import()`. The bundled
547
- * dev server installs the React refresh runtime in the entry's own body, and a chunk the entry
548
- * imports statically runs before that body, so a component in a chunk the bundler shares between
549
- * the entry and a lazy module would run before the runtime and throw. Behind `import()`, every
550
- * component runs after it. The entry awaits nothing at its top level, because a module with a
551
- * top-level await makes the bundler move every module it shares with a lazy module into a chunk
552
- * of its own.
553
926
  */
554
927
  function entryOf(page) {
555
928
  const { glyphs, locales, themes } = page;
@@ -560,26 +933,24 @@ function entryOf(page) {
560
933
  "product",
561
934
  ...themes === void 0 ? [] : [`themes: ${literal(themes, "standalone.themes")}`]
562
935
  ];
563
- const modules = [
564
- ["{ renderStandalone }", quoted("@stealthscale/sdk-host/standalone/app")],
565
- ["{ catalogues }", quoted("virtual:i18n")],
566
- ["{ product }", quoted("virtual:product")],
567
- ...glyphs === void 0 ? [] : [["{ glyphs }", quoted(posix.join("/", normalizePath(glyphs)))]]
568
- ];
569
- return [
570
- "import \"@stealthscale/theme/styles.css\";",
571
- "",
572
- "async function start() {",
573
- ` const [${modules.map(([names]) => names).join(", ")}] = await Promise.all([`,
574
- ...modules.map(([, from]) => ` import(${String(from)}),`),
575
- " ]);",
576
- "",
577
- ` await renderStandalone({ ${members.join(", ")} });`,
578
- "}",
579
- "",
580
- "void start();",
581
- ""
582
- ].join("\n");
936
+ return entrySource("renderStandalone", [
937
+ {
938
+ from: "@stealthscale/sdk-host/standalone/app",
939
+ names: "{ renderStandalone }"
940
+ },
941
+ {
942
+ from: "virtual:i18n",
943
+ names: "{ catalogues }"
944
+ },
945
+ {
946
+ from: "virtual:product",
947
+ names: "{ product }"
948
+ },
949
+ ...glyphs === void 0 ? [] : [{
950
+ from: rootedOf(glyphs),
951
+ names: "{ glyphs }"
952
+ }]
953
+ ], members);
583
954
  }
584
955
  /**
585
956
  * Returns the id the page's document, its entry or its definition resolves to, and undefined for
@@ -589,9 +960,7 @@ function entryOf(page) {
589
960
  * @param id - The specifier.
590
961
  */
591
962
  function pageIdOf(root, id) {
592
- const document = normalizePath(join(root, DOCUMENT));
593
- if (id === DOCUMENT || id === `/${DOCUMENT}` || id === document) return document;
594
- return SPECIFIED.get(id);
963
+ return documentIdOf(root, id) ?? SPECIFIED.get(id);
595
964
  }
596
965
  /**
597
966
  * Returns the source of the page's document, its entry or its definition, and undefined for any
@@ -607,29 +976,26 @@ function pageIdOf(root, id) {
607
976
  * @param id - The resolved id.
608
977
  */
609
978
  function pageSourceOf(root, page, id) {
610
- if (id === normalizePath(join(root, DOCUMENT))) return documentOf(page);
979
+ if (documentIdOf(root, id) !== void 0) return documentOf(page);
611
980
  if (id === RESOLVED_ENTRY) return entryOf(page);
612
- return id === RESOLVED_PRODUCT ? sourceOf(posix.join("/", normalizePath(page.manifest ?? MANIFEST)), page) : void 0;
981
+ return id === RESOLVED_PRODUCT ? sourceOf(rootedOf(page.manifest ?? MANIFEST$1), page) : void 0;
613
982
  }
614
983
  //#endregion
615
984
  //#region src/plugin.ts
616
985
  /**
617
986
  * The Vite plugin that composes a product from its plugins while the product builds, serves the
618
987
  * result as `virtual:product`, and writes the catalogues the product's services read. Under the
619
- * `standalone` option it serves a plugin's standalone page as well.
988
+ * `app` option it serves the product's page, and under the `standalone` option a plugin's
989
+ * standalone page.
620
990
  */
621
991
  /**
622
- * Directory of the build's output the catalogues are written to.
623
- */
624
- const CATALOGUES = ".product";
625
- /**
626
992
  * Path of the definition module where the options state none.
627
993
  */
628
994
  const DEFINITION = "src/product.ts";
629
995
  /**
630
- * Priority of the plugin chunks' group: above the shared chunk's 3, below the vendor chunk's 5.
996
+ * The message the plugin throws when the options name both pages, which both serve `index.html`.
631
997
  */
632
- const PRIORITY = 4;
998
+ const BOTH = "The product plugin serves one page at index.html, and the options name both app and standalone.";
633
999
  /**
634
1000
  * Returns the message a product that does not resolve fails with: every problem, then the hints.
635
1001
  */
@@ -686,38 +1052,6 @@ async function closed(state) {
686
1052
  await (await through)?.close();
687
1053
  }
688
1054
  /**
689
- * Returns the configuration whose group builds each installed plugin's lazy modules into a chunk
690
- * of its own.
691
- *
692
- * @remarks
693
- * The group names a chunk from the web packages the composition found. A build start records
694
- * them before the bundler forms its chunks.
695
- */
696
- function chunked(state) {
697
- return { build: { rolldownOptions: { output: { codeSplitting: { groups: [{
698
- debugName: "plugins",
699
- name: (id, chunking) => pluginChunkOf(id, chunking, state.packages),
700
- priority: PRIORITY
701
- }] } } } } };
702
- }
703
- /**
704
- * Returns the catalogue files of a client build, and none under a dev server or for another
705
- * environment.
706
- *
707
- * @param state - The command, the resolved product and the catalogue plugin's api.
708
- * @param consumer - The consumer of the environment whose bundle is written.
709
- * @throws {@link Error} When a catalogue cannot be read for a description.
710
- */
711
- function catalogueFiles(state, consumer) {
712
- const { command, product: resolved, words } = state;
713
- if (consumer !== "client" || command !== "build" || resolved === void 0 || words === void 0) return [];
714
- return Object.entries(cataloguesFor(resolved, words)).map(([name, catalogue]) => ({
715
- fileName: `${CATALOGUES}/${name}.json`,
716
- source: `${JSON.stringify(catalogue, null, 2)}\n`,
717
- type: "asset"
718
- }));
719
- }
720
- /**
721
1055
  * Records a composition: its files, its web packages, its problems, and its product where it
722
1056
  * resolved, so a composition with problems keeps the last product that resolved.
723
1057
  *
@@ -734,8 +1068,8 @@ function applied(state, composition, warn) {
734
1068
  state.failure = problems.length === 0 ? void 0 : failureOf(composition);
735
1069
  }
736
1070
  /**
737
- * Returns what the page receives after a change: a reload where the product resolved, and the
738
- * problems in the error overlay where it did not.
1071
+ * Returns the notice a page receives after a change, which reloads the page where the product
1072
+ * resolved and shows the problems in the error overlay where it did not.
739
1073
  */
740
1074
  function noticeOf(state) {
741
1075
  return state.failure === void 0 ? {
@@ -806,16 +1140,20 @@ function updated(state, context) {
806
1140
  return [];
807
1141
  }
808
1142
  /**
809
- * Returns the specifier `virtual:product` imports the definition by: `virtual:standalone-product`
810
- * for a definition the plugin generates, and the definition's path from the project root for any
811
- * other.
812
- *
813
- * @param state - The project root.
814
- * @param definition - The definition's path from the project root.
815
- * @param generated - Whether the plugin generates the definition of a standalone page.
1143
+ * Returns the plugin's state before its first hook, with the working directory as the project root
1144
+ * until the configuration resolves.
816
1145
  */
817
- function importedOf(state, definition, generated) {
818
- return generated ? PRODUCT : `/${normalizePath(relative(state.root, resolve(state.root, definition)))}`;
1146
+ function initialState() {
1147
+ return {
1148
+ base: "/",
1149
+ changed: /* @__PURE__ */ new Set(),
1150
+ changes: 0,
1151
+ command: "serve",
1152
+ files: /* @__PURE__ */ new Set(),
1153
+ following: Promise.resolve(),
1154
+ packages: {},
1155
+ root: process.cwd()
1156
+ };
819
1157
  }
820
1158
  /**
821
1159
  * Returns the source of `virtual:product`, which depends on every file the composition read.
@@ -831,6 +1169,41 @@ function served(state, imported, context) {
831
1169
  return moduleOf(imported, state.product);
832
1170
  }
833
1171
  /**
1172
+ * Returns the id the page the options name resolves a specifier to, and undefined for any other
1173
+ * specifier or without a page.
1174
+ *
1175
+ * @param state - The project root.
1176
+ * @param options - The plugin's options, which name the page.
1177
+ * @param id - The specifier.
1178
+ */
1179
+ function pageIdFor(state, options, id) {
1180
+ if (options.app !== void 0) return appIdOf(state.root, id);
1181
+ return options.standalone === void 0 ? void 0 : pageIdOf(state.root, id);
1182
+ }
1183
+ /**
1184
+ * Returns the source of a module of the page the options name, and undefined for any other id or
1185
+ * without a page.
1186
+ *
1187
+ * @remarks
1188
+ * A module of the product's page depends on every file the composition read, because the
1189
+ * document's language and title come from the product.
1190
+ * @param state - The project root, the base, the resolved product and the files the composition
1191
+ * read.
1192
+ * @param options - The plugin's options, which name the page.
1193
+ * @param id - The resolved id.
1194
+ * @param context - The load hook's context, which watches the files.
1195
+ */
1196
+ function pageServed(state, options, id, context) {
1197
+ const { app, standalone } = options;
1198
+ if (app === void 0) return standalone === void 0 ? void 0 : pageSourceOf(state.root, standalone, id);
1199
+ const source = appSourceOf(state.root, {
1200
+ ...app,
1201
+ runtime: options.runtime
1202
+ }, state, id);
1203
+ if (source !== void 0) for (const file of state.files) context.addWatchFile(file);
1204
+ return source;
1205
+ }
1206
+ /**
834
1207
  * Creates the plugin that composes a product from its plugins, serves `virtual:product`, builds
835
1208
  * each plugin's lazy modules into one chunk, and writes the access, flag and operation catalogues
836
1209
  * of a build.
@@ -841,25 +1214,22 @@ function served(state, imported, context) {
841
1214
  * resolves, and `virtual:product` then fails with the problems. A change to a file the
842
1215
  * composition read composes the product again: the page reloads, or shows the problems in the
843
1216
  * error overlay and keeps the last product. The client build writes the catalogues under
844
- * `.product` in its output. Under `standalone`, the plugin writes the definition of the product
845
- * that runs the plugin at `node_modules/.stealth/standalone/product.ts` once the configuration
846
- * resolves, unless `definition` names one. It serves the page's document as `index.html`, its
847
- * entry as `virtual:standalone`, and the generated definition as `virtual:standalone-product`.
848
- * @param options - The definition's path, and the standalone page.
1217
+ * `.product` in its output. Under `app`, the plugin serves the product's document as
1218
+ * `index.html` and its entry as `virtual:product-app`, which renders the product with
1219
+ * `renderProduct`. A dev server that serves one module per file receives the document from a
1220
+ * middleware. Under `standalone`, the plugin writes the definition of the product that runs the
1221
+ * plugin at `node_modules/.stealth/standalone/product.ts` once the configuration resolves,
1222
+ * unless `definition` names one. It serves the page's document as `index.html`, its entry as
1223
+ * `virtual:standalone`, and the generated definition as `virtual:standalone-product`.
1224
+ * @param options - The definition's path, and the product's page or the standalone page.
1225
+ * @throws {@link Error} When the options name both pages, which both serve `index.html`.
849
1226
  */
850
1227
  function product(options = {}) {
851
- const state = {
852
- changed: /* @__PURE__ */ new Set(),
853
- changes: 0,
854
- command: "serve",
855
- files: /* @__PURE__ */ new Set(),
856
- following: Promise.resolve(),
857
- packages: {},
858
- root: process.cwd()
859
- };
1228
+ const state = initialState();
860
1229
  const page = options.standalone;
861
1230
  const generated = page !== void 0 && options.definition === void 0;
862
1231
  const definition = options.definition ?? (generated ? "node_modules/.stealth/standalone/product.ts" : DEFINITION);
1232
+ if (options.app !== void 0 && page !== void 0) throw new Error(BOTH);
863
1233
  return {
864
1234
  /**
865
1235
  * Composes the product, and reports its warnings and its problems.
@@ -881,16 +1251,34 @@ function product(options = {}) {
881
1251
  return closed(state);
882
1252
  },
883
1253
  /**
884
- * Adds the group that builds each installed plugin's lazy modules into a chunk of its own.
1254
+ * Adds the group that builds each installed plugin's lazy modules into a chunk of its own, and
1255
+ * under a build the group of the `dependencies` chunk.
1256
+ */
1257
+ config(_config, { command }) {
1258
+ return chunked(state, command);
1259
+ },
1260
+ /**
1261
+ * Shows the problems in the error overlay of each page that connects while the product does
1262
+ * not resolve, and returns the hook that serves the product's document under a server that
1263
+ * serves one module per file.
1264
+ *
1265
+ * @remarks
1266
+ * A server that bundles reloads the page after a change to a file the page imports, and the
1267
+ * reloaded page starts without the overlay, so the plugin sends the problems again as the
1268
+ * page connects.
885
1269
  */
886
- config() {
887
- return chunked(state);
1270
+ configureServer(server) {
1271
+ server.environments.client.hot.on("vite:client:connect", (_payload, client) => {
1272
+ if (state.failure !== void 0) client.send(noticeOf(state));
1273
+ });
1274
+ return options.app === void 0 ? void 0 : documentHookOf(server, state);
888
1275
  },
889
1276
  /**
890
- * Records the project root, the command, the conditions and the catalogue plugin's api, and
891
- * writes the generated definition of a standalone page.
1277
+ * Records the project root, the base, the command, the conditions and the catalogue plugin's
1278
+ * api, and writes the generated definition of a standalone page.
892
1279
  */
893
1280
  configResolved(config) {
1281
+ state.base = config.base;
894
1282
  state.command = config.command;
895
1283
  state.conditions = config.ssr.resolve?.conditions;
896
1284
  state.root = config.root;
@@ -914,22 +1302,22 @@ function product(options = {}) {
914
1302
  return state.files.has(normalizePath(file)) ? updated(state, this) : void 0;
915
1303
  },
916
1304
  /**
917
- * Serves `virtual:product`, and the standalone page's document, entry and definition.
1305
+ * Serves `virtual:product`, the product page's document and entry, and the standalone page's
1306
+ * document, entry and definition.
918
1307
  *
919
1308
  * @throws {@link Error} When the product did not resolve, with every problem.
920
1309
  */
921
1310
  load(id) {
922
- if (id === RESOLVED) return served(state, importedOf(state, definition, generated), this);
923
- return page === void 0 ? void 0 : pageSourceOf(state.root, page, id);
1311
+ if (id === RESOLVED) return served(state, importedOf(state.root, definition, generated), this);
1312
+ return pageServed(state, options, id, this);
924
1313
  },
925
1314
  name: "stealth:product",
926
1315
  /**
927
- * Claims `virtual:product`, and the standalone page's document, entry and definition, leaving
928
- * every other import alone.
1316
+ * Claims `virtual:product`, the product page's document and entry, and the standalone page's
1317
+ * document, entry and definition, leaving every other import alone.
929
1318
  */
930
1319
  resolveId(id) {
931
- if (id === "virtual:product") return RESOLVED;
932
- return page === void 0 ? void 0 : pageIdOf(state.root, id);
1320
+ return id === "virtual:product" ? RESOLVED : pageIdFor(state, options, id);
933
1321
  },
934
1322
  /**
935
1323
  * Follows a change to a file the last composition read, which the next composition transforms
@@ -944,4 +1332,1037 @@ function product(options = {}) {
944
1332
  };
945
1333
  }
946
1334
  //#endregion
947
- export { ID, product };
1335
+ //#region src/provided/facade.ts
1336
+ /**
1337
+ * Writes the facade of a provided specifier: the entry module a page's import map maps the
1338
+ * specifier to, which re-exports the package the product bundles.
1339
+ *
1340
+ * @remarks
1341
+ * A facade contains no code of its own, so the package's code remains in the chunk the
1342
+ * product's chunk groups put it in, and a release that imports the specifier receives the
1343
+ * product's module. A CommonJS package such as React exports its names from an object, which
1344
+ * `export *` does not forward, so its facade names every export.
1345
+ */
1346
+ /**
1347
+ * The prefix of a facade's id, behind the NUL that marks the module as generated.
1348
+ */
1349
+ const FACADE = "\0stealth-provided:";
1350
+ /**
1351
+ * Matches a name JavaScript accepts as a binding.
1352
+ */
1353
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/u;
1354
+ /**
1355
+ * Returns the id of a specifier's facade.
1356
+ */
1357
+ function facadeIdOf(specifier) {
1358
+ return `${FACADE}${specifier}`;
1359
+ }
1360
+ /**
1361
+ * Returns the specifier of a facade's id, or undefined for any other id.
1362
+ */
1363
+ function specifierOf(id) {
1364
+ return id.startsWith("\0stealth-provided:") ? id.slice(18) : void 0;
1365
+ }
1366
+ /**
1367
+ * Returns a package name or a specifier as part of a chunk's name: without its scope's `@`, and
1368
+ * with every other character outside a name written as `-`.
1369
+ */
1370
+ function slugOf(name) {
1371
+ return name.replace(/^@/u, "").replaceAll(/[^\w-]+/gu, "-");
1372
+ }
1373
+ /**
1374
+ * Returns the name of a facade's chunk: `provided/` and the specifier's slug.
1375
+ */
1376
+ function chunkNameOf(specifier) {
1377
+ return `provided/${slugOf(specifier)}`;
1378
+ }
1379
+ /**
1380
+ * Returns the shape of a module from the bundler's information about it, and from its exports
1381
+ * object where it is CommonJS.
1382
+ *
1383
+ * @remarks
1384
+ * The bundler lists no names for a CommonJS module, whose exports object exists only once the
1385
+ * module runs, so the names come from the object Node loads.
1386
+ * @param module - The bundler's information about the module.
1387
+ * @param exported - Returns the names of the module's exports object, as Node loads it.
1388
+ */
1389
+ function shapeOf(module, exported) {
1390
+ if (module.inputFormat !== "cjs") return {
1391
+ defaulted: module.exports.includes("default"),
1392
+ kind: "module"
1393
+ };
1394
+ return {
1395
+ kind: "commonjs",
1396
+ names: exported().filter((name) => name !== "default" && name !== "__esModule" && IDENTIFIER.test(name))
1397
+ };
1398
+ }
1399
+ /**
1400
+ * Returns the source of a specifier's facade.
1401
+ *
1402
+ * @param specifier - The provided specifier.
1403
+ * @param shape - The exports of the specifier's module.
1404
+ */
1405
+ function facadeOf(specifier, shape) {
1406
+ const from = quoted(specifier);
1407
+ if (shape.kind === "module") return [
1408
+ `export * from ${from};`,
1409
+ ...shape.defaulted ? [`export { default } from ${from};`] : [],
1410
+ ""
1411
+ ].join("\n");
1412
+ return [
1413
+ `import provided from ${from};`,
1414
+ "export default provided;",
1415
+ ...shape.names.length === 0 ? [] : [`export const { ${shape.names.join(", ")} } = provided;`],
1416
+ ""
1417
+ ].join("\n");
1418
+ }
1419
+ //#endregion
1420
+ //#region src/provided/pinned.ts
1421
+ /**
1422
+ * Returns a map from the id of each module of the first build to the name of its chunk.
1423
+ *
1424
+ * @remarks
1425
+ * The modules of a static entry's chunk have no pin. The second chunk of one name in file name
1426
+ * order takes the name with `-2` appended, and the third takes it with `-3` appended.
1427
+ * @param chunks - The chunks of the first build.
1428
+ */
1429
+ function pinnedOf(chunks) {
1430
+ const counts = /* @__PURE__ */ new Map();
1431
+ const pinned = /* @__PURE__ */ new Map();
1432
+ const ordered = chunks.filter(({ isEntry }) => !isEntry).toSorted((one, two) => one.fileName.localeCompare(two.fileName));
1433
+ for (const { modules, name } of ordered) {
1434
+ const count = (counts.get(name) ?? 0) + 1;
1435
+ const pin = count === 1 ? name : `${name}-${String(count)}`;
1436
+ counts.set(name, count);
1437
+ for (const id of Object.keys(modules)) pinned.set(id, pin);
1438
+ }
1439
+ return pinned;
1440
+ }
1441
+ /**
1442
+ * Returns the code-splitting group that places each pinned module in the second build.
1443
+ *
1444
+ * @remarks
1445
+ * A pinned chunk can import a chunk of the facades' code that imports it back. Rolldown defines a
1446
+ * CommonJS module's wrapper at the top level of its chunk, and an ES module that imports it calls
1447
+ * the wrapper at its own top level. In such a cycle a chunk can call a wrapper that is not
1448
+ * defined yet. The group builds each pin's CommonJS modules into a chunk of their own. As long as
1449
+ * those modules require only CommonJS modules, that chunk imports only rolldown's runtime and
1450
+ * other such chunks. It then runs before every chunk that calls its wrappers.
1451
+ * @param pins - Returns a map from the id of each module of the first build to its chunk once the
1452
+ * first build has run.
1453
+ */
1454
+ function pinnedGroup(pins) {
1455
+ return {
1456
+ debugName: "pinned",
1457
+ name: (id, chunking) => {
1458
+ const pin = pins()?.get(id);
1459
+ if (pin === void 0) return null;
1460
+ return chunking.getModuleInfo(id)?.inputFormat === "cjs" ? `${pin}-cjs` : pin;
1461
+ }
1462
+ };
1463
+ }
1464
+ //#endregion
1465
+ //#region src/provided/first.ts
1466
+ /**
1467
+ * Runs a product's first build: the build's own configuration without the facades, written
1468
+ * nowhere, whose chunks the second build pins the product's modules to.
1469
+ */
1470
+ /**
1471
+ * Name of the plugin that marks the first build, which leaves the facades out.
1472
+ */
1473
+ const FIRST_BUILD = "stealth:provided-first";
1474
+ /**
1475
+ * Returns true where a configuration's plugins mark the first build.
1476
+ *
1477
+ * @param plugins - The resolved plugins of a build.
1478
+ */
1479
+ function isFirstBuild(plugins) {
1480
+ return plugins.some(({ name }) => name === FIRST_BUILD);
1481
+ }
1482
+ /**
1483
+ * Returns a plugin option with every nested list flattened into one list.
1484
+ */
1485
+ function flattened(option) {
1486
+ return Array.isArray(option) ? option.flatMap((one) => flattened(one)) : [option];
1487
+ }
1488
+ /**
1489
+ * Returns the plugins a caller passed inline, with the second build's own facade plugin left out.
1490
+ */
1491
+ function inlinePlugins(plugins, own) {
1492
+ return (plugins ?? []).flatMap((option) => flattened(option)).filter((one) => one !== own);
1493
+ }
1494
+ /**
1495
+ * Returns the chunks of the first build of the configuration a second build resolved.
1496
+ *
1497
+ * @remarks
1498
+ * The first build reads the same configuration file and the same inline configuration, with the
1499
+ * plugin that marks it, without watching and without writing. A facade plugin of the first build
1500
+ * reads the mark and adds nothing, so the first build places each module as the product's build
1501
+ * does without facades.
1502
+ * @param config - The second build's configuration file, inline configuration, mode and root.
1503
+ * @param own - The second build's facade plugin, which a caller may have passed inline.
1504
+ */
1505
+ async function firstBuild(config, own) {
1506
+ const { configFile, inlineConfig, mode, root } = config;
1507
+ return [await build({
1508
+ ...inlineConfig,
1509
+ build: {
1510
+ ...inlineConfig.build,
1511
+ watch: null,
1512
+ write: false
1513
+ },
1514
+ configFile: configFile ?? false,
1515
+ logLevel: "silent",
1516
+ mode,
1517
+ plugins: [...inlinePlugins(inlineConfig.plugins, own), { name: FIRST_BUILD }],
1518
+ root
1519
+ })].flat().flatMap(({ output }) => output).filter((file) => file.type === "chunk");
1520
+ }
1521
+ //#endregion
1522
+ //#region src/provided/packages.ts
1523
+ /**
1524
+ * Lists the packages a page provides to the releases it loads, and the specifiers of each.
1525
+ *
1526
+ * @remarks
1527
+ * A module that keeps state at module level exists once per page, or two copies of it disagree.
1528
+ * The provided packages are React, the libraries whose contexts the providers wrap, the five Zag
1529
+ * modules of the overlays, and the `@stealthscale` packages a plugin's components import: the
1530
+ * SDK, the providers, `settings`, `hooks`, `theme` and every component package. A product
1531
+ * provides a package where a package on its dependency graph depends on it or peers on it.
1532
+ */
1533
+ /**
1534
+ * The provided specifiers of the packages that are not `@stealthscale` packages, by package.
1535
+ *
1536
+ * @remarks
1537
+ * React's compiled components import `react/compiler-runtime` and `react/jsx-runtime`, which
1538
+ * are specifiers of their own: a release that bundled either would bundle React's index too.
1539
+ */
1540
+ const LIBRARIES = {
1541
+ "@tanstack/react-form": ["@tanstack/react-form"],
1542
+ "@tanstack/react-hotkeys": ["@tanstack/react-hotkeys"],
1543
+ "@tanstack/react-query": ["@tanstack/react-query"],
1544
+ "@tanstack/react-router": ["@tanstack/react-router"],
1545
+ "@tanstack/router-core": ["@tanstack/router-core"],
1546
+ "@zag-js/aria-hidden": ["@zag-js/aria-hidden"],
1547
+ "@zag-js/dismissable": ["@zag-js/dismissable"],
1548
+ "@zag-js/focus-trap": ["@zag-js/focus-trap"],
1549
+ "@zag-js/focus-visible": ["@zag-js/focus-visible"],
1550
+ "@zag-js/remove-scroll": ["@zag-js/remove-scroll"],
1551
+ i18next: ["i18next"],
1552
+ react: [
1553
+ "react",
1554
+ "react/jsx-runtime",
1555
+ "react/compiler-runtime"
1556
+ ],
1557
+ "react-dom": ["react-dom"],
1558
+ "react-i18next": ["react-i18next"]
1559
+ };
1560
+ /**
1561
+ * Matches the name of an `@stealthscale` package a page provides.
1562
+ */
1563
+ const PROVIDED = /^@stealthscale\/(?:sdk-core|sdk-plugin|settings|hooks|theme|provider-[a-z0-9-]+|component-[a-z0-9-]+)$/u;
1564
+ /**
1565
+ * The entries of a provided `@stealthscale` package that a page does not map: its manifest, its
1566
+ * preset, its test helpers and its stylesheet.
1567
+ */
1568
+ const WITHHELD = /* @__PURE__ */ new Set([
1569
+ "./package.json",
1570
+ "./testing",
1571
+ "./theme"
1572
+ ]);
1573
+ /**
1574
+ * Returns the specifiers of an `@stealthscale` package from the subpaths its `exports` maps, every
1575
+ * entry but the withheld ones and the stylesheets.
1576
+ *
1577
+ * @param name - The package's name.
1578
+ * @param exports - The package's `exports` member.
1579
+ */
1580
+ function specifiersOf(name, exports) {
1581
+ if (typeof exports !== "object" || exports === null) return [name];
1582
+ const subpaths = Object.keys(exports).filter((subpath) => subpath.startsWith("."));
1583
+ return (subpaths.length === 0 ? ["."] : subpaths).filter((subpath) => !WITHHELD.has(subpath) && !subpath.endsWith(".css")).map((subpath) => subpath === "." ? name : `${name}/${subpath.slice(2)}`);
1584
+ }
1585
+ /**
1586
+ * Returns the directory a package resolves from: the root where the root's own `node_modules`
1587
+ * contains it, else the first package on the graph that does.
1588
+ */
1589
+ function foundFrom(name, root, graph) {
1590
+ for (const from of [root, ...graph.map(({ at }) => at)]) {
1591
+ const at = packageAt(name, from);
1592
+ if (at !== void 0) return [from, at];
1593
+ }
1594
+ }
1595
+ /**
1596
+ * Returns true for the name of a package a page provides.
1597
+ */
1598
+ function isProvidedPackage(name) {
1599
+ return Object.hasOwn(LIBRARIES, name) || PROVIDED.test(name);
1600
+ }
1601
+ /**
1602
+ * Returns the specifiers and the version of a provided package installed in a directory.
1603
+ *
1604
+ * @param name - The package's name.
1605
+ * @param at - The package's directory.
1606
+ * @returns The specifiers and the version, or undefined where the manifest states no version.
1607
+ */
1608
+ function installedAt(name, at) {
1609
+ const manifest = manifestAt(at);
1610
+ const version = manifest?.["version"];
1611
+ if (typeof version !== "string") return void 0;
1612
+ return {
1613
+ specifiers: LIBRARIES[name] ?? specifiersOf(name, manifest?.["exports"]),
1614
+ version
1615
+ };
1616
+ }
1617
+ /**
1618
+ * Returns the provided package of a name, or undefined where it is not installed on the graph or
1619
+ * states no version.
1620
+ */
1621
+ function providedOf$1(name, root, graph) {
1622
+ const found = foundFrom(name, root, graph);
1623
+ const installed = found === void 0 ? void 0 : installedAt(name, found[1]);
1624
+ return found === void 0 || installed === void 0 ? void 0 : {
1625
+ from: found[0],
1626
+ name,
1627
+ ...installed
1628
+ };
1629
+ }
1630
+ /**
1631
+ * Returns the names of the packages a package on the graph peers on.
1632
+ */
1633
+ function peersOf({ manifest }) {
1634
+ const peers = manifest["peerDependencies"];
1635
+ return typeof peers === "object" && peers !== null ? Object.keys(peers) : [];
1636
+ }
1637
+ /**
1638
+ * Returns every package the product at a root provides, sorted by name.
1639
+ *
1640
+ * @remarks
1641
+ * The walk of the graph reads `dependencies` alone. A peer is bundled where a module that peers
1642
+ * on it imports it, so the names come from the peers of every package on the graph as well.
1643
+ * @param root - The product's root, whose dependency graph the walk reads.
1644
+ */
1645
+ function providedPackages(root) {
1646
+ const graph = dependencies(root);
1647
+ return [.../* @__PURE__ */ new Set([...Object.keys(LIBRARIES), ...[...graph.map(({ named: name }) => name), ...graph.flatMap((dependency) => peersOf(dependency))].filter((name) => PROVIDED.test(name))])].toSorted().flatMap((name) => providedOf$1(name, root, graph) ?? []);
1648
+ }
1649
+ /**
1650
+ * Returns the name of the package a specifier imports from: its first segment, or its first two
1651
+ * for a scoped package.
1652
+ */
1653
+ function packageOf(specifier) {
1654
+ return specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
1655
+ }
1656
+ /**
1657
+ * Returns the path a provided specifier resolves from: the `package.json` of the directory its
1658
+ * package resolves from.
1659
+ *
1660
+ * @param provided - The provided packages.
1661
+ * @param specifier - A specifier a facade imports, such as `react/jsx-runtime`.
1662
+ * @returns The path, or undefined for a specifier no provided package maps.
1663
+ */
1664
+ function importerOf(provided, specifier) {
1665
+ const owner = provided.find(({ specifiers }) => specifiers.includes(specifier));
1666
+ return owner === void 0 ? void 0 : join(owner.from, "package.json");
1667
+ }
1668
+ //#endregion
1669
+ //#region src/provided/manifest.ts
1670
+ /**
1671
+ * Returns the paths of some chunks and of every chunk that they import statically. The given
1672
+ * chunks come first.
1673
+ */
1674
+ function closureOf(chunks, seeds) {
1675
+ const closure = [];
1676
+ /**
1677
+ * Adds a chunk to the closure, then the chunks that it imports.
1678
+ */
1679
+ const visit = (name) => {
1680
+ const chunk = chunks.get(name);
1681
+ if (chunk === void 0 || closure.includes(name)) return;
1682
+ closure.push(name);
1683
+ for (const imported of chunk.imports) visit(imported);
1684
+ };
1685
+ for (const seed of seeds) visit(seed);
1686
+ return closure;
1687
+ }
1688
+ /**
1689
+ * Returns the facade chunk of each provided specifier, sorted by specifier.
1690
+ */
1691
+ function facadesOf$1(chunks) {
1692
+ return [...chunks.values()].flatMap((chunk) => {
1693
+ const specifier = chunk.facadeModuleId === null ? void 0 : specifierOf(chunk.facadeModuleId);
1694
+ return specifier === void 0 ? [] : [{
1695
+ chunk,
1696
+ specifier
1697
+ }];
1698
+ }).toSorted((one, two) => one.specifier.localeCompare(two.specifier));
1699
+ }
1700
+ /**
1701
+ * Returns the paths of the chunks that a page loads when it starts. These are the entries other
1702
+ * than the facades and the chunks that those entries import statically. They also include each
1703
+ * chunk of a module that such an entry imports through `import()`, and the static imports of that
1704
+ * chunk.
1705
+ */
1706
+ function startupOf(chunks, facades) {
1707
+ const provided = new Set(facades.map(({ chunk }) => chunk.fileName));
1708
+ const entries = [...chunks.values()].filter(({ fileName, isEntry }) => isEntry && !provided.has(fileName));
1709
+ const seeds = [...entries.map(({ fileName }) => fileName), ...entries.flatMap(({ dynamicImports }) => dynamicImports)];
1710
+ return new Set(closureOf(chunks, seeds));
1711
+ }
1712
+ /**
1713
+ * Returns what a build provides. The result maps each specifier to the URL of its facade chunk, and
1714
+ * to the URLs of the chunks that the facade imports outside the page's startup. It also contains
1715
+ * the page's fallback language and the version of each provided package.
1716
+ *
1717
+ * @param bundle - The build's output files, by file name.
1718
+ * @param base - The public base the build serves its files at.
1719
+ * @param packages - The packages the build provides.
1720
+ * @param language - The page's fallback language.
1721
+ */
1722
+ function providedOf(bundle, base, packages, language) {
1723
+ const chunks = new Map(Object.values(bundle).flatMap((file) => file.type === "chunk" ? [[file.fileName, file]] : []));
1724
+ const facades = facadesOf$1(chunks);
1725
+ const startup = startupOf(chunks, facades);
1726
+ return {
1727
+ chunks: Object.fromEntries(facades.map(({ chunk, specifier }) => [specifier, closureOf(chunks, [chunk.fileName]).filter((name) => name !== chunk.fileName && !startup.has(name)).map((name) => `${base}${name}`)])),
1728
+ imports: Object.fromEntries(facades.map(({ chunk, specifier }) => [specifier, `${base}${chunk.fileName}`])),
1729
+ language,
1730
+ versions: Object.fromEntries(packages.map(({ name, version }) => [name, version]))
1731
+ };
1732
+ }
1733
+ /**
1734
+ * Returns the import map of the provided specifiers. The build adds it as the first of its tags in
1735
+ * a document's head.
1736
+ *
1737
+ * @param imports - The URL of each provided specifier's entry, by specifier.
1738
+ */
1739
+ function importMapOf(imports) {
1740
+ return {
1741
+ attrs: { type: "importmap" },
1742
+ children: JSON.stringify({ imports }),
1743
+ injectTo: "head-prepend",
1744
+ tag: "script"
1745
+ };
1746
+ }
1747
+ //#endregion
1748
+ //#region src/provided/plugin.ts
1749
+ /**
1750
+ * Provides a product's shared modules to the releases that its page loads.
1751
+ *
1752
+ * @remarks
1753
+ * The plugin writes one entry per provided specifier, a file that lists their URLs and the
1754
+ * provided packages' versions, and the import map in the page's document. It runs in a build
1755
+ * alone. A development server does not serve the provided entries, so a product loads releases
1756
+ * from its build.
1757
+ */
1758
+ /**
1759
+ * Returns the names of the exports object of a CommonJS module that Node loads.
1760
+ */
1761
+ function exportedOf(from, specifier) {
1762
+ const loaded = createRequire(from)(specifier);
1763
+ return typeof loaded === "object" && loaded !== null ? Object.keys(loaded) : [];
1764
+ }
1765
+ /**
1766
+ * Returns the page's fallback language from the catalogue plugin.
1767
+ *
1768
+ * @throws {@link Error} When the configuration lacks the catalogue plugin.
1769
+ */
1770
+ function languageOf(api) {
1771
+ if (api === void 0) throw new Error("The configuration lacks the stealth:i18n plugin. The build reads the page's fallback language from that plugin and writes it into provided.json. Add the layers of @stealthscale/vite-config-i18n.");
1772
+ return api.fallback;
1773
+ }
1774
+ /**
1775
+ * Returns the entry chunk of the facade of each specifier of each provided package.
1776
+ */
1777
+ function facadesOf(packages) {
1778
+ return packages.flatMap(({ specifiers }) => specifiers.map((specifier) => ({
1779
+ id: facadeIdOf(specifier),
1780
+ name: chunkNameOf(specifier),
1781
+ preserveSignature: "strict",
1782
+ type: "chunk"
1783
+ })));
1784
+ }
1785
+ /**
1786
+ * Returns the source of the facade of a specifier a provided package maps, and null for any other
1787
+ * id.
1788
+ *
1789
+ * @param context - The load hook's context, which resolves and loads the specifier's module.
1790
+ * @param packages - The packages the product provides.
1791
+ * @param id - The id the hook loads.
1792
+ * @throws {@link Error} When the facade's specifier does not resolve.
1793
+ */
1794
+ async function facadeSourceOf(context, packages, id) {
1795
+ const specifier = specifierOf(id);
1796
+ const from = specifier === void 0 ? void 0 : importerOf(packages, specifier);
1797
+ if (specifier === void 0 || from === void 0) return null;
1798
+ const resolved = await context.resolve(specifier, from, { skipSelf: true });
1799
+ if (resolved === null) throw new Error(`The provided specifier ${specifier} does not resolve from ${from}.`);
1800
+ return facadeOf(specifier, shapeOf(await context.load({ id: resolved.id }), () => exportedOf(from, specifier)));
1801
+ }
1802
+ /**
1803
+ * Creates the plugin that provides the product's shared modules to the releases its page loads.
1804
+ *
1805
+ * @remarks
1806
+ * The client build first runs a build of the same configuration without the facades. It then
1807
+ * builds each module of that first build into the chunk of the same name, and each CommonJS
1808
+ * module into a chunk beside it. It emits each facade as an entry chunk that keeps its exports.
1809
+ * A facade imports its specifier, and the plugin resolves the specifier from the package that
1810
+ * depends on it, so the facade and the product share one module. Rolldown builds the code that
1811
+ * only the facades need into chunks that only the facades import. The plugin writes
1812
+ * `.product/provided.json` beside the page. The file contains the chunks that each facade imports
1813
+ * and the page's fallback language. The plugin writes the import map before the module scripts of
1814
+ * every document that the build writes.
1815
+ */
1816
+ function provided() {
1817
+ const state = {
1818
+ base: "/",
1819
+ first: false,
1820
+ language: "",
1821
+ packages: []
1822
+ };
1823
+ const plugin = {
1824
+ apply: "build",
1825
+ /**
1826
+ * Runs the first build in the client build, and emits the facade of every provided specifier.
1827
+ *
1828
+ * @throws {@link Error} When the configuration lacks the catalogue plugin.
1829
+ */
1830
+ async buildStart() {
1831
+ if (state.first || this.environment.config.consumer !== "client") return;
1832
+ state.language = languageOf(state.api);
1833
+ state.pinned = pinnedOf(await firstBuild(this.environment.config, plugin));
1834
+ for (const facade of facadesOf(state.packages)) this.emitFile(facade);
1835
+ },
1836
+ /**
1837
+ * Records the catalogue plugin's api, the public base, whether the build is the first build,
1838
+ * and the packages the product provides.
1839
+ */
1840
+ configResolved(config) {
1841
+ state.api = cataloguesOf(config.plugins);
1842
+ state.base = config.base;
1843
+ state.first = isFirstBuild(config.plugins);
1844
+ state.packages = state.first ? [] : providedPackages(config.root);
1845
+ },
1846
+ enforce: "pre",
1847
+ /**
1848
+ * Writes `.product/provided.json` into the client build's output.
1849
+ */
1850
+ generateBundle(_options, bundle) {
1851
+ if (state.pinned === void 0 || this.environment.config.consumer !== "client") return;
1852
+ const listing = providedOf(bundle, state.base, state.packages, state.language);
1853
+ this.emitFile({
1854
+ fileName: PROVIDED_FILE,
1855
+ source: `${JSON.stringify(listing, void 0, 2)}\n`,
1856
+ type: "asset"
1857
+ });
1858
+ },
1859
+ /**
1860
+ * Returns the source of a facade, and nothing for any other id.
1861
+ *
1862
+ * @throws {@link Error} When the facade's specifier does not resolve.
1863
+ */
1864
+ load(id) {
1865
+ return facadeSourceOf(this, state.packages, id);
1866
+ },
1867
+ name: "stealth:provided",
1868
+ /**
1869
+ * Replaces the client build's code-splitting groups with the one group that pins each module
1870
+ * of the first build to its chunk.
1871
+ *
1872
+ * @remarks
1873
+ * Rolldown reads the output options before the build starts, and calls the group's name
1874
+ * function after the first build has run.
1875
+ */
1876
+ outputOptions(options) {
1877
+ if (state.first || this.environment.config.consumer !== "client") return null;
1878
+ const splitting = typeof options.codeSplitting === "object" ? options.codeSplitting : {};
1879
+ const group = pinnedGroup(() => state.pinned);
1880
+ return {
1881
+ ...options,
1882
+ codeSplitting: {
1883
+ ...splitting,
1884
+ groups: [group]
1885
+ }
1886
+ };
1887
+ },
1888
+ /**
1889
+ * Claims a facade's id, and resolves a facade's import of a provided specifier from the package
1890
+ * that depends on it.
1891
+ */
1892
+ resolveId(id, importer) {
1893
+ if (specifierOf(id) !== void 0) return id;
1894
+ const from = importer !== void 0 && specifierOf(importer) !== void 0 ? importerOf(state.packages, id) : void 0;
1895
+ return from === void 0 ? null : this.resolve(id, from, { skipSelf: true });
1896
+ },
1897
+ transformIndexHtml: {
1898
+ /**
1899
+ * Writes the import map as the first tag that the client build adds to a document's head,
1900
+ * so the map precedes the page's module scripts.
1901
+ */
1902
+ handler(_html, context) {
1903
+ return state.pinned === void 0 || context.bundle === void 0 ? void 0 : [importMapOf(providedOf(context.bundle, state.base, state.packages, state.language).imports)];
1904
+ },
1905
+ order: "post"
1906
+ }
1907
+ };
1908
+ return plugin;
1909
+ }
1910
+ //#endregion
1911
+ //#region src/release/catalogues.ts
1912
+ /**
1913
+ * Returns the catalogues of a plugin's namespace, the fallback language first.
1914
+ *
1915
+ * @param api - The catalogue plugin's api.
1916
+ * @param namespace - The plugin's namespace, its id.
1917
+ * @throws {@link Error} When the namespace has no catalogue in the fallback language.
1918
+ */
1919
+ function releaseCatalogues(api, namespace) {
1920
+ const { fallback } = api;
1921
+ const languages = new Set(api.catalogues().filter((catalogue) => catalogue.namespace === namespace).map(({ language }) => language));
1922
+ if (!languages.has(fallback)) throw new Error(`The namespace ${namespace} has no catalogue in the fallback language ${fallback}, and a page refuses such a release.`);
1923
+ const others = [...languages].filter((language) => language !== fallback).toSorted();
1924
+ return {
1925
+ files: [fallback, ...others].map((language) => ({
1926
+ fileName: `locales/${language}.json`,
1927
+ source: `${JSON.stringify(api.words(language, namespace), void 0, 2)}\n`
1928
+ })),
1929
+ languages: [fallback, ...others]
1930
+ };
1931
+ }
1932
+ //#endregion
1933
+ //#region src/release/describe.ts
1934
+ /**
1935
+ * Writes the parts of a release's description: the caret range of a version, the integrity of a
1936
+ * file, and the text of `plugin.json`.
1937
+ */
1938
+ /**
1939
+ * Matches the major and the minor at the start of a version.
1940
+ */
1941
+ const MINOR = /^(\d+)\.(\d+)\./u;
1942
+ /**
1943
+ * Returns the caret range a release requires of a version it was built against: the version's
1944
+ * major and minor, from the minor's first patch.
1945
+ *
1946
+ * @remarks
1947
+ * A release built against 0.2.3 requires `^0.2.0`, and one built against 1.4.2 requires
1948
+ * `^1.4.0`.
1949
+ * @throws {@link Error} When the text does not start with a major and a minor.
1950
+ */
1951
+ function caretOf(version) {
1952
+ const match = MINOR.exec(version);
1953
+ if (match === null) throw new Error(`${version} is not a version.`);
1954
+ return `^${match.slice(1, 3).join(".")}.0`;
1955
+ }
1956
+ /**
1957
+ * Returns the integrity of a file's bytes, in the form `sha384-<base64>`.
1958
+ */
1959
+ function integrityOf(content) {
1960
+ return `sha384-${createHash("sha384").update(content).digest("base64")}`;
1961
+ }
1962
+ /**
1963
+ * Returns the text of a release's `plugin.json`.
1964
+ *
1965
+ * @throws {@link Error} When the description is not of the shape a page reads, with every fault.
1966
+ */
1967
+ function describedOf(release) {
1968
+ const problems = releaseProblems(release);
1969
+ if (problems.length > 0) throw new Error(["The release's description is malformed:", ...problems.map((problem) => lineOf(problem))].join("\n"));
1970
+ return `${JSON.stringify(release, void 0, 2)}\n`;
1971
+ }
1972
+ //#endregion
1973
+ //#region src/release/evaluate.ts
1974
+ /**
1975
+ * Reads what a release's build needs from the plugin's own modules in Node: the plugin's id from
1976
+ * its manifest, and the recipes its preset declares.
1977
+ */
1978
+ /**
1979
+ * Returns the member of a value under a key where the value is an object, and undefined otherwise.
1980
+ */
1981
+ function memberOf(value, key) {
1982
+ return typeof value === "object" && value !== null ? Reflect.get(value, key) : void 0;
1983
+ }
1984
+ /**
1985
+ * Returns the name of every recipe and every slot recipe a preset declares, under `theme` and
1986
+ * under `theme.extend`.
1987
+ *
1988
+ * @param preset - The preset a package publishes under `./theme`.
1989
+ */
1990
+ function recipesOf(preset) {
1991
+ const theme = memberOf(preset, "theme");
1992
+ return [theme, memberOf(theme, "extend")].flatMap((part) => ["recipes", "slotRecipes"].flatMap((key) => {
1993
+ const recipes = memberOf(part, key);
1994
+ return typeof recipes === "object" && recipes !== null ? Object.keys(recipes) : [];
1995
+ }));
1996
+ }
1997
+ /**
1998
+ * Returns the plugin id the `manifest` export of a module states, or undefined where it states
1999
+ * none.
2000
+ */
2001
+ function pluginIdIn(module) {
2002
+ const id = memberOf(memberOf(memberOf(module, "manifest"), "contract"), "pluginId");
2003
+ return typeof id === "string" ? id : void 0;
2004
+ }
2005
+ /**
2006
+ * Imports the module that exports the plugin's manifest, and returns the plugin's id.
2007
+ *
2008
+ * @param through - The importer, opened and closed by the caller.
2009
+ * @param root - The web package's directory.
2010
+ * @param manifest - The module's path from the package's directory.
2011
+ * @throws {@link Error} When the module exports no manifest under `manifest`.
2012
+ */
2013
+ async function releasedId(through, root, manifest) {
2014
+ const { module } = await through.import(join(root, manifest));
2015
+ const id = pluginIdIn(module);
2016
+ if (id === void 0) throw new Error(`${manifest} exports no manifest under the name manifest, which a page imports from a release's manifest.js.`);
2017
+ return id;
2018
+ }
2019
+ /**
2020
+ * Imports the preset a web package publishes under `./theme`, and throws where it declares a
2021
+ * recipe.
2022
+ *
2023
+ * @remarks
2024
+ * A page compiles its stylesheet from the component packages it provides, so a release's own
2025
+ * recipe would have no rule there. A package without a `./theme` entry passes.
2026
+ * @param through - The importer, opened and closed by the caller.
2027
+ * @param root - The web package's directory.
2028
+ * @param conditions - The conditions the package's `exports` resolve under. None where left out.
2029
+ * @throws {@link Error} When the preset declares a recipe, naming every recipe.
2030
+ */
2031
+ async function refuseRecipes(through, root, conditions) {
2032
+ const target = exportTarget(manifestAt(root) ?? {}, "./theme", conditions);
2033
+ if (target === void 0) return;
2034
+ const { module } = await through.import(join(root, target));
2035
+ const recipes = recipesOf(memberOf(module, "default"));
2036
+ if (recipes.length > 0) throw new Error(`The package's preset declares the recipes ${recipes.join(", ")}. A release renders the components the page provides and declares no recipe of its own.`);
2037
+ }
2038
+ //#endregion
2039
+ //#region src/release/imports.ts
2040
+ /**
2041
+ * Decides how a release imports a module of a provided package: by a specifier the page's import
2042
+ * map resolves, or not at all.
2043
+ *
2044
+ * @remarks
2045
+ * A release imports every provided specifier by name and bundles no module of a provided package.
2046
+ * A specifier that the page does not map, such as a subpath its package withholds, would bundle a
2047
+ * second copy of the package's modules into the release, so the build fails on it.
2048
+ */
2049
+ /**
2050
+ * Returns the provided package a module of a release imports by a specifier, or undefined for a
2051
+ * specifier of any other package.
2052
+ *
2053
+ * @param source - The specifier the module imports.
2054
+ * @param importer - The path of the module.
2055
+ * @throws {@link Error} When the package is provided but not installed with a version for the
2056
+ * module, or the page does not map the specifier.
2057
+ */
2058
+ function providedImportOf(source, importer) {
2059
+ const name = packageOf(source);
2060
+ if (!isProvidedPackage(name)) return void 0;
2061
+ const at = packageAt(name, dirname(importer));
2062
+ const installed = at === void 0 ? void 0 : installedAt(name, at);
2063
+ if (installed === void 0) throw new Error(`${importer} imports ${source}, and ${name} is not installed with a version.`);
2064
+ if (!installed.specifiers.includes(source)) throw new Error(`${importer} imports ${source}, which the page does not map, so the release would bundle a copy of ${name}. Import one of ${installed.specifiers.join(", ")}.`);
2065
+ return {
2066
+ name,
2067
+ version: installed.version
2068
+ };
2069
+ }
2070
+ //#endregion
2071
+ //#region src/release/preloads.ts
2072
+ /**
2073
+ * Rewrites a release's lazy imports of its own modules, so that the page preloads a module's static
2074
+ * imports while the release imports the module.
2075
+ *
2076
+ * @remarks
2077
+ * The build rewrites each chunk before rolldown writes the hashes into the chunks' names and
2078
+ * minifies them. Rolldown replaces a hash in both copies of a path.
2079
+ */
2080
+ /**
2081
+ * The specifier of the package that exports `preloaded`.
2082
+ */
2083
+ const SDK = "@stealthscale/sdk-core";
2084
+ /**
2085
+ * Name under which a rewritten chunk imports `preloaded`.
2086
+ */
2087
+ const PRELOADED = "__stealth_preloaded__";
2088
+ /**
2089
+ * Matches the string literal of a lazy import of a module of the release: a relative path between
2090
+ * quotes or backticks, without an expression and without a backslash.
2091
+ */
2092
+ const RELATIVE = /^(["'`])\.{1,2}\/[^"'`$\\]+\1$/u;
2093
+ /**
2094
+ * Returns the lazy imports of the release's own modules in a chunk's code, in order.
2095
+ */
2096
+ function lazyImportsOf(code) {
2097
+ const { dynamicImports } = parseSync("chunk.js", code).module;
2098
+ return dynamicImports.flatMap(({ end, moduleRequest, start }) => {
2099
+ const literal = code.slice(moduleRequest.start, moduleRequest.end);
2100
+ return RELATIVE.test(literal) ? [{
2101
+ end,
2102
+ literal,
2103
+ start
2104
+ }] : [];
2105
+ });
2106
+ }
2107
+ /**
2108
+ * Returns a chunk's code with each lazy import of a module of the release called through
2109
+ * `preloaded`, or undefined for a chunk without such an import.
2110
+ *
2111
+ * @remarks
2112
+ * The call passes the import's own string literal twice, as the chunk writes it. The literal
2113
+ * contains a relative path alone, so the rewrite constructs no code from an escaped value.
2114
+ * @param code - The chunk's code.
2115
+ * @param chunk - The chunk's file name, for the error message.
2116
+ * @throws {@link Error} When the code already uses the name under which the rewrite imports
2117
+ * `preloaded`.
2118
+ */
2119
+ function withPreloads(code, chunk) {
2120
+ const found = lazyImportsOf(code);
2121
+ if (found.length === 0) return void 0;
2122
+ if (code.includes("__stealth_preloaded__")) throw new Error(`The chunk ${chunk} already uses the name ${PRELOADED}. The build imports preloaded under that name.`);
2123
+ const rewritten = found.toReversed().reduce((text, { end, literal, start }) => {
2124
+ const call = `${PRELOADED}(import.meta.url, ${literal}, () => import(${literal}))`;
2125
+ return `${text.slice(0, start)}${call}${text.slice(end)}`;
2126
+ }, code);
2127
+ return `import { preloaded as ${PRELOADED} } from "${SDK}";\n${rewritten}`;
2128
+ }
2129
+ //#endregion
2130
+ //#region src/release/plugin.ts
2131
+ /**
2132
+ * Builds a plugin's web package as a release that a page loads at run time. The release contains
2133
+ * the package's modules, its catalogues and `plugin.json`, which describes them.
2134
+ */
2135
+ /**
2136
+ * Path from the package's directory to the directory that the build writes a release to. The
2137
+ * directory is beside `dist`, because a pack empties `dist` before it writes.
2138
+ */
2139
+ const RELEASE_DIRECTORY = "dist-release";
2140
+ /**
2141
+ * Path from the package's directory to the manifest module when the options leave it out.
2142
+ */
2143
+ const MANIFEST = "src/manifest.ts";
2144
+ /**
2145
+ * Returns the library build configuration of a release.
2146
+ *
2147
+ * @remarks
2148
+ * The build writes the manifest module as `manifest.js` with its exports, and every other module
2149
+ * under `assets` with a hash in its name. Rolldown builds a module that the manifest shares with
2150
+ * a lazy module into `manifest.js`, so the loader imports the manifest with one request. A
2151
+ * library build leaves out Vite's preload helper and inlines every file that a module imports. It
2152
+ * also leaves `process.env.NODE_ENV` as written, so the configuration defines `NODE_ENV` from the
2153
+ * build's mode. The configuration minifies the output, because a page loads a release and no
2154
+ * bundler reads it.
2155
+ * @param root - The package's directory.
2156
+ * @param manifest - The manifest module's path from the package's directory.
2157
+ * @param mode - The build's mode.
2158
+ */
2159
+ function configOf(root, manifest, mode) {
2160
+ return {
2161
+ build: {
2162
+ copyPublicDir: false,
2163
+ emptyOutDir: true,
2164
+ lib: {
2165
+ entry: { manifest: join(root, manifest) },
2166
+ formats: ["es"]
2167
+ },
2168
+ outDir: RELEASE_DIRECTORY,
2169
+ rolldownOptions: {
2170
+ output: {
2171
+ chunkFileNames: "assets/[name]-[hash].js",
2172
+ entryFileNames: "[name].js",
2173
+ minify: true
2174
+ },
2175
+ preserveEntrySignatures: "allow-extension"
2176
+ }
2177
+ },
2178
+ define: { "process.env.NODE_ENV": JSON.stringify(mode) }
2179
+ };
2180
+ }
2181
+ /**
2182
+ * Refuses a package whose preset declares a recipe, and returns the plugin's id.
2183
+ *
2184
+ * @throws {@link Error} When the preset declares a recipe or the manifest module does not export a
2185
+ * manifest.
2186
+ */
2187
+ async function identified(state, manifest) {
2188
+ const through = await importer({
2189
+ conditions: state.conditions,
2190
+ root: state.root
2191
+ });
2192
+ try {
2193
+ await refuseRecipes(through, state.root, state.conditions);
2194
+ return await releasedId(through, state.root, manifest);
2195
+ } finally {
2196
+ await through.close();
2197
+ }
2198
+ }
2199
+ /**
2200
+ * Returns the chunks of a release's bundle.
2201
+ *
2202
+ * @throws {@link Error} When the bundle contains a file other than a module. A page does not load a
2203
+ * stylesheet or any other such file from a release.
2204
+ */
2205
+ function chunksOf(bundle) {
2206
+ const files = Object.values(bundle);
2207
+ const assets = files.filter((file) => file.type === "asset").map(({ fileName }) => fileName);
2208
+ if (assets.length > 0) throw new Error(`The release contains ${assets.join(", ")}. A page does not load such a file from a release. A release's styles come from the page's stylesheet, and the build inlines every other file that a module imports.`);
2209
+ return files.filter((file) => file.type === "chunk");
2210
+ }
2211
+ /**
2212
+ * Returns the version in the manifest of the package in a directory, or the empty string when the
2213
+ * manifest has no version.
2214
+ */
2215
+ function versionAt(root) {
2216
+ const version = manifestAt(root)?.["version"];
2217
+ return typeof version === "string" ? version : "";
2218
+ }
2219
+ /**
2220
+ * Returns the static imports of each chunk of the release, keyed by the chunk's path. The imports
2221
+ * of a chunk are the chunks and provided specifiers that it imports. They include
2222
+ * `@stealthscale/sdk-core` for a chunk whose lazy imports the build calls through `preloaded`.
2223
+ */
2224
+ function importsOf(chunks, bundle) {
2225
+ const imports = chunks.map(({ dynamicImports, fileName, imports: own }) => {
2226
+ return [fileName, dynamicImports.some((name) => Object.hasOwn(bundle, name)) && !own.includes("@stealthscale/sdk-core") ? [...own, SDK] : own];
2227
+ });
2228
+ return Object.fromEntries(imports.toSorted(([one], [two]) => one.localeCompare(two)));
2229
+ }
2230
+ /**
2231
+ * Returns a map from the name of each provided package that the release's chunks import to its
2232
+ * caret range.
2233
+ */
2234
+ function requiresOf(state, imports) {
2235
+ const required = Object.values(imports).flat().flatMap((specifier) => {
2236
+ const provided = state.imported.get(specifier);
2237
+ return provided === void 0 ? [] : [[provided.name, caretOf(provided.version)]];
2238
+ });
2239
+ return Object.fromEntries(required.toSorted(([one], [two]) => one.localeCompare(two)));
2240
+ }
2241
+ /**
2242
+ * Returns a map from the path of each file of the release except `plugin.json` to its integrity.
2243
+ */
2244
+ function filesOf(chunks, catalogues) {
2245
+ const files = [...chunks.map(({ code, fileName }) => [fileName, integrityOf(code)]), ...catalogues.map(({ fileName, source }) => [fileName, integrityOf(source)])];
2246
+ return Object.fromEntries(files.toSorted(([one], [two]) => one.localeCompare(two)));
2247
+ }
2248
+ /**
2249
+ * Creates the plugin that builds a plugin's web package as a release.
2250
+ *
2251
+ * @remarks
2252
+ * The build is a library build whose one entry is the module that exports the manifest. The build
2253
+ * keeps every import of a provided specifier as an import by name, and the page's import map
2254
+ * resolves it. It calls each lazy import of a module of the release through `preloaded` of
2255
+ * `@stealthscale/sdk-core`. `preloaded` passes the module's URL to the page's preloader before
2256
+ * the import. The build fails on an import of a provided package that the page does not map. It
2257
+ * also fails on a stylesheet or any other file that is not a module. The plugin writes the
2258
+ * catalogue of the plugin's namespace in each language as `locales/<language>.json`. It writes
2259
+ * `plugin.json` with the plugin's id and version, its languages, each module's static imports,
2260
+ * the caret range of each provided package it imports, and the integrity of every other file. It
2261
+ * fails a package whose preset declares a recipe.
2262
+ * @param options - The manifest module's path.
2263
+ */
2264
+ function release(options = {}) {
2265
+ const manifest = options.manifest ?? MANIFEST;
2266
+ const state = {
2267
+ imported: /* @__PURE__ */ new Map(),
2268
+ root: process.cwd()
2269
+ };
2270
+ return {
2271
+ apply: "build",
2272
+ /**
2273
+ * Reads the plugin's id from its manifest, and refuses a preset that declares a recipe.
2274
+ *
2275
+ * @throws {@link Error} When the configuration lacks the catalogue plugin, the preset declares a
2276
+ * recipe, or the manifest module does not export a manifest.
2277
+ */
2278
+ async buildStart() {
2279
+ const { api } = state;
2280
+ if (api === void 0) throw new Error("The configuration lacks the stealth:i18n plugin. The release build reads the release's catalogues from that plugin. Add the layers of @stealthscale/vite-config-i18n.");
2281
+ state.started = {
2282
+ api,
2283
+ id: await identified(state, manifest)
2284
+ };
2285
+ },
2286
+ /**
2287
+ * Returns the release's build configuration.
2288
+ */
2289
+ config(config, { mode }) {
2290
+ return configOf(config.root ?? process.cwd(), manifest, mode);
2291
+ },
2292
+ /**
2293
+ * Records the package's directory, the conditions and the catalogue plugin's api.
2294
+ */
2295
+ configResolved(config) {
2296
+ state.api = cataloguesOf(config.plugins);
2297
+ state.conditions = config.ssr.resolve?.conditions;
2298
+ state.root = config.root;
2299
+ },
2300
+ enforce: "pre",
2301
+ generateBundle: {
2302
+ /**
2303
+ * Writes the release's catalogues and its `plugin.json` after every other plugin has written
2304
+ * its files.
2305
+ *
2306
+ * @throws {@link Error} When the bundle contains a file other than a module, the namespace
2307
+ * lacks a catalogue in the fallback language, or the description is malformed.
2308
+ */
2309
+ handler(_options, bundle) {
2310
+ const { api, id } = state.started;
2311
+ const chunks = chunksOf(bundle);
2312
+ const imports = importsOf(chunks, bundle);
2313
+ const catalogues = releaseCatalogues(api, id);
2314
+ const description = describedOf({
2315
+ files: filesOf(chunks, catalogues.files),
2316
+ id,
2317
+ imports,
2318
+ languages: catalogues.languages,
2319
+ requires: requiresOf(state, imports),
2320
+ version: versionAt(state.root)
2321
+ });
2322
+ for (const { fileName, source } of catalogues.files) this.emitFile({
2323
+ fileName,
2324
+ source,
2325
+ type: "asset"
2326
+ });
2327
+ this.emitFile({
2328
+ fileName: RELEASE_FILE,
2329
+ source: description,
2330
+ type: "asset"
2331
+ });
2332
+ },
2333
+ order: "post"
2334
+ },
2335
+ name: "stealth:release",
2336
+ /**
2337
+ * Calls each lazy import of a module of the release through `preloaded`, and records the
2338
+ * package that exports it.
2339
+ *
2340
+ * @throws {@link Error} When the chunk already uses the name under which the rewrite imports
2341
+ * `preloaded`, or when the manifest module has no installed version of
2342
+ * `@stealthscale/sdk-core`.
2343
+ */
2344
+ renderChunk(code, chunk) {
2345
+ const rewritten = withPreloads(code, chunk.fileName);
2346
+ if (rewritten === void 0) return null;
2347
+ state.imported.set(SDK, providedImportOf(SDK, join(state.root, manifest)));
2348
+ return rewritten;
2349
+ },
2350
+ /**
2351
+ * Keeps an import of a provided specifier as an import by name, and records its package.
2352
+ *
2353
+ * @throws {@link Error} When the page does not map the specifier, or its package is not
2354
+ * installed with a version.
2355
+ */
2356
+ resolveId(source, importing) {
2357
+ const provided = importing === void 0 ? void 0 : providedImportOf(source, importing);
2358
+ if (provided === void 0) return null;
2359
+ state.imported.set(source, provided);
2360
+ return {
2361
+ external: true,
2362
+ id: source
2363
+ };
2364
+ }
2365
+ };
2366
+ }
2367
+ //#endregion
2368
+ export { ID, RELEASE_DIRECTORY, product, provided, release };