@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/README.md +126 -10
- package/client.d.ts +7 -1
- package/dist/cyclonedx/bom.json +1 -1
- package/dist/index.d.mts +84 -6
- package/dist/index.mjs +1614 -193
- package/package.json +14 -14
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,246 @@
|
|
|
1
|
-
import {
|
|
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
|
|
136
|
-
*
|
|
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
|
-
*
|
|
145
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
172
|
-
|
|
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
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
"
|
|
529
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
* `
|
|
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
|
-
*
|
|
996
|
+
* The message the plugin throws when the options name both pages, which both serve `index.html`.
|
|
631
997
|
*/
|
|
632
|
-
const
|
|
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
|
|
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
|
|
810
|
-
*
|
|
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
|
|
818
|
-
return
|
|
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 `
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
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
|
-
|
|
887
|
-
|
|
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
|
|
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`,
|
|
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
|
|
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`,
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|