@deepseek-ai/dsh-client-modules 0.1.1-rc.2 → 0.1.2-alpha.3
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.i18n.yaml +2 -2
- package/README.md +107 -7
- package/README.zh.md +110 -10
- package/lib/client.js +67 -15
- package/lib/index.js +470 -102
- package/lib/types/client/manifest.d.ts +36 -15
- package/lib/types/client/system.d.ts +5 -3
- package/lib/types/index.d.ts +66 -17
- package/package.json +6 -9
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { Service } from "@deepseek-ai/cordis";
|
|
7
7
|
//#region lib/types/client/manifest.js
|
|
8
8
|
/**
|
|
@@ -67,11 +67,12 @@ function stripClientSuffix(spec) {
|
|
|
67
67
|
* Node half of the client module system (`dsh.client` dual-face package): scans
|
|
68
68
|
* the host Loader's entries for packages declaring `dsh.client`, composes the
|
|
69
69
|
* `window.__DSH_BOOT__` entry graph (wire single source: {@link WebBootEntry}
|
|
70
|
-
* in `./client/manifest.ts`) in module-graph order, serves
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* injection table, and provides the
|
|
74
|
-
* half's registration/notification
|
|
70
|
+
* in `./client/manifest.ts`) in module-graph order, serves one-or-more-plugin
|
|
71
|
+
* combo scripts plus their source maps,
|
|
72
|
+
* contributes the registration facade, application preloads, bootstrap scripts,
|
|
73
|
+
* and graph to the webserver's index injection table, and provides the
|
|
74
|
+
* `clientModuleHost` service (the HMR node half's registration/notification
|
|
75
|
+
* face).
|
|
75
76
|
*
|
|
76
77
|
* Scanning is incremental per package — there is no full-rescan code path.
|
|
77
78
|
* Every cordis `internal/plugin` emission (fiber construction/disposal) marks
|
|
@@ -79,9 +80,10 @@ function stripClientSuffix(spec) {
|
|
|
79
80
|
* against the live loader entries. The activation pass seeds the same dirty
|
|
80
81
|
* set with all current entries and flushes synchronously, so first scan and
|
|
81
82
|
* steady state share one implementation. Package metadata (including the
|
|
82
|
-
* negative "not a client package" verdict) is cached per
|
|
83
|
-
*
|
|
84
|
-
*
|
|
83
|
+
* negative "not a client package" verdict) is cached per Loader specifier and
|
|
84
|
+
* owning-tree base URL until restart. The manifest package name identifies
|
|
85
|
+
* the browser module; distinct active Loader sources for that package are a
|
|
86
|
+
* composition error. Bundle content changes reach the graph only through
|
|
85
87
|
* {@link ClientModuleRegistry.rebuilt}.
|
|
86
88
|
* @module @deepseek-ai/dsh-client-modules
|
|
87
89
|
*/
|
|
@@ -116,6 +118,24 @@ var ClientPackageCompositionError = class extends AggregateError {
|
|
|
116
118
|
super(failures, lines.join("\n"));
|
|
117
119
|
}
|
|
118
120
|
};
|
|
121
|
+
/** Versioned code is immutable; mismatched revisions are rejected instead of serving newer bytes. */
|
|
122
|
+
const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
123
|
+
/** Generated request URLs stay below conservative browser and intermediary request-target limits. */
|
|
124
|
+
const MAX_COMBO_URL_BYTES = 3 * 1024;
|
|
125
|
+
const HASH_REVISION_LENGTH = 12;
|
|
126
|
+
const COMBO_REVISION_PLACEHOLDER = "0".repeat(HASH_REVISION_LENGTH);
|
|
127
|
+
/** Source-map trailer emitted by tsdown at the end of every client bundle. */
|
|
128
|
+
const SOURCE_MAP_TRAILER = /(?:\r?\n)?\/\/# sourceMappingURL=[^\r\n]*(?:\r?\n)?$/;
|
|
129
|
+
/** Debugger source name appended to page bundles in the WebWorker image. */
|
|
130
|
+
const SOURCE_URL_TRAILER = /(?:\r?\n)?\/\/# sourceURL=([^\r\n]+)(?:\r?\n)?$/;
|
|
131
|
+
/** Return a bare package-root specifier, excluding package subpaths and path-like entries. */
|
|
132
|
+
function exactPackageSpecifier(specifier) {
|
|
133
|
+
if (specifier.startsWith("@")) {
|
|
134
|
+
const parts = specifier.split("/");
|
|
135
|
+
return parts.length === 2 && parts.every(Boolean) ? specifier : void 0;
|
|
136
|
+
}
|
|
137
|
+
return specifier.length > 0 && !specifier.includes("/") ? specifier : void 0;
|
|
138
|
+
}
|
|
119
139
|
/** Narrow an unknown parsed JSON value to the `dsh.client` declaration, throwing on malformed fields. */
|
|
120
140
|
function parseDshClient(pkgName, value) {
|
|
121
141
|
if (value === void 0) return void 0;
|
|
@@ -144,15 +164,172 @@ function clientExportOf(pkgName, exportsField) {
|
|
|
144
164
|
}
|
|
145
165
|
throw new Error(`client-modules: ${pkgName} exports["./client"] must be a string or an object with a string default`);
|
|
146
166
|
}
|
|
147
|
-
/** sha1 content hash shortened to 12 hex chars (
|
|
167
|
+
/** sha1 content hash shortened to 12 hex chars (combo / graph / rebuilt-artifact rev). */
|
|
148
168
|
function shortHash(input) {
|
|
149
|
-
return createHash("sha1").update(input).digest("hex").slice(0,
|
|
169
|
+
return createHash("sha1").update(input).digest("hex").slice(0, HASH_REVISION_LENGTH);
|
|
170
|
+
}
|
|
171
|
+
/** Hash several response fields without allowing bytes to move across field boundaries. */
|
|
172
|
+
function framedHash(domain, parts) {
|
|
173
|
+
const hash = createHash("sha1").update(domain).update("\0");
|
|
174
|
+
for (const part of parts) hash.update(`${String(part.byteLength)}:`).update(part);
|
|
175
|
+
return hash.digest("hex").slice(0, HASH_REVISION_LENGTH);
|
|
176
|
+
}
|
|
177
|
+
/** Hash every artifact input served after HMR observes one plugin change. */
|
|
178
|
+
function artifactRevision(bundle, sourceMap) {
|
|
179
|
+
return framedHash("plugin-artifact", sourceMap === void 0 ? [bundle] : [bundle, sourceMap.body]);
|
|
180
|
+
}
|
|
181
|
+
/** Address one ordered plugin-file list through the shared combo route. */
|
|
182
|
+
function comboUrl(ids, rev, sourceMap = false) {
|
|
183
|
+
return `/plugins/??${ids.map((id) => `${id}/client.js${sourceMap ? ".map" : ""}`).join(",")}&rev=${rev}`;
|
|
184
|
+
}
|
|
185
|
+
/** Measure the longer map-form URL used to partition a startup resource list. */
|
|
186
|
+
function projectedComboUrlBytes(records) {
|
|
187
|
+
return Buffer.byteLength(comboUrl(records.map((record) => record.entry.id), COMBO_REVISION_PLACEHOLDER, true));
|
|
188
|
+
}
|
|
189
|
+
/** Partition one phase in graph order without allowing a generated URL above the protocol limit. */
|
|
190
|
+
function partitionComboRecords(records) {
|
|
191
|
+
const chunks = [];
|
|
192
|
+
let current = [];
|
|
193
|
+
for (const record of records) {
|
|
194
|
+
const candidate = [...current, record];
|
|
195
|
+
if (projectedComboUrlBytes(candidate) <= MAX_COMBO_URL_BYTES) {
|
|
196
|
+
current = candidate;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (current.length === 0) throw new Error(`client-modules: ${record.entry.id} exceeds the ${String(MAX_COMBO_URL_BYTES)}-byte combo URL limit`);
|
|
200
|
+
chunks.push(current);
|
|
201
|
+
current = [record];
|
|
202
|
+
if (projectedComboUrlBytes(current) > MAX_COMBO_URL_BYTES) throw new Error(`client-modules: ${record.entry.id} exceeds the ${String(MAX_COMBO_URL_BYTES)}-byte combo URL limit`);
|
|
203
|
+
}
|
|
204
|
+
if (current.length > 0) chunks.push(current);
|
|
205
|
+
return chunks;
|
|
206
|
+
}
|
|
207
|
+
/** Remove bundle-local debug directives and retain their stable generated-file name. */
|
|
208
|
+
function comboSource(record) {
|
|
209
|
+
let source = record.bundle.toString("utf8");
|
|
210
|
+
const sourceUrl = SOURCE_URL_TRAILER.exec(source)?.[1];
|
|
211
|
+
source = source.replace(SOURCE_URL_TRAILER, "").replace(SOURCE_MAP_TRAILER, "");
|
|
212
|
+
if (!source.endsWith("\n")) source += "\n";
|
|
213
|
+
const fallbackSource = sourceUrl === void 0 ? `/plugins/${record.entry.id}/client.js` : /^(?:[A-Za-z][A-Za-z\d+.-]*:|\/)/.test(sourceUrl) ? sourceUrl : `/${sourceUrl}`;
|
|
214
|
+
return {
|
|
215
|
+
source,
|
|
216
|
+
fallbackSource
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** Stamp a combo script's absolute indexed-map URL onto its executable bytes. */
|
|
220
|
+
function comboScript(input, sourceMapUrl) {
|
|
221
|
+
return Buffer.from(sourceMapUrl === void 0 ? input : `${input}//# sourceMappingURL=${sourceMapUrl}\n`);
|
|
222
|
+
}
|
|
223
|
+
/** Parse an optional source-map artifact; missing maps do not prevent plugin execution. */
|
|
224
|
+
function sourceMapSnapshot(clientPath) {
|
|
225
|
+
let body;
|
|
226
|
+
try {
|
|
227
|
+
body = readFileSync(`${clientPath}.map`);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error.code === "ENOENT") return void 0;
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
const value = JSON.parse(body.toString("utf8"));
|
|
233
|
+
const parsed = typeof value === "object" && value !== null ? value : void 0;
|
|
234
|
+
if (parsed === void 0 || parsed.version !== 3 || !Array.isArray(parsed.sources) || parsed.sources.some((source) => typeof source !== "string") || !Array.isArray(parsed.names) || parsed.names.some((name) => typeof name !== "string") || typeof parsed.mappings !== "string") throw new Error(`client-modules: ${clientPath}.map is not a regular Source Map v3 object`);
|
|
235
|
+
return {
|
|
236
|
+
body,
|
|
237
|
+
parsed
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/** Count generated lines while assembling indexed-map section offsets. */
|
|
241
|
+
function newlineCount(value) {
|
|
242
|
+
let count = 0;
|
|
243
|
+
for (const char of value) if (char === "\n") count += 1;
|
|
244
|
+
return count;
|
|
245
|
+
}
|
|
246
|
+
/** Resolve section sources against their original per-plugin map URL before combo relocation. */
|
|
247
|
+
function comboSectionMap(record) {
|
|
248
|
+
const original = record.sourceMap?.parsed;
|
|
249
|
+
/* v8 ignore next -- callers add sections only for records with a source map. */
|
|
250
|
+
if (original === void 0) throw new Error(`client-modules: source map missing for ${record.entry.id}`);
|
|
251
|
+
const sourcePaths = original.sources;
|
|
252
|
+
const sourceRoot = typeof original.sourceRoot === "string" ? original.sourceRoot : "";
|
|
253
|
+
const base = new URL(`/plugins/${record.entry.id}/client.js.map`, "http://dsh.invalid");
|
|
254
|
+
const relocated = sourcePaths.map((source) => {
|
|
255
|
+
const separator = sourceRoot !== "" && !sourceRoot.endsWith("/") && !source.startsWith("/") ? "/" : "";
|
|
256
|
+
const resolved = new URL(`${sourceRoot}${separator}${source}`, base);
|
|
257
|
+
return resolved.origin === base.origin ? `${resolved.pathname}${resolved.search}${resolved.hash}` : resolved.href;
|
|
258
|
+
});
|
|
259
|
+
const section = {
|
|
260
|
+
...original,
|
|
261
|
+
sources: relocated
|
|
262
|
+
};
|
|
263
|
+
delete section.sourceRoot;
|
|
264
|
+
return section;
|
|
265
|
+
}
|
|
266
|
+
/** Map each generated line to the same line in a bundled JavaScript source. */
|
|
267
|
+
function identitySectionMap(source, sourceUrl) {
|
|
268
|
+
const mappings = Array.from({ length: newlineCount(source) }, (_, index) => index === 0 ? "AAAA" : "AACA").join(";");
|
|
269
|
+
return {
|
|
270
|
+
version: 3,
|
|
271
|
+
names: [],
|
|
272
|
+
sources: [sourceUrl],
|
|
273
|
+
sourcesContent: [source],
|
|
274
|
+
mappings
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/** Concatenate one or more factory registrations and compose their maps as indexed sections. */
|
|
278
|
+
function buildCombo(records, revision) {
|
|
279
|
+
let source = "";
|
|
280
|
+
const sections = [];
|
|
281
|
+
let line = 0;
|
|
282
|
+
for (const record of records) {
|
|
283
|
+
const prepared = comboSource(record);
|
|
284
|
+
const section = record.sourceMap === void 0 ? identitySectionMap(prepared.source, prepared.fallbackSource) : comboSectionMap(record);
|
|
285
|
+
sections.push({
|
|
286
|
+
offset: {
|
|
287
|
+
line,
|
|
288
|
+
column: 0
|
|
289
|
+
},
|
|
290
|
+
map: section
|
|
291
|
+
});
|
|
292
|
+
const bundle = `${prepared.source};\n`;
|
|
293
|
+
source += bundle;
|
|
294
|
+
line += newlineCount(bundle);
|
|
295
|
+
}
|
|
296
|
+
const sourceMap = Buffer.from(`${JSON.stringify({
|
|
297
|
+
version: 3,
|
|
298
|
+
file: "client.js",
|
|
299
|
+
sections
|
|
300
|
+
})}\n`);
|
|
301
|
+
const sourceBytes = Buffer.from(source);
|
|
302
|
+
const rev = revision ?? framedHash("combo", [sourceBytes, sourceMap]);
|
|
303
|
+
const entries = records.map((record) => record.entry.id);
|
|
304
|
+
const url = comboUrl(entries, rev);
|
|
305
|
+
const sourceMapUrl = comboUrl(entries, rev, true);
|
|
306
|
+
return {
|
|
307
|
+
url,
|
|
308
|
+
rev,
|
|
309
|
+
entries,
|
|
310
|
+
script: comboScript(source, sourceMapUrl),
|
|
311
|
+
sourceMap,
|
|
312
|
+
sourceMapUrl
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** Add initial-load scheduling metadata to a combo artifact. */
|
|
316
|
+
function buildBatch(phase, records) {
|
|
317
|
+
const artifact = buildCombo(records);
|
|
318
|
+
return {
|
|
319
|
+
...artifact,
|
|
320
|
+
descriptor: {
|
|
321
|
+
phase,
|
|
322
|
+
url: artifact.url,
|
|
323
|
+
rev: artifact.rev,
|
|
324
|
+
entries: artifact.entries
|
|
325
|
+
}
|
|
326
|
+
};
|
|
150
327
|
}
|
|
151
328
|
/** Graph row for one bundle rev (url carries the rev as its cache-busting query). */
|
|
152
329
|
function graphRow(id, rev, fields) {
|
|
153
330
|
return {
|
|
154
331
|
id,
|
|
155
|
-
url:
|
|
332
|
+
url: comboUrl([id], rev),
|
|
156
333
|
rev,
|
|
157
334
|
...fields.inject !== void 0 ? { inject: fields.inject } : {},
|
|
158
335
|
...fields.immediately ? { immediately: true } : {},
|
|
@@ -194,17 +371,18 @@ function orderByModuleGraph(entries) {
|
|
|
194
371
|
}
|
|
195
372
|
/** Bootstrap package whose ordinary client bundle supplies the module-system implementation. */
|
|
196
373
|
const CLIENT_MODULES_ID = "@deepseek-ai/dsh-client-modules";
|
|
197
|
-
/**
|
|
198
|
-
const PARSER_PRELOAD_IDS = [CLIENT_MODULES_ID
|
|
374
|
+
/** Dynamic bundles grouped into the parser bootstrap batch before the Vite shell. */
|
|
375
|
+
const PARSER_PRELOAD_IDS = [CLIENT_MODULES_ID];
|
|
199
376
|
/**
|
|
200
377
|
* The boot protocol as index injection rows. The inline registration queue
|
|
201
|
-
* precedes
|
|
202
|
-
* `
|
|
378
|
+
* precedes the application-batch preload and the blocking bootstrap batch. Its
|
|
379
|
+
* `create()` method materializes the modules
|
|
203
380
|
* bundle, delegates construction to that bundle, and leaves the same facade
|
|
204
381
|
* in live-registration mode. The graph global follows before the shell reads
|
|
205
382
|
* it.
|
|
206
383
|
* @param graph - the composed entry graph.
|
|
207
|
-
* @returns head rows in execution order: queue script,
|
|
384
|
+
* @returns head rows in execution order: queue script, application preloads,
|
|
385
|
+
* blocking bootstrap scripts, graph global.
|
|
208
386
|
*/
|
|
209
387
|
function bootInjections(graph) {
|
|
210
388
|
const queue = `(()=>{
|
|
@@ -229,24 +407,28 @@ window.__ModuleLoader__={
|
|
|
229
407
|
}
|
|
230
408
|
}
|
|
231
409
|
})()`;
|
|
232
|
-
const
|
|
410
|
+
const bootstrap = graph.batches.filter((batch) => batch.phase === "bootstrap");
|
|
411
|
+
const application = graph.batches.filter((batch) => batch.phase === "application");
|
|
412
|
+
const rows = [{
|
|
413
|
+
kind: "script",
|
|
414
|
+
placement: "head",
|
|
415
|
+
text: queue
|
|
416
|
+
}];
|
|
417
|
+
for (const batch of application) rows.push({
|
|
418
|
+
kind: "script-preload",
|
|
419
|
+
src: batch.url
|
|
420
|
+
});
|
|
421
|
+
for (const batch of bootstrap) rows.push({
|
|
233
422
|
kind: "script-src",
|
|
234
423
|
placement: "head",
|
|
235
|
-
src:
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
...preload,
|
|
244
|
-
{
|
|
245
|
-
kind: "global",
|
|
246
|
-
name: "__DSH_BOOT__",
|
|
247
|
-
value: graph
|
|
248
|
-
}
|
|
249
|
-
];
|
|
424
|
+
src: batch.url
|
|
425
|
+
});
|
|
426
|
+
rows.push({
|
|
427
|
+
kind: "global",
|
|
428
|
+
name: "__DSH_BOOT__",
|
|
429
|
+
value: graph
|
|
430
|
+
});
|
|
431
|
+
return rows;
|
|
250
432
|
}
|
|
251
433
|
/**
|
|
252
434
|
* The web plugin table service: incremental `dsh.client` scan + wire composition
|
|
@@ -258,11 +440,17 @@ window.__ModuleLoader__={
|
|
|
258
440
|
var ClientModuleRegistry = class extends Service {
|
|
259
441
|
static inject = ["webServer", "loader"];
|
|
260
442
|
table = /* @__PURE__ */ new Map();
|
|
443
|
+
sources = /* @__PURE__ */ new Map();
|
|
261
444
|
pkgMeta = /* @__PURE__ */ new Map();
|
|
262
445
|
rebuildListeners = /* @__PURE__ */ new Set();
|
|
263
446
|
graphListeners = /* @__PURE__ */ new Set();
|
|
264
447
|
dirty = /* @__PURE__ */ new Set();
|
|
265
|
-
|
|
448
|
+
initialRevisionNonce = randomBytes(8).toString("hex");
|
|
449
|
+
nextInitialRevision = 0;
|
|
450
|
+
responses = /* @__PURE__ */ new Map();
|
|
451
|
+
batchResponses = /* @__PURE__ */ new Map();
|
|
452
|
+
/** One prior graph generation covers a request racing the HMR recomposition that replaced its URL. */
|
|
453
|
+
previousBatchResponses = /* @__PURE__ */ new Map();
|
|
266
454
|
flushQueued = false;
|
|
267
455
|
composed;
|
|
268
456
|
/**
|
|
@@ -271,9 +459,6 @@ var ClientModuleRegistry = class extends Service {
|
|
|
271
459
|
*/
|
|
272
460
|
constructor(ctx) {
|
|
273
461
|
super(ctx, "clientModules");
|
|
274
|
-
if (ctx.baseUrl === void 0) throw new Error("client-modules: ctx.baseUrl is unset — the node half needs the config-tree anchor to resolve plugin packages");
|
|
275
|
-
const require = createRequire(ctx.baseUrl);
|
|
276
|
-
this.resolvePkgJson = (spec) => require.resolve(`${spec}/package.json`);
|
|
277
462
|
ctx.on("internal/plugin", (fiber) => {
|
|
278
463
|
const entryName = fiber.entry?.options.name;
|
|
279
464
|
if (entryName === void 0) return;
|
|
@@ -317,6 +502,18 @@ var ClientModuleRegistry = class extends Service {
|
|
|
317
502
|
return this.table.get(id)?.meta.clientPath;
|
|
318
503
|
}
|
|
319
504
|
/**
|
|
505
|
+
* Filesystem baseline captured before an entry's current bytes were read.
|
|
506
|
+
* HMR compares it with the live files when installing a watch, so a write
|
|
507
|
+
* between startup composition and watch installation cannot disappear into
|
|
508
|
+
* the watcher's initial state.
|
|
509
|
+
* @param id - entry id (package name).
|
|
510
|
+
* @returns the path and baseline, or undefined for an unknown id.
|
|
511
|
+
*/
|
|
512
|
+
artifactBaseline(id) {
|
|
513
|
+
const baseline = this.table.get(id)?.baseline;
|
|
514
|
+
return baseline === void 0 ? void 0 : { ...baseline };
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
320
517
|
* Re-hash one bundle (the HMR watch's registration hook — the only entry
|
|
321
518
|
* point through which bundle content changes reach the graph).
|
|
322
519
|
* @param id - entry id (package name).
|
|
@@ -325,9 +522,16 @@ var ClientModuleRegistry = class extends Service {
|
|
|
325
522
|
rebuilt(id) {
|
|
326
523
|
const record = this.table.get(id);
|
|
327
524
|
if (record === void 0) return void 0;
|
|
328
|
-
const
|
|
525
|
+
const baseline = this.captureArtifactBaseline(record.meta.clientPath);
|
|
526
|
+
const bundle = readFileSync(record.meta.clientPath);
|
|
527
|
+
const sourceMap = this.readSourceMapSnapshot(record.meta.clientPath);
|
|
528
|
+
const rev = artifactRevision(bundle, sourceMap);
|
|
529
|
+
record.baseline = baseline;
|
|
329
530
|
if (rev === record.entry.rev) return rev;
|
|
330
531
|
record.entry = graphRow(id, rev, record.meta);
|
|
532
|
+
record.bundle = bundle;
|
|
533
|
+
if (sourceMap === void 0) delete record.sourceMap;
|
|
534
|
+
else record.sourceMap = sourceMap;
|
|
331
535
|
this.composed = this.compose();
|
|
332
536
|
for (const notify of this.rebuildListeners) try {
|
|
333
537
|
notify(id, rev);
|
|
@@ -362,9 +566,46 @@ var ClientModuleRegistry = class extends Service {
|
|
|
362
566
|
}
|
|
363
567
|
compose() {
|
|
364
568
|
const entries = orderByModuleGraph([...this.table.values()].map((record) => record.entry));
|
|
569
|
+
const bootstrap = PARSER_PRELOAD_IDS.map((id) => this.table.get(id)).filter((record) => record !== void 0);
|
|
570
|
+
const bootstrapIds = new Set(bootstrap.map((record) => record.entry.id));
|
|
571
|
+
const application = entries.filter((entry) => !bootstrapIds.has(entry.id)).map((entry) => this.table.get(entry.id)).filter((record) => record !== void 0);
|
|
572
|
+
const artifacts = [];
|
|
573
|
+
for (const records of partitionComboRecords(bootstrap)) artifacts.push(buildBatch("bootstrap", records));
|
|
574
|
+
for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records));
|
|
575
|
+
const batchResponses = /* @__PURE__ */ new Map();
|
|
576
|
+
for (const artifact of artifacts) {
|
|
577
|
+
batchResponses.set(artifact.descriptor.url, {
|
|
578
|
+
body: artifact.script,
|
|
579
|
+
contentType: "text/javascript; charset=utf-8"
|
|
580
|
+
});
|
|
581
|
+
batchResponses.set(artifact.sourceMapUrl, {
|
|
582
|
+
body: artifact.sourceMap,
|
|
583
|
+
contentType: "application/json; charset=utf-8"
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
const responses = new Map(batchResponses);
|
|
587
|
+
for (const record of this.table.values()) {
|
|
588
|
+
const artifact = buildCombo([record], record.entry.rev);
|
|
589
|
+
responses.set(artifact.url, {
|
|
590
|
+
body: artifact.script,
|
|
591
|
+
contentType: "text/javascript; charset=utf-8"
|
|
592
|
+
});
|
|
593
|
+
responses.set(artifact.sourceMapUrl, {
|
|
594
|
+
body: artifact.sourceMap,
|
|
595
|
+
contentType: "application/json; charset=utf-8"
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
this.previousBatchResponses = this.batchResponses;
|
|
599
|
+
this.batchResponses = batchResponses;
|
|
600
|
+
this.responses = responses;
|
|
601
|
+
const batches = artifacts.map((artifact) => artifact.descriptor);
|
|
365
602
|
return {
|
|
366
|
-
rev: shortHash(JSON.stringify(
|
|
367
|
-
|
|
603
|
+
rev: shortHash(JSON.stringify({
|
|
604
|
+
entries,
|
|
605
|
+
batches
|
|
606
|
+
})),
|
|
607
|
+
entries,
|
|
608
|
+
batches
|
|
368
609
|
};
|
|
369
610
|
}
|
|
370
611
|
notifyGraphChanged() {
|
|
@@ -374,64 +615,202 @@ var ClientModuleRegistry = class extends Service {
|
|
|
374
615
|
this.ctx.logger.error(error);
|
|
375
616
|
}
|
|
376
617
|
}
|
|
377
|
-
resolveMeta(
|
|
378
|
-
const
|
|
618
|
+
resolveMeta(loaderName, baseUrl) {
|
|
619
|
+
const sourceKey = this.sourceKey(loaderName, baseUrl);
|
|
620
|
+
const cached = this.pkgMeta.get(sourceKey);
|
|
379
621
|
if (cached !== void 0) return cached;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
} catch {
|
|
384
|
-
this.pkgMeta.set(pkgName, null);
|
|
622
|
+
const located = this.locatePkgJson(loaderName, baseUrl);
|
|
623
|
+
if (located === void 0) {
|
|
624
|
+
this.pkgMeta.set(sourceKey, null);
|
|
385
625
|
return null;
|
|
386
626
|
}
|
|
627
|
+
const { packageName, path: pkgPath } = located;
|
|
387
628
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
388
629
|
const dsh = pkg.dsh;
|
|
389
|
-
const decl = parseDshClient(
|
|
630
|
+
const decl = parseDshClient(packageName, dsh !== null && typeof dsh === "object" ? dsh.client : void 0);
|
|
390
631
|
if (decl === void 0 || decl.platform !== "web") {
|
|
391
|
-
this.pkgMeta.set(
|
|
632
|
+
this.pkgMeta.set(sourceKey, null);
|
|
392
633
|
return null;
|
|
393
634
|
}
|
|
394
|
-
const clientRel = clientExportOf(
|
|
395
|
-
if (clientRel === void 0) throw new Error(`client-modules: ${
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
635
|
+
const clientRel = clientExportOf(packageName, pkg.exports);
|
|
636
|
+
if (clientRel === void 0) throw new Error(`client-modules: ${packageName} declares dsh.client but exports no "./client" bundle`);
|
|
637
|
+
const resolved = {
|
|
638
|
+
packageName,
|
|
639
|
+
meta: {
|
|
640
|
+
clientPath: join(dirname(pkgPath), clientRel),
|
|
641
|
+
...decl.inject !== void 0 ? { inject: decl.inject } : {},
|
|
642
|
+
external: decl.external ?? [],
|
|
643
|
+
immediately: decl.immediately === true
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
this.pkgMeta.set(sourceKey, resolved);
|
|
647
|
+
return resolved;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Locate the manifest of the package the Loader mounts for a row. The row's
|
|
651
|
+
* module location is authoritative: the specifier resolves through the same
|
|
652
|
+
* Loader resolution that imported the row's host half — including any
|
|
653
|
+
* active ESM hooks — and the nearest ancestor manifest declaring the name
|
|
654
|
+
* owns the module. Tree-anchored `require` resolution remains only for
|
|
655
|
+
* runtimes without Node internals.
|
|
656
|
+
* @param loaderName - module specifier of the loader row.
|
|
657
|
+
* @param baseUrl - resolution base of the tree that owns the row.
|
|
658
|
+
* @returns the manifest path, or `undefined` when the name resolves to no package root.
|
|
659
|
+
*/
|
|
660
|
+
locatePkgJson(loaderName, baseUrl) {
|
|
661
|
+
if (loaderName.startsWith("cordis:")) return void 0;
|
|
662
|
+
const pathLike = loaderName.startsWith(".") || loaderName.startsWith("file:") || isAbsolute(loaderName);
|
|
663
|
+
const expectedPackageName = pathLike ? void 0 : exactPackageSpecifier(loaderName);
|
|
664
|
+
if (!pathLike && expectedPackageName === void 0) return void 0;
|
|
665
|
+
const internal = this.ctx.loader.internal;
|
|
666
|
+
if (internal === void 0 || typeof Reflect.get(internal, "resolveSync") !== "function") {
|
|
667
|
+
if (expectedPackageName === void 0) {
|
|
668
|
+
const moduleUrl = loaderName.startsWith("file:") ? loaderName : isAbsolute(loaderName) ? pathToFileURL(loaderName).href : new URL(loaderName, baseUrl).href;
|
|
669
|
+
return this.nearestPackage(moduleUrl);
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
return {
|
|
673
|
+
path: createRequire(baseUrl).resolve(`${expectedPackageName}/package.json`),
|
|
674
|
+
packageName: expectedPackageName
|
|
675
|
+
};
|
|
676
|
+
} catch {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
let moduleUrl;
|
|
681
|
+
try {
|
|
682
|
+
moduleUrl = internal.version === "v2" ? internal.resolveSync(baseUrl, {
|
|
683
|
+
specifier: loaderName,
|
|
684
|
+
attributes: {}
|
|
685
|
+
}).url : internal.resolveSync(loaderName, baseUrl, {}).url;
|
|
686
|
+
} catch {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
return this.nearestPackage(moduleUrl, expectedPackageName);
|
|
690
|
+
}
|
|
691
|
+
nearestPackage(moduleUrl, expectedPackageName) {
|
|
692
|
+
if (!moduleUrl.startsWith("file:")) return void 0;
|
|
693
|
+
let dir = dirname(fileURLToPath(moduleUrl));
|
|
694
|
+
while (true) {
|
|
695
|
+
const candidate = join(dir, "package.json");
|
|
696
|
+
if (existsSync(candidate)) try {
|
|
697
|
+
const name = JSON.parse(readFileSync(candidate, "utf8")).name;
|
|
698
|
+
if (typeof name === "string" && (expectedPackageName === void 0 || name === expectedPackageName)) return {
|
|
699
|
+
path: candidate,
|
|
700
|
+
packageName: name
|
|
701
|
+
};
|
|
702
|
+
} catch {}
|
|
703
|
+
const parent = dirname(dir);
|
|
704
|
+
if (parent === dir) break;
|
|
705
|
+
dir = parent;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
sourceKey(loaderName, baseUrl) {
|
|
709
|
+
return `${baseUrl}\0${loaderName}`;
|
|
710
|
+
}
|
|
711
|
+
/** Capture the bundle stats before reading its bytes. */
|
|
712
|
+
captureArtifactBaseline(clientPath) {
|
|
713
|
+
const bundle = statSync(clientPath);
|
|
714
|
+
return {
|
|
715
|
+
path: clientPath,
|
|
716
|
+
mtimeMs: bundle.mtimeMs,
|
|
717
|
+
size: bundle.size
|
|
401
718
|
};
|
|
402
|
-
|
|
403
|
-
|
|
719
|
+
}
|
|
720
|
+
/** Allocate an opaque initial row revision without inspecting artifact bytes. */
|
|
721
|
+
allocateInitialRevision() {
|
|
722
|
+
return `${this.initialRevisionNonce}-${String(this.nextInitialRevision++)}`;
|
|
404
723
|
}
|
|
405
724
|
/**
|
|
406
|
-
* Read the activation-time bundle
|
|
725
|
+
* Read the activation-time bundle and optional source-map snapshots.
|
|
407
726
|
* @param pkgName - package that declares the client bundle.
|
|
408
727
|
* @param clientPath - absolute path of the built client artifact.
|
|
409
|
-
* @returns the
|
|
728
|
+
* @returns the immutable bytes plus the pre-read filesystem baseline.
|
|
410
729
|
* @throws {MissingClientBundleError} when the read fails with `ENOENT`; other filesystem errors are rethrown unchanged.
|
|
411
730
|
*/
|
|
412
|
-
|
|
731
|
+
initialBundleSnapshot(pkgName, clientPath) {
|
|
413
732
|
try {
|
|
414
|
-
|
|
733
|
+
const baseline = this.captureArtifactBaseline(clientPath);
|
|
734
|
+
const bundle = readFileSync(clientPath);
|
|
735
|
+
const sourceMap = this.readSourceMapSnapshot(clientPath);
|
|
736
|
+
return {
|
|
737
|
+
bundle,
|
|
738
|
+
baseline,
|
|
739
|
+
...sourceMap === void 0 ? {} : { sourceMap }
|
|
740
|
+
};
|
|
415
741
|
} catch (error) {
|
|
416
742
|
if (error.code !== "ENOENT") throw error;
|
|
417
743
|
throw new MissingClientBundleError(pkgName, clientPath, error);
|
|
418
744
|
}
|
|
419
745
|
}
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
746
|
+
/** Treat a missing, torn, or malformed development map as an identity-mapped artifact revision. */
|
|
747
|
+
readSourceMapSnapshot(clientPath) {
|
|
748
|
+
try {
|
|
749
|
+
return sourceMapSnapshot(clientPath);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
this.ctx.logger.warn(error);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/** Reconcile one entry name against the live Loader sources. @returns whether the table changed. */
|
|
756
|
+
processOne(entryName, onError) {
|
|
757
|
+
const nextSources = /* @__PURE__ */ new Map();
|
|
758
|
+
for (const entry of this.ctx.loader.entries()) {
|
|
759
|
+
if (entry.options.name !== entryName || entry.fiber === void 0 || entry.disabled) continue;
|
|
760
|
+
const source = this.resolveSource(entry);
|
|
761
|
+
if (source !== void 0) nextSources.set(source.sourceKey, source);
|
|
762
|
+
}
|
|
763
|
+
const affectedPackages = /* @__PURE__ */ new Set();
|
|
764
|
+
for (const [sourceKey, source] of this.sources) {
|
|
765
|
+
if (source.loaderName !== entryName) continue;
|
|
766
|
+
affectedPackages.add(source.packageName);
|
|
767
|
+
if (!nextSources.has(sourceKey)) this.sources.delete(sourceKey);
|
|
768
|
+
}
|
|
769
|
+
for (const [sourceKey, source] of nextSources) {
|
|
770
|
+
affectedPackages.add(source.packageName);
|
|
771
|
+
this.sources.set(sourceKey, source);
|
|
772
|
+
}
|
|
773
|
+
let changed = false;
|
|
774
|
+
for (const packageName of affectedPackages) try {
|
|
775
|
+
if (this.reconcilePackage(packageName)) changed = true;
|
|
776
|
+
} catch (error) {
|
|
777
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
778
|
+
}
|
|
779
|
+
return changed;
|
|
780
|
+
}
|
|
781
|
+
resolveSource(entry) {
|
|
782
|
+
const loaderName = entry.options.name;
|
|
783
|
+
const baseUrl = entry.parent.tree.ctx.baseUrl;
|
|
784
|
+
if (baseUrl === void 0) throw new Error(`client-modules: loader entry ${loaderName} has no resolution base URL`);
|
|
785
|
+
const resolved = this.resolveMeta(loaderName, baseUrl);
|
|
786
|
+
if (resolved === null) return void 0;
|
|
787
|
+
return {
|
|
788
|
+
...resolved,
|
|
789
|
+
loaderName,
|
|
790
|
+
baseUrl,
|
|
791
|
+
sourceKey: this.sourceKey(loaderName, baseUrl)
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
reconcilePackage(packageName) {
|
|
795
|
+
const sources = [];
|
|
796
|
+
for (const source of this.sources.values()) if (source.packageName === packageName) sources.push(source);
|
|
797
|
+
if (sources.length > 1) {
|
|
798
|
+
const locations = sources.map((source) => `${JSON.stringify(source.loaderName)} from ${source.baseUrl}`).join(", ");
|
|
799
|
+
throw new Error(`client-modules: package ${packageName} resolves from multiple active Loader sources: ${locations}; remove one entry`);
|
|
426
800
|
}
|
|
427
|
-
|
|
428
|
-
if (this.table.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const rev = this.
|
|
432
|
-
this.table.set(
|
|
433
|
-
entry: graphRow(
|
|
434
|
-
|
|
801
|
+
const source = sources[0];
|
|
802
|
+
if (source === void 0) return this.table.delete(packageName);
|
|
803
|
+
if (this.table.get(packageName)?.sourceKey === source.sourceKey) return false;
|
|
804
|
+
const snapshot = this.initialBundleSnapshot(packageName, source.meta.clientPath);
|
|
805
|
+
const rev = this.allocateInitialRevision();
|
|
806
|
+
this.table.set(packageName, {
|
|
807
|
+
entry: graphRow(packageName, rev, source.meta),
|
|
808
|
+
loaderName: source.loaderName,
|
|
809
|
+
sourceKey: source.sourceKey,
|
|
810
|
+
meta: source.meta,
|
|
811
|
+
bundle: snapshot.bundle,
|
|
812
|
+
baseline: snapshot.baseline,
|
|
813
|
+
...snapshot.sourceMap === void 0 ? {} : { sourceMap: snapshot.sourceMap }
|
|
435
814
|
});
|
|
436
815
|
return true;
|
|
437
816
|
}
|
|
@@ -440,7 +819,7 @@ var ClientModuleRegistry = class extends Service {
|
|
|
440
819
|
for (const entryName of [...this.dirty]) {
|
|
441
820
|
this.dirty.delete(entryName);
|
|
442
821
|
try {
|
|
443
|
-
if (this.processOne(entryName)) changed = true;
|
|
822
|
+
if (this.processOne(entryName, onError)) changed = true;
|
|
444
823
|
} catch (error) {
|
|
445
824
|
onError(error instanceof Error ? error : new Error(String(error)));
|
|
446
825
|
}
|
|
@@ -456,37 +835,26 @@ var ClientModuleRegistry = class extends Service {
|
|
|
456
835
|
this.composed = composed;
|
|
457
836
|
this.notifyGraphChanged();
|
|
458
837
|
}
|
|
459
|
-
serveBundle =
|
|
838
|
+
serveBundle = (req, res) => {
|
|
460
839
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
461
840
|
res.writeHead(405);
|
|
462
841
|
res.end();
|
|
463
842
|
return;
|
|
464
843
|
}
|
|
465
844
|
/* v8 ignore next -- `?? '/'` arm: node:http always sets url on server requests. */
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
const isSourceMap = pathname.startsWith(prefix) && pathname.endsWith(mapSuffix);
|
|
471
|
-
const suffix = isSourceMap ? mapSuffix : bundleSuffix;
|
|
472
|
-
const clientPath = pathname.startsWith(prefix) && pathname.endsWith(suffix) ? this.clientPath(pathname.slice(9, -suffix.length)) : void 0;
|
|
473
|
-
const path = clientPath === void 0 ? void 0 : `${clientPath}${isSourceMap ? ".map" : ""}`;
|
|
474
|
-
if (path === void 0) {
|
|
475
|
-
res.writeHead(404);
|
|
476
|
-
res.end();
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
try {
|
|
480
|
-
const body = await readFile(path);
|
|
845
|
+
const requestUrl = new URL(req.url ?? "/", "http://x");
|
|
846
|
+
const resourceUrl = `${requestUrl.pathname}${requestUrl.search}`;
|
|
847
|
+
const response = this.responses.get(resourceUrl) ?? this.previousBatchResponses.get(resourceUrl);
|
|
848
|
+
if (response !== void 0) {
|
|
481
849
|
res.writeHead(200, {
|
|
482
|
-
"content-type":
|
|
483
|
-
"cache-control":
|
|
850
|
+
"content-type": response.contentType,
|
|
851
|
+
"cache-control": IMMUTABLE_CACHE
|
|
484
852
|
});
|
|
485
|
-
res.end(body);
|
|
486
|
-
|
|
487
|
-
res.writeHead(404);
|
|
488
|
-
res.end();
|
|
853
|
+
res.end(req.method === "HEAD" ? void 0 : response.body);
|
|
854
|
+
return;
|
|
489
855
|
}
|
|
856
|
+
res.writeHead(404);
|
|
857
|
+
res.end();
|
|
490
858
|
};
|
|
491
859
|
};
|
|
492
860
|
//#endregion
|