@crazx/dsh-client-modules 0.1.1-rc.2.zw.1 → 0.1.2-alpha.3.zw.2
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 +489 -103
- package/lib/types/client/manifest.d.ts +36 -15
- package/lib/types/client/system.d.ts +5 -3
- package/lib/types/index.d.ts +68 -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,40 @@ 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
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A scoped republish keeps the unscoped name and changes only the scope
|
|
141
|
+
* (`@deepseek-ai/dsh-client-modules` vs `@crazx/dsh-client-modules`). The
|
|
142
|
+
* graph row id stays the Loader specifier; this match only locates the
|
|
143
|
+
* owning manifest.
|
|
144
|
+
*/
|
|
145
|
+
function manifestOwnsLoaderPackage(manifestName, expectedPackageName) {
|
|
146
|
+
if (manifestName === expectedPackageName) return true;
|
|
147
|
+
if (!manifestName.startsWith("@") || !expectedPackageName.startsWith("@")) return false;
|
|
148
|
+
const manifestSlash = manifestName.indexOf("/");
|
|
149
|
+
const expectedSlash = expectedPackageName.indexOf("/");
|
|
150
|
+
if (manifestSlash === -1 || expectedSlash === -1) return false;
|
|
151
|
+
const manifestSuffix = manifestName.slice(manifestSlash + 1);
|
|
152
|
+
const expectedSuffix = expectedPackageName.slice(expectedSlash + 1);
|
|
153
|
+
return expectedSuffix.length > 0 && manifestSuffix === expectedSuffix;
|
|
154
|
+
}
|
|
119
155
|
/** Narrow an unknown parsed JSON value to the `dsh.client` declaration, throwing on malformed fields. */
|
|
120
156
|
function parseDshClient(pkgName, value) {
|
|
121
157
|
if (value === void 0) return void 0;
|
|
@@ -144,15 +180,172 @@ function clientExportOf(pkgName, exportsField) {
|
|
|
144
180
|
}
|
|
145
181
|
throw new Error(`client-modules: ${pkgName} exports["./client"] must be a string or an object with a string default`);
|
|
146
182
|
}
|
|
147
|
-
/** sha1 content hash shortened to 12 hex chars (
|
|
183
|
+
/** sha1 content hash shortened to 12 hex chars (combo / graph / rebuilt-artifact rev). */
|
|
148
184
|
function shortHash(input) {
|
|
149
|
-
return createHash("sha1").update(input).digest("hex").slice(0,
|
|
185
|
+
return createHash("sha1").update(input).digest("hex").slice(0, HASH_REVISION_LENGTH);
|
|
186
|
+
}
|
|
187
|
+
/** Hash several response fields without allowing bytes to move across field boundaries. */
|
|
188
|
+
function framedHash(domain, parts) {
|
|
189
|
+
const hash = createHash("sha1").update(domain).update("\0");
|
|
190
|
+
for (const part of parts) hash.update(`${String(part.byteLength)}:`).update(part);
|
|
191
|
+
return hash.digest("hex").slice(0, HASH_REVISION_LENGTH);
|
|
192
|
+
}
|
|
193
|
+
/** Hash every artifact input served after HMR observes one plugin change. */
|
|
194
|
+
function artifactRevision(bundle, sourceMap) {
|
|
195
|
+
return framedHash("plugin-artifact", sourceMap === void 0 ? [bundle] : [bundle, sourceMap.body]);
|
|
196
|
+
}
|
|
197
|
+
/** Address one ordered plugin-file list through the shared combo route. */
|
|
198
|
+
function comboUrl(ids, rev, sourceMap = false) {
|
|
199
|
+
return `/plugins/??${ids.map((id) => `${id}/client.js${sourceMap ? ".map" : ""}`).join(",")}&rev=${rev}`;
|
|
200
|
+
}
|
|
201
|
+
/** Measure the longer map-form URL used to partition a startup resource list. */
|
|
202
|
+
function projectedComboUrlBytes(records) {
|
|
203
|
+
return Buffer.byteLength(comboUrl(records.map((record) => record.entry.id), COMBO_REVISION_PLACEHOLDER, true));
|
|
204
|
+
}
|
|
205
|
+
/** Partition one phase in graph order without allowing a generated URL above the protocol limit. */
|
|
206
|
+
function partitionComboRecords(records) {
|
|
207
|
+
const chunks = [];
|
|
208
|
+
let current = [];
|
|
209
|
+
for (const record of records) {
|
|
210
|
+
const candidate = [...current, record];
|
|
211
|
+
if (projectedComboUrlBytes(candidate) <= MAX_COMBO_URL_BYTES) {
|
|
212
|
+
current = candidate;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (current.length === 0) throw new Error(`client-modules: ${record.entry.id} exceeds the ${String(MAX_COMBO_URL_BYTES)}-byte combo URL limit`);
|
|
216
|
+
chunks.push(current);
|
|
217
|
+
current = [record];
|
|
218
|
+
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`);
|
|
219
|
+
}
|
|
220
|
+
if (current.length > 0) chunks.push(current);
|
|
221
|
+
return chunks;
|
|
222
|
+
}
|
|
223
|
+
/** Remove bundle-local debug directives and retain their stable generated-file name. */
|
|
224
|
+
function comboSource(record) {
|
|
225
|
+
let source = record.bundle.toString("utf8");
|
|
226
|
+
const sourceUrl = SOURCE_URL_TRAILER.exec(source)?.[1];
|
|
227
|
+
source = source.replace(SOURCE_URL_TRAILER, "").replace(SOURCE_MAP_TRAILER, "");
|
|
228
|
+
if (!source.endsWith("\n")) source += "\n";
|
|
229
|
+
const fallbackSource = sourceUrl === void 0 ? `/plugins/${record.entry.id}/client.js` : /^(?:[A-Za-z][A-Za-z\d+.-]*:|\/)/.test(sourceUrl) ? sourceUrl : `/${sourceUrl}`;
|
|
230
|
+
return {
|
|
231
|
+
source,
|
|
232
|
+
fallbackSource
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** Stamp a combo script's absolute indexed-map URL onto its executable bytes. */
|
|
236
|
+
function comboScript(input, sourceMapUrl) {
|
|
237
|
+
return Buffer.from(sourceMapUrl === void 0 ? input : `${input}//# sourceMappingURL=${sourceMapUrl}\n`);
|
|
238
|
+
}
|
|
239
|
+
/** Parse an optional source-map artifact; missing maps do not prevent plugin execution. */
|
|
240
|
+
function sourceMapSnapshot(clientPath) {
|
|
241
|
+
let body;
|
|
242
|
+
try {
|
|
243
|
+
body = readFileSync(`${clientPath}.map`);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error.code === "ENOENT") return void 0;
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
const value = JSON.parse(body.toString("utf8"));
|
|
249
|
+
const parsed = typeof value === "object" && value !== null ? value : void 0;
|
|
250
|
+
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`);
|
|
251
|
+
return {
|
|
252
|
+
body,
|
|
253
|
+
parsed
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** Count generated lines while assembling indexed-map section offsets. */
|
|
257
|
+
function newlineCount(value) {
|
|
258
|
+
let count = 0;
|
|
259
|
+
for (const char of value) if (char === "\n") count += 1;
|
|
260
|
+
return count;
|
|
261
|
+
}
|
|
262
|
+
/** Resolve section sources against their original per-plugin map URL before combo relocation. */
|
|
263
|
+
function comboSectionMap(record) {
|
|
264
|
+
const original = record.sourceMap?.parsed;
|
|
265
|
+
/* v8 ignore next -- callers add sections only for records with a source map. */
|
|
266
|
+
if (original === void 0) throw new Error(`client-modules: source map missing for ${record.entry.id}`);
|
|
267
|
+
const sourcePaths = original.sources;
|
|
268
|
+
const sourceRoot = typeof original.sourceRoot === "string" ? original.sourceRoot : "";
|
|
269
|
+
const base = new URL(`/plugins/${record.entry.id}/client.js.map`, "http://dsh.invalid");
|
|
270
|
+
const relocated = sourcePaths.map((source) => {
|
|
271
|
+
const separator = sourceRoot !== "" && !sourceRoot.endsWith("/") && !source.startsWith("/") ? "/" : "";
|
|
272
|
+
const resolved = new URL(`${sourceRoot}${separator}${source}`, base);
|
|
273
|
+
return resolved.origin === base.origin ? `${resolved.pathname}${resolved.search}${resolved.hash}` : resolved.href;
|
|
274
|
+
});
|
|
275
|
+
const section = {
|
|
276
|
+
...original,
|
|
277
|
+
sources: relocated
|
|
278
|
+
};
|
|
279
|
+
delete section.sourceRoot;
|
|
280
|
+
return section;
|
|
281
|
+
}
|
|
282
|
+
/** Map each generated line to the same line in a bundled JavaScript source. */
|
|
283
|
+
function identitySectionMap(source, sourceUrl) {
|
|
284
|
+
const mappings = Array.from({ length: newlineCount(source) }, (_, index) => index === 0 ? "AAAA" : "AACA").join(";");
|
|
285
|
+
return {
|
|
286
|
+
version: 3,
|
|
287
|
+
names: [],
|
|
288
|
+
sources: [sourceUrl],
|
|
289
|
+
sourcesContent: [source],
|
|
290
|
+
mappings
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/** Concatenate one or more factory registrations and compose their maps as indexed sections. */
|
|
294
|
+
function buildCombo(records, revision) {
|
|
295
|
+
let source = "";
|
|
296
|
+
const sections = [];
|
|
297
|
+
let line = 0;
|
|
298
|
+
for (const record of records) {
|
|
299
|
+
const prepared = comboSource(record);
|
|
300
|
+
const section = record.sourceMap === void 0 ? identitySectionMap(prepared.source, prepared.fallbackSource) : comboSectionMap(record);
|
|
301
|
+
sections.push({
|
|
302
|
+
offset: {
|
|
303
|
+
line,
|
|
304
|
+
column: 0
|
|
305
|
+
},
|
|
306
|
+
map: section
|
|
307
|
+
});
|
|
308
|
+
const bundle = `${prepared.source};\n`;
|
|
309
|
+
source += bundle;
|
|
310
|
+
line += newlineCount(bundle);
|
|
311
|
+
}
|
|
312
|
+
const sourceMap = Buffer.from(`${JSON.stringify({
|
|
313
|
+
version: 3,
|
|
314
|
+
file: "client.js",
|
|
315
|
+
sections
|
|
316
|
+
})}\n`);
|
|
317
|
+
const sourceBytes = Buffer.from(source);
|
|
318
|
+
const rev = revision ?? framedHash("combo", [sourceBytes, sourceMap]);
|
|
319
|
+
const entries = records.map((record) => record.entry.id);
|
|
320
|
+
const url = comboUrl(entries, rev);
|
|
321
|
+
const sourceMapUrl = comboUrl(entries, rev, true);
|
|
322
|
+
return {
|
|
323
|
+
url,
|
|
324
|
+
rev,
|
|
325
|
+
entries,
|
|
326
|
+
script: comboScript(source, sourceMapUrl),
|
|
327
|
+
sourceMap,
|
|
328
|
+
sourceMapUrl
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** Add initial-load scheduling metadata to a combo artifact. */
|
|
332
|
+
function buildBatch(phase, records) {
|
|
333
|
+
const artifact = buildCombo(records);
|
|
334
|
+
return {
|
|
335
|
+
...artifact,
|
|
336
|
+
descriptor: {
|
|
337
|
+
phase,
|
|
338
|
+
url: artifact.url,
|
|
339
|
+
rev: artifact.rev,
|
|
340
|
+
entries: artifact.entries
|
|
341
|
+
}
|
|
342
|
+
};
|
|
150
343
|
}
|
|
151
344
|
/** Graph row for one bundle rev (url carries the rev as its cache-busting query). */
|
|
152
345
|
function graphRow(id, rev, fields) {
|
|
153
346
|
return {
|
|
154
347
|
id,
|
|
155
|
-
url:
|
|
348
|
+
url: comboUrl([id], rev),
|
|
156
349
|
rev,
|
|
157
350
|
...fields.inject !== void 0 ? { inject: fields.inject } : {},
|
|
158
351
|
...fields.immediately ? { immediately: true } : {},
|
|
@@ -194,17 +387,18 @@ function orderByModuleGraph(entries) {
|
|
|
194
387
|
}
|
|
195
388
|
/** Bootstrap package whose ordinary client bundle supplies the module-system implementation. */
|
|
196
389
|
const CLIENT_MODULES_ID = "@deepseek-ai/dsh-client-modules";
|
|
197
|
-
/**
|
|
198
|
-
const PARSER_PRELOAD_IDS = [CLIENT_MODULES_ID
|
|
390
|
+
/** Dynamic bundles grouped into the parser bootstrap batch before the Vite shell. */
|
|
391
|
+
const PARSER_PRELOAD_IDS = [CLIENT_MODULES_ID];
|
|
199
392
|
/**
|
|
200
393
|
* The boot protocol as index injection rows. The inline registration queue
|
|
201
|
-
* precedes
|
|
202
|
-
* `
|
|
394
|
+
* precedes the application-batch preload and the blocking bootstrap batch. Its
|
|
395
|
+
* `create()` method materializes the modules
|
|
203
396
|
* bundle, delegates construction to that bundle, and leaves the same facade
|
|
204
397
|
* in live-registration mode. The graph global follows before the shell reads
|
|
205
398
|
* it.
|
|
206
399
|
* @param graph - the composed entry graph.
|
|
207
|
-
* @returns head rows in execution order: queue script,
|
|
400
|
+
* @returns head rows in execution order: queue script, application preloads,
|
|
401
|
+
* blocking bootstrap scripts, graph global.
|
|
208
402
|
*/
|
|
209
403
|
function bootInjections(graph) {
|
|
210
404
|
const queue = `(()=>{
|
|
@@ -229,24 +423,28 @@ window.__ModuleLoader__={
|
|
|
229
423
|
}
|
|
230
424
|
}
|
|
231
425
|
})()`;
|
|
232
|
-
const
|
|
426
|
+
const bootstrap = graph.batches.filter((batch) => batch.phase === "bootstrap");
|
|
427
|
+
const application = graph.batches.filter((batch) => batch.phase === "application");
|
|
428
|
+
const rows = [{
|
|
429
|
+
kind: "script",
|
|
430
|
+
placement: "head",
|
|
431
|
+
text: queue
|
|
432
|
+
}];
|
|
433
|
+
for (const batch of application) rows.push({
|
|
434
|
+
kind: "script-preload",
|
|
435
|
+
src: batch.url
|
|
436
|
+
});
|
|
437
|
+
for (const batch of bootstrap) rows.push({
|
|
233
438
|
kind: "script-src",
|
|
234
439
|
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
|
-
];
|
|
440
|
+
src: batch.url
|
|
441
|
+
});
|
|
442
|
+
rows.push({
|
|
443
|
+
kind: "global",
|
|
444
|
+
name: "__DSH_BOOT__",
|
|
445
|
+
value: graph
|
|
446
|
+
});
|
|
447
|
+
return rows;
|
|
250
448
|
}
|
|
251
449
|
/**
|
|
252
450
|
* The web plugin table service: incremental `dsh.client` scan + wire composition
|
|
@@ -258,11 +456,17 @@ window.__ModuleLoader__={
|
|
|
258
456
|
var ClientModuleRegistry = class extends Service {
|
|
259
457
|
static inject = ["webServer", "loader"];
|
|
260
458
|
table = /* @__PURE__ */ new Map();
|
|
459
|
+
sources = /* @__PURE__ */ new Map();
|
|
261
460
|
pkgMeta = /* @__PURE__ */ new Map();
|
|
262
461
|
rebuildListeners = /* @__PURE__ */ new Set();
|
|
263
462
|
graphListeners = /* @__PURE__ */ new Set();
|
|
264
463
|
dirty = /* @__PURE__ */ new Set();
|
|
265
|
-
|
|
464
|
+
initialRevisionNonce = randomBytes(8).toString("hex");
|
|
465
|
+
nextInitialRevision = 0;
|
|
466
|
+
responses = /* @__PURE__ */ new Map();
|
|
467
|
+
batchResponses = /* @__PURE__ */ new Map();
|
|
468
|
+
/** One prior graph generation covers a request racing the HMR recomposition that replaced its URL. */
|
|
469
|
+
previousBatchResponses = /* @__PURE__ */ new Map();
|
|
266
470
|
flushQueued = false;
|
|
267
471
|
composed;
|
|
268
472
|
/**
|
|
@@ -271,9 +475,6 @@ var ClientModuleRegistry = class extends Service {
|
|
|
271
475
|
*/
|
|
272
476
|
constructor(ctx) {
|
|
273
477
|
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
478
|
ctx.on("internal/plugin", (fiber) => {
|
|
278
479
|
const entryName = fiber.entry?.options.name;
|
|
279
480
|
if (entryName === void 0) return;
|
|
@@ -317,6 +518,18 @@ var ClientModuleRegistry = class extends Service {
|
|
|
317
518
|
return this.table.get(id)?.meta.clientPath;
|
|
318
519
|
}
|
|
319
520
|
/**
|
|
521
|
+
* Filesystem baseline captured before an entry's current bytes were read.
|
|
522
|
+
* HMR compares it with the live files when installing a watch, so a write
|
|
523
|
+
* between startup composition and watch installation cannot disappear into
|
|
524
|
+
* the watcher's initial state.
|
|
525
|
+
* @param id - entry id (package name).
|
|
526
|
+
* @returns the path and baseline, or undefined for an unknown id.
|
|
527
|
+
*/
|
|
528
|
+
artifactBaseline(id) {
|
|
529
|
+
const baseline = this.table.get(id)?.baseline;
|
|
530
|
+
return baseline === void 0 ? void 0 : { ...baseline };
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
320
533
|
* Re-hash one bundle (the HMR watch's registration hook — the only entry
|
|
321
534
|
* point through which bundle content changes reach the graph).
|
|
322
535
|
* @param id - entry id (package name).
|
|
@@ -325,9 +538,16 @@ var ClientModuleRegistry = class extends Service {
|
|
|
325
538
|
rebuilt(id) {
|
|
326
539
|
const record = this.table.get(id);
|
|
327
540
|
if (record === void 0) return void 0;
|
|
328
|
-
const
|
|
541
|
+
const baseline = this.captureArtifactBaseline(record.meta.clientPath);
|
|
542
|
+
const bundle = readFileSync(record.meta.clientPath);
|
|
543
|
+
const sourceMap = this.readSourceMapSnapshot(record.meta.clientPath);
|
|
544
|
+
const rev = artifactRevision(bundle, sourceMap);
|
|
545
|
+
record.baseline = baseline;
|
|
329
546
|
if (rev === record.entry.rev) return rev;
|
|
330
547
|
record.entry = graphRow(id, rev, record.meta);
|
|
548
|
+
record.bundle = bundle;
|
|
549
|
+
if (sourceMap === void 0) delete record.sourceMap;
|
|
550
|
+
else record.sourceMap = sourceMap;
|
|
331
551
|
this.composed = this.compose();
|
|
332
552
|
for (const notify of this.rebuildListeners) try {
|
|
333
553
|
notify(id, rev);
|
|
@@ -362,9 +582,46 @@ var ClientModuleRegistry = class extends Service {
|
|
|
362
582
|
}
|
|
363
583
|
compose() {
|
|
364
584
|
const entries = orderByModuleGraph([...this.table.values()].map((record) => record.entry));
|
|
585
|
+
const bootstrap = PARSER_PRELOAD_IDS.map((id) => this.table.get(id)).filter((record) => record !== void 0);
|
|
586
|
+
const bootstrapIds = new Set(bootstrap.map((record) => record.entry.id));
|
|
587
|
+
const application = entries.filter((entry) => !bootstrapIds.has(entry.id)).map((entry) => this.table.get(entry.id)).filter((record) => record !== void 0);
|
|
588
|
+
const artifacts = [];
|
|
589
|
+
for (const records of partitionComboRecords(bootstrap)) artifacts.push(buildBatch("bootstrap", records));
|
|
590
|
+
for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records));
|
|
591
|
+
const batchResponses = /* @__PURE__ */ new Map();
|
|
592
|
+
for (const artifact of artifacts) {
|
|
593
|
+
batchResponses.set(artifact.descriptor.url, {
|
|
594
|
+
body: artifact.script,
|
|
595
|
+
contentType: "text/javascript; charset=utf-8"
|
|
596
|
+
});
|
|
597
|
+
batchResponses.set(artifact.sourceMapUrl, {
|
|
598
|
+
body: artifact.sourceMap,
|
|
599
|
+
contentType: "application/json; charset=utf-8"
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
const responses = new Map(batchResponses);
|
|
603
|
+
for (const record of this.table.values()) {
|
|
604
|
+
const artifact = buildCombo([record], record.entry.rev);
|
|
605
|
+
responses.set(artifact.url, {
|
|
606
|
+
body: artifact.script,
|
|
607
|
+
contentType: "text/javascript; charset=utf-8"
|
|
608
|
+
});
|
|
609
|
+
responses.set(artifact.sourceMapUrl, {
|
|
610
|
+
body: artifact.sourceMap,
|
|
611
|
+
contentType: "application/json; charset=utf-8"
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
this.previousBatchResponses = this.batchResponses;
|
|
615
|
+
this.batchResponses = batchResponses;
|
|
616
|
+
this.responses = responses;
|
|
617
|
+
const batches = artifacts.map((artifact) => artifact.descriptor);
|
|
365
618
|
return {
|
|
366
|
-
rev: shortHash(JSON.stringify(
|
|
367
|
-
|
|
619
|
+
rev: shortHash(JSON.stringify({
|
|
620
|
+
entries,
|
|
621
|
+
batches
|
|
622
|
+
})),
|
|
623
|
+
entries,
|
|
624
|
+
batches
|
|
368
625
|
};
|
|
369
626
|
}
|
|
370
627
|
notifyGraphChanged() {
|
|
@@ -374,64 +631,204 @@ var ClientModuleRegistry = class extends Service {
|
|
|
374
631
|
this.ctx.logger.error(error);
|
|
375
632
|
}
|
|
376
633
|
}
|
|
377
|
-
resolveMeta(
|
|
378
|
-
const
|
|
634
|
+
resolveMeta(loaderName, baseUrl) {
|
|
635
|
+
const sourceKey = this.sourceKey(loaderName, baseUrl);
|
|
636
|
+
const cached = this.pkgMeta.get(sourceKey);
|
|
379
637
|
if (cached !== void 0) return cached;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
} catch {
|
|
384
|
-
this.pkgMeta.set(pkgName, null);
|
|
638
|
+
const located = this.locatePkgJson(loaderName, baseUrl);
|
|
639
|
+
if (located === void 0) {
|
|
640
|
+
this.pkgMeta.set(sourceKey, null);
|
|
385
641
|
return null;
|
|
386
642
|
}
|
|
643
|
+
const { packageName, path: pkgPath } = located;
|
|
387
644
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
388
645
|
const dsh = pkg.dsh;
|
|
389
|
-
const decl = parseDshClient(
|
|
646
|
+
const decl = parseDshClient(packageName, dsh !== null && typeof dsh === "object" ? dsh.client : void 0);
|
|
390
647
|
if (decl === void 0 || decl.platform !== "web") {
|
|
391
|
-
this.pkgMeta.set(
|
|
648
|
+
this.pkgMeta.set(sourceKey, null);
|
|
392
649
|
return null;
|
|
393
650
|
}
|
|
394
|
-
const clientRel = clientExportOf(
|
|
395
|
-
if (clientRel === void 0) throw new Error(`client-modules: ${
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
651
|
+
const clientRel = clientExportOf(packageName, pkg.exports);
|
|
652
|
+
if (clientRel === void 0) throw new Error(`client-modules: ${packageName} declares dsh.client but exports no "./client" bundle`);
|
|
653
|
+
const resolved = {
|
|
654
|
+
packageName,
|
|
655
|
+
meta: {
|
|
656
|
+
clientPath: join(dirname(pkgPath), clientRel),
|
|
657
|
+
...decl.inject !== void 0 ? { inject: decl.inject } : {},
|
|
658
|
+
external: decl.external ?? [],
|
|
659
|
+
immediately: decl.immediately === true
|
|
660
|
+
}
|
|
401
661
|
};
|
|
402
|
-
this.pkgMeta.set(
|
|
403
|
-
return
|
|
662
|
+
this.pkgMeta.set(sourceKey, resolved);
|
|
663
|
+
return resolved;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Locate the manifest of the package the Loader mounts for a row. The row's
|
|
667
|
+
* module location is authoritative: the specifier resolves through the same
|
|
668
|
+
* Loader resolution that imported the row's host half — including any
|
|
669
|
+
* active ESM hooks — and the nearest ancestor manifest declaring the name,
|
|
670
|
+
* or a scoped republish of the same unscoped name, owns the module. The
|
|
671
|
+
* returned `packageName` is the Loader specifier so the graph row id stays
|
|
672
|
+
* the name the composition and HTML preload list already use. Tree-anchored
|
|
673
|
+
* `require` resolution remains only for runtimes without Node internals.
|
|
674
|
+
* @param loaderName - module specifier of the loader row.
|
|
675
|
+
* @param baseUrl - resolution base of the tree that owns the row.
|
|
676
|
+
* @returns the manifest path, or `undefined` when the name resolves to no package root.
|
|
677
|
+
*/
|
|
678
|
+
locatePkgJson(loaderName, baseUrl) {
|
|
679
|
+
if (loaderName.startsWith("cordis:")) return void 0;
|
|
680
|
+
const pathLike = loaderName.startsWith(".") || loaderName.startsWith("file:") || isAbsolute(loaderName);
|
|
681
|
+
const expectedPackageName = pathLike ? void 0 : exactPackageSpecifier(loaderName);
|
|
682
|
+
if (!pathLike && expectedPackageName === void 0) return void 0;
|
|
683
|
+
const internal = this.ctx.loader.internal;
|
|
684
|
+
if (internal === void 0 || typeof Reflect.get(internal, "resolveSync") !== "function") {
|
|
685
|
+
if (expectedPackageName === void 0) {
|
|
686
|
+
const moduleUrl = loaderName.startsWith("file:") ? loaderName : isAbsolute(loaderName) ? pathToFileURL(loaderName).href : new URL(loaderName, baseUrl).href;
|
|
687
|
+
return this.nearestPackage(moduleUrl);
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
return {
|
|
691
|
+
path: createRequire(baseUrl).resolve(`${expectedPackageName}/package.json`),
|
|
692
|
+
packageName: expectedPackageName
|
|
693
|
+
};
|
|
694
|
+
} catch {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
let moduleUrl;
|
|
699
|
+
try {
|
|
700
|
+
moduleUrl = internal.version === "v2" ? internal.resolveSync(baseUrl, {
|
|
701
|
+
specifier: loaderName,
|
|
702
|
+
attributes: {}
|
|
703
|
+
}).url : internal.resolveSync(loaderName, baseUrl, {}).url;
|
|
704
|
+
} catch {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
return this.nearestPackage(moduleUrl, expectedPackageName);
|
|
708
|
+
}
|
|
709
|
+
nearestPackage(moduleUrl, expectedPackageName) {
|
|
710
|
+
if (!moduleUrl.startsWith("file:")) return void 0;
|
|
711
|
+
let dir = dirname(fileURLToPath(moduleUrl));
|
|
712
|
+
while (true) {
|
|
713
|
+
const candidate = join(dir, "package.json");
|
|
714
|
+
if (existsSync(candidate)) try {
|
|
715
|
+
const name = JSON.parse(readFileSync(candidate, "utf8")).name;
|
|
716
|
+
if (typeof name === "string" && (expectedPackageName === void 0 || manifestOwnsLoaderPackage(name, expectedPackageName))) return {
|
|
717
|
+
path: candidate,
|
|
718
|
+
packageName: expectedPackageName ?? name
|
|
719
|
+
};
|
|
720
|
+
} catch {}
|
|
721
|
+
const parent = dirname(dir);
|
|
722
|
+
if (parent === dir) break;
|
|
723
|
+
dir = parent;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
sourceKey(loaderName, baseUrl) {
|
|
727
|
+
return `${baseUrl}\0${loaderName}`;
|
|
728
|
+
}
|
|
729
|
+
/** Capture the bundle stats before reading its bytes. */
|
|
730
|
+
captureArtifactBaseline(clientPath) {
|
|
731
|
+
const bundle = statSync(clientPath);
|
|
732
|
+
return {
|
|
733
|
+
path: clientPath,
|
|
734
|
+
mtimeMs: bundle.mtimeMs,
|
|
735
|
+
size: bundle.size
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
/** Allocate an opaque initial row revision without inspecting artifact bytes. */
|
|
739
|
+
allocateInitialRevision() {
|
|
740
|
+
return `${this.initialRevisionNonce}-${String(this.nextInitialRevision++)}`;
|
|
404
741
|
}
|
|
405
742
|
/**
|
|
406
|
-
* Read the activation-time bundle
|
|
743
|
+
* Read the activation-time bundle and optional source-map snapshots.
|
|
407
744
|
* @param pkgName - package that declares the client bundle.
|
|
408
745
|
* @param clientPath - absolute path of the built client artifact.
|
|
409
|
-
* @returns the
|
|
746
|
+
* @returns the immutable bytes plus the pre-read filesystem baseline.
|
|
410
747
|
* @throws {MissingClientBundleError} when the read fails with `ENOENT`; other filesystem errors are rethrown unchanged.
|
|
411
748
|
*/
|
|
412
|
-
|
|
749
|
+
initialBundleSnapshot(pkgName, clientPath) {
|
|
413
750
|
try {
|
|
414
|
-
|
|
751
|
+
const baseline = this.captureArtifactBaseline(clientPath);
|
|
752
|
+
const bundle = readFileSync(clientPath);
|
|
753
|
+
const sourceMap = this.readSourceMapSnapshot(clientPath);
|
|
754
|
+
return {
|
|
755
|
+
bundle,
|
|
756
|
+
baseline,
|
|
757
|
+
...sourceMap === void 0 ? {} : { sourceMap }
|
|
758
|
+
};
|
|
415
759
|
} catch (error) {
|
|
416
760
|
if (error.code !== "ENOENT") throw error;
|
|
417
761
|
throw new MissingClientBundleError(pkgName, clientPath, error);
|
|
418
762
|
}
|
|
419
763
|
}
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
764
|
+
/** Treat a missing, torn, or malformed development map as an identity-mapped artifact revision. */
|
|
765
|
+
readSourceMapSnapshot(clientPath) {
|
|
766
|
+
try {
|
|
767
|
+
return sourceMapSnapshot(clientPath);
|
|
768
|
+
} catch (error) {
|
|
769
|
+
this.ctx.logger.warn(error);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/** Reconcile one entry name against the live Loader sources. @returns whether the table changed. */
|
|
774
|
+
processOne(entryName, onError) {
|
|
775
|
+
const nextSources = /* @__PURE__ */ new Map();
|
|
776
|
+
for (const entry of this.ctx.loader.entries()) {
|
|
777
|
+
if (entry.options.name !== entryName || entry.fiber === void 0 || entry.disabled) continue;
|
|
778
|
+
const source = this.resolveSource(entry);
|
|
779
|
+
if (source !== void 0) nextSources.set(source.sourceKey, source);
|
|
780
|
+
}
|
|
781
|
+
const affectedPackages = /* @__PURE__ */ new Set();
|
|
782
|
+
for (const [sourceKey, source] of this.sources) {
|
|
783
|
+
if (source.loaderName !== entryName) continue;
|
|
784
|
+
affectedPackages.add(source.packageName);
|
|
785
|
+
if (!nextSources.has(sourceKey)) this.sources.delete(sourceKey);
|
|
786
|
+
}
|
|
787
|
+
for (const [sourceKey, source] of nextSources) {
|
|
788
|
+
affectedPackages.add(source.packageName);
|
|
789
|
+
this.sources.set(sourceKey, source);
|
|
790
|
+
}
|
|
791
|
+
let changed = false;
|
|
792
|
+
for (const packageName of affectedPackages) try {
|
|
793
|
+
if (this.reconcilePackage(packageName)) changed = true;
|
|
794
|
+
} catch (error) {
|
|
795
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
796
|
+
}
|
|
797
|
+
return changed;
|
|
798
|
+
}
|
|
799
|
+
resolveSource(entry) {
|
|
800
|
+
const loaderName = entry.options.name;
|
|
801
|
+
const baseUrl = entry.parent.tree.ctx.baseUrl;
|
|
802
|
+
if (baseUrl === void 0) throw new Error(`client-modules: loader entry ${loaderName} has no resolution base URL`);
|
|
803
|
+
const resolved = this.resolveMeta(loaderName, baseUrl);
|
|
804
|
+
if (resolved === null) return void 0;
|
|
805
|
+
return {
|
|
806
|
+
...resolved,
|
|
807
|
+
loaderName,
|
|
808
|
+
baseUrl,
|
|
809
|
+
sourceKey: this.sourceKey(loaderName, baseUrl)
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
reconcilePackage(packageName) {
|
|
813
|
+
const sources = [];
|
|
814
|
+
for (const source of this.sources.values()) if (source.packageName === packageName) sources.push(source);
|
|
815
|
+
if (sources.length > 1) {
|
|
816
|
+
const locations = sources.map((source) => `${JSON.stringify(source.loaderName)} from ${source.baseUrl}`).join(", ");
|
|
817
|
+
throw new Error(`client-modules: package ${packageName} resolves from multiple active Loader sources: ${locations}; remove one entry`);
|
|
426
818
|
}
|
|
427
|
-
|
|
428
|
-
if (this.table.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const rev = this.
|
|
432
|
-
this.table.set(
|
|
433
|
-
entry: graphRow(
|
|
434
|
-
|
|
819
|
+
const source = sources[0];
|
|
820
|
+
if (source === void 0) return this.table.delete(packageName);
|
|
821
|
+
if (this.table.get(packageName)?.sourceKey === source.sourceKey) return false;
|
|
822
|
+
const snapshot = this.initialBundleSnapshot(packageName, source.meta.clientPath);
|
|
823
|
+
const rev = this.allocateInitialRevision();
|
|
824
|
+
this.table.set(packageName, {
|
|
825
|
+
entry: graphRow(packageName, rev, source.meta),
|
|
826
|
+
loaderName: source.loaderName,
|
|
827
|
+
sourceKey: source.sourceKey,
|
|
828
|
+
meta: source.meta,
|
|
829
|
+
bundle: snapshot.bundle,
|
|
830
|
+
baseline: snapshot.baseline,
|
|
831
|
+
...snapshot.sourceMap === void 0 ? {} : { sourceMap: snapshot.sourceMap }
|
|
435
832
|
});
|
|
436
833
|
return true;
|
|
437
834
|
}
|
|
@@ -440,7 +837,7 @@ var ClientModuleRegistry = class extends Service {
|
|
|
440
837
|
for (const entryName of [...this.dirty]) {
|
|
441
838
|
this.dirty.delete(entryName);
|
|
442
839
|
try {
|
|
443
|
-
if (this.processOne(entryName)) changed = true;
|
|
840
|
+
if (this.processOne(entryName, onError)) changed = true;
|
|
444
841
|
} catch (error) {
|
|
445
842
|
onError(error instanceof Error ? error : new Error(String(error)));
|
|
446
843
|
}
|
|
@@ -456,38 +853,27 @@ var ClientModuleRegistry = class extends Service {
|
|
|
456
853
|
this.composed = composed;
|
|
457
854
|
this.notifyGraphChanged();
|
|
458
855
|
}
|
|
459
|
-
serveBundle =
|
|
856
|
+
serveBundle = (req, res) => {
|
|
460
857
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
461
858
|
res.writeHead(405);
|
|
462
859
|
res.end();
|
|
463
860
|
return;
|
|
464
861
|
}
|
|
465
862
|
/* 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);
|
|
863
|
+
const requestUrl = new URL(req.url ?? "/", "http://x");
|
|
864
|
+
const resourceUrl = `${requestUrl.pathname}${requestUrl.search}`;
|
|
865
|
+
const response = this.responses.get(resourceUrl) ?? this.previousBatchResponses.get(resourceUrl);
|
|
866
|
+
if (response !== void 0) {
|
|
481
867
|
res.writeHead(200, {
|
|
482
|
-
"content-type":
|
|
483
|
-
"cache-control":
|
|
484
|
-
"content-length": body.length
|
|
868
|
+
"content-type": response.contentType,
|
|
869
|
+
"cache-control": IMMUTABLE_CACHE,
|
|
870
|
+
"content-length": response.body.length
|
|
485
871
|
});
|
|
486
|
-
res.end(body);
|
|
487
|
-
|
|
488
|
-
res.writeHead(404);
|
|
489
|
-
res.end();
|
|
872
|
+
res.end(req.method === "HEAD" ? void 0 : response.body);
|
|
873
|
+
return;
|
|
490
874
|
}
|
|
875
|
+
res.writeHead(404);
|
|
876
|
+
res.end();
|
|
491
877
|
};
|
|
492
878
|
};
|
|
493
879
|
//#endregion
|