@deepseek-ai/dsh-client-modules 0.1.5-rc.2 → 0.1.6-alpha.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 +22 -9
- package/README.zh.md +28 -15
- package/lib/client.js +454 -32
- package/lib/index.js +179 -99
- package/lib/types/client/entries.d.ts +76 -0
- package/lib/types/client/entry-lifecycle.d.ts +14 -0
- package/lib/types/client/index.d.ts +8 -4
- package/lib/types/client/manifest.d.ts +44 -14
- package/lib/types/client/system.d.ts +13 -8
- package/lib/types/index.d.ts +14 -8
- package/package.json +5 -4
package/lib/index.js
CHANGED
|
@@ -50,6 +50,43 @@ function optionalStringArray(subject, field, value) {
|
|
|
50
50
|
return value;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
+
* Narrow an unknown parsed JSON value to the `dsh.client` declaration. Shared
|
|
54
|
+
* by the node half's Loader scan and the roster generator, so both read a
|
|
55
|
+
* package's browser declaration through one validator.
|
|
56
|
+
* @param pkgName - package name used as the diagnostic prefix.
|
|
57
|
+
* @param value - the raw `dsh.client` field of the package manifest.
|
|
58
|
+
* @returns the validated declaration, or undefined when the field is absent.
|
|
59
|
+
* @throws {Error} when the field is present but any member is malformed.
|
|
60
|
+
*/
|
|
61
|
+
function parseDshClient(pkgName, value) {
|
|
62
|
+
if (value === void 0) return void 0;
|
|
63
|
+
if (typeof value !== "object" || value === null) throw new Error(`client-modules: ${pkgName} has a non-object dsh.client declaration`);
|
|
64
|
+
const decl = value;
|
|
65
|
+
if (typeof decl.platform !== "string") throw new Error(`client-modules: ${pkgName} dsh.client.platform must be a string`);
|
|
66
|
+
const inject = optionalStringArray(pkgName, "dsh.client.inject", decl.inject);
|
|
67
|
+
const external = optionalStringArray(pkgName, "dsh.client.external", decl.external);
|
|
68
|
+
if (decl.immediately !== void 0 && typeof decl.immediately !== "boolean") throw new Error(`client-modules: ${pkgName} dsh.client.immediately must be a boolean`);
|
|
69
|
+
return {
|
|
70
|
+
platform: decl.platform,
|
|
71
|
+
...inject !== void 0 ? { inject } : {},
|
|
72
|
+
...external !== void 0 ? { external } : {},
|
|
73
|
+
...decl.immediately !== void 0 ? { immediately: decl.immediately } : {}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The bare package-root specifier `specifier` names, or undefined for a subpath, a path, or any scheme-qualified
|
|
78
|
+
* specifier (`cordis:` builtins, `node:` modules, URLs).
|
|
79
|
+
* @param specifier - Loader row name.
|
|
80
|
+
* @returns the package name, or undefined.
|
|
81
|
+
*/
|
|
82
|
+
function exactPackageSpecifier(specifier) {
|
|
83
|
+
if (specifier.startsWith("@")) {
|
|
84
|
+
const parts = specifier.split("/");
|
|
85
|
+
return parts.length === 2 && parts.every(Boolean) ? specifier : void 0;
|
|
86
|
+
}
|
|
87
|
+
return specifier.length > 0 && !specifier.includes("/") && !specifier.includes(":") ? specifier : void 0;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
53
90
|
* Normalize a module specifier onto the graph row that owns it: a plugin bundle
|
|
54
91
|
* IS its package's client half, so `<id>/client` (the exports subpath external
|
|
55
92
|
* bundles emit) and the bare package name resolve to the same exports. Both the
|
|
@@ -71,7 +108,7 @@ function stripClientSuffix(spec) {
|
|
|
71
108
|
* combo scripts plus their source maps,
|
|
72
109
|
* contributes the registration facade, application preloads, bootstrap scripts,
|
|
73
110
|
* and graph to the webserver's index injection table, and provides the
|
|
74
|
-
* `
|
|
111
|
+
* `clientModules` service (the HMR node half's registration/notification
|
|
75
112
|
* face).
|
|
76
113
|
*
|
|
77
114
|
* Scanning is incremental per package — there is no full-rescan code path.
|
|
@@ -128,30 +165,8 @@ const COMBO_REVISION_PLACEHOLDER = "0".repeat(HASH_REVISION_LENGTH);
|
|
|
128
165
|
const SOURCE_MAP_TRAILER = /(?:\r?\n)?\/\/# sourceMappingURL=[^\r\n]*(?:\r?\n)?$/;
|
|
129
166
|
/** Debugger source name appended to page bundles in the WebWorker image. */
|
|
130
167
|
const SOURCE_URL_TRAILER = /(?:\r?\n)?\/\/# sourceURL=([^\r\n]+)(?:\r?\n)?$/;
|
|
131
|
-
/**
|
|
132
|
-
|
|
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
|
-
/** Narrow an unknown parsed JSON value to the `dsh.client` declaration, throwing on malformed fields. */
|
|
140
|
-
function parseDshClient(pkgName, value) {
|
|
141
|
-
if (value === void 0) return void 0;
|
|
142
|
-
if (typeof value !== "object" || value === null) throw new Error(`client-modules: ${pkgName} has a non-object dsh.client declaration`);
|
|
143
|
-
const decl = value;
|
|
144
|
-
if (typeof decl.platform !== "string") throw new Error(`client-modules: ${pkgName} dsh.client.platform must be a string`);
|
|
145
|
-
const inject = optionalStringArray(pkgName, "dsh.client.inject", decl.inject);
|
|
146
|
-
const external = optionalStringArray(pkgName, "dsh.client.external", decl.external);
|
|
147
|
-
if (decl.immediately !== void 0 && typeof decl.immediately !== "boolean") throw new Error(`client-modules: ${pkgName} dsh.client.immediately must be a boolean`);
|
|
148
|
-
return {
|
|
149
|
-
platform: decl.platform,
|
|
150
|
-
...inject !== void 0 ? { inject } : {},
|
|
151
|
-
...external !== void 0 ? { external } : {},
|
|
152
|
-
...decl.immediately !== void 0 ? { immediately: decl.immediately } : {}
|
|
153
|
-
};
|
|
154
|
-
}
|
|
168
|
+
/** Published package-local client chunk names accepted by the on-demand route. */
|
|
169
|
+
const CLIENT_CHUNK = /^client\.[A-Za-z0-9][A-Za-z0-9._-]*\.js$/;
|
|
155
170
|
/** Resolve `exports["./client"]` to a relative path, accepting the string and one-level conditional forms. */
|
|
156
171
|
function clientExportOf(pkgName, exportsField) {
|
|
157
172
|
if (typeof exportsField !== "object" || exportsField === null) return void 0;
|
|
@@ -174,14 +189,18 @@ function framedHash(domain, parts) {
|
|
|
174
189
|
for (const part of parts) hash.update(`${String(part.byteLength)}:`).update(part);
|
|
175
190
|
return hash.digest("hex").slice(0, HASH_REVISION_LENGTH);
|
|
176
191
|
}
|
|
177
|
-
/** Hash
|
|
178
|
-
function artifactRevision(bundle,
|
|
179
|
-
return framedHash("plugin-artifact",
|
|
192
|
+
/** Hash one completed build generation observed through its entry artifact. */
|
|
193
|
+
function artifactRevision(bundle, baseline) {
|
|
194
|
+
return framedHash("plugin-artifact", [bundle, Buffer.from(String(baseline.mtimeMs))]);
|
|
180
195
|
}
|
|
181
196
|
/** Address one ordered plugin-file list through the shared combo route. */
|
|
182
197
|
function comboUrl(ids, rev, sourceMap = false) {
|
|
183
198
|
return `/plugins/??${ids.map((id) => `${id}/client.js${sourceMap ? ".map" : ""}`).join(",")}&rev=${rev}`;
|
|
184
199
|
}
|
|
200
|
+
/** Address one package-local chunk through the same revision as its entry. */
|
|
201
|
+
function chunkUrl(id, fileName, rev, sourceMap = false) {
|
|
202
|
+
return `/plugins/${id}/${fileName}${sourceMap ? ".map" : ""}?rev=${rev}`;
|
|
203
|
+
}
|
|
185
204
|
/** Measure the longer map-form URL used to partition a startup resource list. */
|
|
186
205
|
function projectedComboUrlBytes(records) {
|
|
187
206
|
return Buffer.byteLength(comboUrl(records.map((record) => record.entry.id), COMBO_REVISION_PLACEHOLDER, true));
|
|
@@ -205,12 +224,12 @@ function partitionComboRecords(records) {
|
|
|
205
224
|
return chunks;
|
|
206
225
|
}
|
|
207
226
|
/** Remove bundle-local debug directives and retain their stable generated-file name. */
|
|
208
|
-
function
|
|
209
|
-
let source =
|
|
227
|
+
function prepareSource(resource) {
|
|
228
|
+
let source = resource.bundle.toString("utf8");
|
|
210
229
|
const sourceUrl = SOURCE_URL_TRAILER.exec(source)?.[1];
|
|
211
230
|
source = source.replace(SOURCE_URL_TRAILER, "").replace(SOURCE_MAP_TRAILER, "");
|
|
212
231
|
if (!source.endsWith("\n")) source += "\n";
|
|
213
|
-
const fallbackSource = sourceUrl === void 0 ? `/plugins/${
|
|
232
|
+
const fallbackSource = sourceUrl === void 0 ? `/plugins/${resource.id}/${resource.fileName}` : /^(?:[A-Za-z][A-Za-z\d+.-]*:|\/)/.test(sourceUrl) ? sourceUrl : `/${sourceUrl}`;
|
|
214
233
|
return {
|
|
215
234
|
source,
|
|
216
235
|
fallbackSource
|
|
@@ -220,8 +239,8 @@ function comboSource(record) {
|
|
|
220
239
|
function comboScript(input, sourceMapUrl) {
|
|
221
240
|
return Buffer.from(sourceMapUrl === void 0 ? input : `${input}//# sourceMappingURL=${sourceMapUrl}\n`);
|
|
222
241
|
}
|
|
223
|
-
/**
|
|
224
|
-
function
|
|
242
|
+
/** Read and parse an optional source map when its combo-map endpoint is requested. */
|
|
243
|
+
function readSourceMap(clientPath) {
|
|
225
244
|
let body;
|
|
226
245
|
try {
|
|
227
246
|
body = readFileSync(`${clientPath}.map`);
|
|
@@ -232,10 +251,7 @@ function sourceMapSnapshot(clientPath) {
|
|
|
232
251
|
const value = JSON.parse(body.toString("utf8"));
|
|
233
252
|
const parsed = typeof value === "object" && value !== null ? value : void 0;
|
|
234
253
|
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
|
-
};
|
|
254
|
+
return parsed;
|
|
239
255
|
}
|
|
240
256
|
/** Count generated lines while assembling indexed-map section offsets. */
|
|
241
257
|
function newlineCount(value) {
|
|
@@ -244,13 +260,10 @@ function newlineCount(value) {
|
|
|
244
260
|
return count;
|
|
245
261
|
}
|
|
246
262
|
/** Resolve section sources against their original per-plugin map URL before combo relocation. */
|
|
247
|
-
function comboSectionMap(
|
|
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}`);
|
|
263
|
+
function comboSectionMap(resource, original) {
|
|
251
264
|
const sourcePaths = original.sources;
|
|
252
265
|
const sourceRoot = typeof original.sourceRoot === "string" ? original.sourceRoot : "";
|
|
253
|
-
const base = new URL(`/plugins/${
|
|
266
|
+
const base = new URL(`/plugins/${resource.id}/client.js.map`, "http://dsh.invalid");
|
|
254
267
|
const relocated = sourcePaths.map((source) => {
|
|
255
268
|
const separator = sourceRoot !== "" && !sourceRoot.endsWith("/") && !source.startsWith("/") ? "/" : "";
|
|
256
269
|
const resolved = new URL(`${sourceRoot}${separator}${source}`, base);
|
|
@@ -274,14 +287,35 @@ function identitySectionMap(source, sourceUrl) {
|
|
|
274
287
|
mappings
|
|
275
288
|
};
|
|
276
289
|
}
|
|
277
|
-
/**
|
|
278
|
-
function
|
|
290
|
+
/** Run one producer in the first requester's microtask, not off-thread, and share its settlement. */
|
|
291
|
+
function lazyBody(produce) {
|
|
292
|
+
let result;
|
|
293
|
+
return () => {
|
|
294
|
+
result ??= Promise.resolve().then(produce);
|
|
295
|
+
return result;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/** Derive one combo revision from the ordered immutable row revisions. */
|
|
299
|
+
function comboRevision(resources) {
|
|
300
|
+
return framedHash("combo", resources.flatMap((resource) => [Buffer.from(resource.id), Buffer.from(resource.rev)]));
|
|
301
|
+
}
|
|
302
|
+
/** Concatenate one or more factory registrations without reading or composing source maps. */
|
|
303
|
+
function buildComboScript(resources, sourceMapUrl) {
|
|
279
304
|
let source = "";
|
|
305
|
+
for (const resource of resources) source += `${prepareSource(resource).source};\n`;
|
|
306
|
+
return comboScript(source, sourceMapUrl);
|
|
307
|
+
}
|
|
308
|
+
/** Compose one indexed map from source-map files read only for this request. */
|
|
309
|
+
function buildComboSourceMap(resources, sourceMapOf, fileName = "client.js") {
|
|
280
310
|
const sections = [];
|
|
281
311
|
let line = 0;
|
|
282
|
-
for (const
|
|
283
|
-
const prepared =
|
|
284
|
-
const
|
|
312
|
+
for (const resource of resources) {
|
|
313
|
+
const prepared = prepareSource(resource);
|
|
314
|
+
const sourceMap = sourceMapOf(resource.clientPath);
|
|
315
|
+
let section = identitySectionMap(prepared.source, prepared.fallbackSource);
|
|
316
|
+
if (sourceMap !== void 0) try {
|
|
317
|
+
section = comboSectionMap(resource, sourceMap);
|
|
318
|
+
} catch {}
|
|
285
319
|
sections.push({
|
|
286
320
|
offset: {
|
|
287
321
|
line,
|
|
@@ -289,32 +323,39 @@ function buildCombo(records, revision) {
|
|
|
289
323
|
},
|
|
290
324
|
map: section
|
|
291
325
|
});
|
|
292
|
-
|
|
293
|
-
source += bundle;
|
|
294
|
-
line += newlineCount(bundle);
|
|
326
|
+
line += newlineCount(`${prepared.source};\n`);
|
|
295
327
|
}
|
|
296
|
-
|
|
328
|
+
return Buffer.from(`${JSON.stringify({
|
|
297
329
|
version: 3,
|
|
298
|
-
file:
|
|
330
|
+
file: fileName,
|
|
299
331
|
sections
|
|
300
332
|
})}\n`);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
333
|
+
}
|
|
334
|
+
/** Describe one combo and defer its executable and debug payloads independently. */
|
|
335
|
+
function buildCombo(records, sourceMapOf, revision) {
|
|
336
|
+
const resources = records.map((record) => ({
|
|
337
|
+
id: record.entry.id,
|
|
338
|
+
rev: record.entry.rev,
|
|
339
|
+
clientPath: record.meta.clientPath,
|
|
340
|
+
fileName: "client.js",
|
|
341
|
+
bundle: record.bundle
|
|
342
|
+
}));
|
|
343
|
+
const rev = revision ?? comboRevision(resources);
|
|
344
|
+
const entries = resources.map((resource) => resource.id);
|
|
304
345
|
const url = comboUrl(entries, rev);
|
|
305
346
|
const sourceMapUrl = comboUrl(entries, rev, true);
|
|
306
347
|
return {
|
|
307
348
|
url,
|
|
308
349
|
rev,
|
|
309
350
|
entries,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
351
|
+
sourceMapUrl,
|
|
352
|
+
scriptBody: lazyBody(() => buildComboScript(resources, sourceMapUrl)),
|
|
353
|
+
sourceMapBody: lazyBody(() => buildComboSourceMap(resources, sourceMapOf))
|
|
313
354
|
};
|
|
314
355
|
}
|
|
315
356
|
/** Add initial-load scheduling metadata to a combo artifact. */
|
|
316
|
-
function buildBatch(phase, records) {
|
|
317
|
-
const artifact = buildCombo(records);
|
|
357
|
+
function buildBatch(phase, records, sourceMapOf) {
|
|
358
|
+
const artifact = buildCombo(records, sourceMapOf);
|
|
318
359
|
return {
|
|
319
360
|
...artifact,
|
|
320
361
|
descriptor: {
|
|
@@ -455,6 +496,7 @@ var ClientModuleRegistry = class extends Service {
|
|
|
455
496
|
composed;
|
|
456
497
|
/**
|
|
457
498
|
* Build the service: subscribe, seed, and run the activation flush.
|
|
499
|
+
* Bundle routes follow the optional Web carrier's injected lifecycle.
|
|
458
500
|
* @param ctx - plugin context carrying Loader and an optional Web carrier.
|
|
459
501
|
*/
|
|
460
502
|
constructor(ctx) {
|
|
@@ -484,8 +526,7 @@ var ClientModuleRegistry = class extends Service {
|
|
|
484
526
|
handler: this.serveBundle
|
|
485
527
|
}), "client-modules: bundle route");
|
|
486
528
|
};
|
|
487
|
-
|
|
488
|
-
else registerWebCarrier(ctx);
|
|
529
|
+
ctx.inject(["webServer"], registerWebCarrier);
|
|
489
530
|
ctx.on("webserver/index-inject", (table) => {
|
|
490
531
|
table.push(...bootInjections(this.composed));
|
|
491
532
|
});
|
|
@@ -508,12 +549,13 @@ var ClientModuleRegistry = class extends Service {
|
|
|
508
549
|
/**
|
|
509
550
|
* Serve an advertised revisioned bundle or source map without a Web server.
|
|
510
551
|
* Unknown URLs return 404, unsupported methods return 405, and `HEAD`
|
|
511
|
-
* returns the same immutable headers without a body.
|
|
552
|
+
* returns the same immutable headers without materializing a body. Each body
|
|
553
|
+
* is built once on its first `GET`; script construction never reads maps.
|
|
512
554
|
* @param request - shell-carrier request for a `/plugins` resource.
|
|
513
555
|
* @returns the exact response also exposed by the optional Web route.
|
|
514
556
|
*/
|
|
515
|
-
fetchBundle(request) {
|
|
516
|
-
const resource = this.bundleResource(request.method, request.url);
|
|
557
|
+
async fetchBundle(request) {
|
|
558
|
+
const resource = await this.bundleResource(request.method, request.url);
|
|
517
559
|
const body = resource.body === void 0 ? null : Uint8Array.from(resource.body);
|
|
518
560
|
return new Response(body, {
|
|
519
561
|
status: resource.status,
|
|
@@ -533,8 +575,8 @@ var ClientModuleRegistry = class extends Service {
|
|
|
533
575
|
return baseline === void 0 ? void 0 : { ...baseline };
|
|
534
576
|
}
|
|
535
577
|
/**
|
|
536
|
-
*
|
|
537
|
-
* point through which
|
|
578
|
+
* Publish one completed bundle generation (the HMR watch's registration
|
|
579
|
+
* hook — the only entry point through which build changes reach the graph).
|
|
538
580
|
* @param id - entry id (package name).
|
|
539
581
|
* @returns the new rev, or undefined for an unknown id.
|
|
540
582
|
*/
|
|
@@ -543,14 +585,11 @@ var ClientModuleRegistry = class extends Service {
|
|
|
543
585
|
if (record === void 0) return void 0;
|
|
544
586
|
const baseline = this.captureArtifactBaseline(record.meta.clientPath);
|
|
545
587
|
const bundle = readFileSync(record.meta.clientPath);
|
|
546
|
-
const
|
|
547
|
-
const rev = artifactRevision(bundle, sourceMap);
|
|
588
|
+
const rev = artifactRevision(bundle, baseline);
|
|
548
589
|
record.baseline = baseline;
|
|
549
590
|
if (rev === record.entry.rev) return rev;
|
|
550
591
|
record.entry = graphRow(id, rev, record.meta);
|
|
551
592
|
record.bundle = bundle;
|
|
552
|
-
if (sourceMap === void 0) delete record.sourceMap;
|
|
553
|
-
else record.sourceMap = sourceMap;
|
|
554
593
|
this.composed = this.compose();
|
|
555
594
|
for (const notify of this.rebuildListeners) try {
|
|
556
595
|
notify(id, rev);
|
|
@@ -589,31 +628,32 @@ var ClientModuleRegistry = class extends Service {
|
|
|
589
628
|
const bootstrapIds = new Set(bootstrap.map((record) => record.entry.id));
|
|
590
629
|
const application = entries.filter((entry) => !bootstrapIds.has(entry.id)).map((entry) => this.table.get(entry.id)).filter((record) => record !== void 0);
|
|
591
630
|
const artifacts = [];
|
|
592
|
-
for (const records of partitionComboRecords(bootstrap)) artifacts.push(buildBatch("bootstrap", records));
|
|
593
|
-
for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records));
|
|
631
|
+
for (const records of partitionComboRecords(bootstrap)) artifacts.push(buildBatch("bootstrap", records, this.readSourceMap));
|
|
632
|
+
for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records, this.readSourceMap));
|
|
594
633
|
const batchResponses = /* @__PURE__ */ new Map();
|
|
595
634
|
for (const artifact of artifacts) {
|
|
596
|
-
batchResponses.set(artifact.descriptor.url, {
|
|
597
|
-
body: artifact.
|
|
635
|
+
batchResponses.set(artifact.descriptor.url, this.responses.get(artifact.descriptor.url) ?? {
|
|
636
|
+
body: artifact.scriptBody,
|
|
598
637
|
contentType: "text/javascript; charset=utf-8"
|
|
599
638
|
});
|
|
600
|
-
batchResponses.set(artifact.sourceMapUrl, {
|
|
601
|
-
body: artifact.
|
|
639
|
+
batchResponses.set(artifact.sourceMapUrl, this.responses.get(artifact.sourceMapUrl) ?? {
|
|
640
|
+
body: artifact.sourceMapBody,
|
|
602
641
|
contentType: "application/json; charset=utf-8"
|
|
603
642
|
});
|
|
604
643
|
}
|
|
605
644
|
const responses = new Map(batchResponses);
|
|
606
645
|
for (const record of this.table.values()) {
|
|
607
|
-
const artifact = buildCombo([record], record.entry.rev);
|
|
608
|
-
responses.set(artifact.url, {
|
|
609
|
-
body: artifact.
|
|
646
|
+
const artifact = buildCombo([record], this.readSourceMap, record.entry.rev);
|
|
647
|
+
responses.set(artifact.url, responses.get(artifact.url) ?? this.responses.get(artifact.url) ?? {
|
|
648
|
+
body: artifact.scriptBody,
|
|
610
649
|
contentType: "text/javascript; charset=utf-8"
|
|
611
650
|
});
|
|
612
|
-
responses.set(artifact.sourceMapUrl, {
|
|
613
|
-
body: artifact.
|
|
651
|
+
responses.set(artifact.sourceMapUrl, responses.get(artifact.sourceMapUrl) ?? this.responses.get(artifact.sourceMapUrl) ?? {
|
|
652
|
+
body: artifact.sourceMapBody,
|
|
614
653
|
contentType: "application/json; charset=utf-8"
|
|
615
654
|
});
|
|
616
655
|
}
|
|
656
|
+
for (const [resourceUrl, response] of this.responses) if (this.chunkRequest(new URL(resourceUrl, "http://x")) !== void 0) responses.set(resourceUrl, response);
|
|
617
657
|
this.previousBatchResponses = this.batchResponses;
|
|
618
658
|
this.batchResponses = batchResponses;
|
|
619
659
|
this.responses = responses;
|
|
@@ -741,7 +781,7 @@ var ClientModuleRegistry = class extends Service {
|
|
|
741
781
|
return `${this.initialRevisionNonce}-${String(this.nextInitialRevision++)}`;
|
|
742
782
|
}
|
|
743
783
|
/**
|
|
744
|
-
* Read the activation-time bundle
|
|
784
|
+
* Read the activation-time bundle snapshot.
|
|
745
785
|
* @param pkgName - package that declares the client bundle.
|
|
746
786
|
* @param clientPath - absolute path of the built client artifact.
|
|
747
787
|
* @returns the immutable bytes plus the pre-read filesystem baseline.
|
|
@@ -750,27 +790,24 @@ var ClientModuleRegistry = class extends Service {
|
|
|
750
790
|
initialBundleSnapshot(pkgName, clientPath) {
|
|
751
791
|
try {
|
|
752
792
|
const baseline = this.captureArtifactBaseline(clientPath);
|
|
753
|
-
const bundle = readFileSync(clientPath);
|
|
754
|
-
const sourceMap = this.readSourceMapSnapshot(clientPath);
|
|
755
793
|
return {
|
|
756
|
-
bundle,
|
|
757
|
-
baseline
|
|
758
|
-
...sourceMap === void 0 ? {} : { sourceMap }
|
|
794
|
+
bundle: readFileSync(clientPath),
|
|
795
|
+
baseline
|
|
759
796
|
};
|
|
760
797
|
} catch (error) {
|
|
761
798
|
if (error.code !== "ENOENT") throw error;
|
|
762
799
|
throw new MissingClientBundleError(pkgName, clientPath, error);
|
|
763
800
|
}
|
|
764
801
|
}
|
|
765
|
-
/** Treat a missing, torn, or malformed development map as an identity
|
|
766
|
-
|
|
802
|
+
/** Treat a missing, torn, or malformed development map as an identity section. */
|
|
803
|
+
readSourceMap = (clientPath) => {
|
|
767
804
|
try {
|
|
768
|
-
return
|
|
805
|
+
return readSourceMap(clientPath);
|
|
769
806
|
} catch (error) {
|
|
770
807
|
this.ctx.logger.warn(error);
|
|
771
808
|
return;
|
|
772
809
|
}
|
|
773
|
-
}
|
|
810
|
+
};
|
|
774
811
|
/** Reconcile one entry name against the live Loader sources. @returns whether the table changed. */
|
|
775
812
|
processOne(entryName, onError) {
|
|
776
813
|
const nextSources = /* @__PURE__ */ new Map();
|
|
@@ -828,8 +865,7 @@ var ClientModuleRegistry = class extends Service {
|
|
|
828
865
|
sourceKey: source.sourceKey,
|
|
829
866
|
meta: source.meta,
|
|
830
867
|
bundle: snapshot.bundle,
|
|
831
|
-
baseline: snapshot.baseline
|
|
832
|
-
...snapshot.sourceMap === void 0 ? {} : { sourceMap: snapshot.sourceMap }
|
|
868
|
+
baseline: snapshot.baseline
|
|
833
869
|
});
|
|
834
870
|
return true;
|
|
835
871
|
}
|
|
@@ -854,24 +890,68 @@ var ClientModuleRegistry = class extends Service {
|
|
|
854
890
|
this.composed = composed;
|
|
855
891
|
this.notifyGraphChanged();
|
|
856
892
|
}
|
|
857
|
-
|
|
893
|
+
/** Match an exact current-revision package-local chunk URL without reading its file. */
|
|
894
|
+
chunkRequest(requestUrl) {
|
|
895
|
+
const resourceUrl = `${requestUrl.pathname}${requestUrl.search}`;
|
|
896
|
+
for (const record of this.table.values()) {
|
|
897
|
+
const prefix = `/plugins/${record.entry.id}/`;
|
|
898
|
+
if (!requestUrl.pathname.startsWith(prefix)) continue;
|
|
899
|
+
const requested = requestUrl.pathname.slice(prefix.length);
|
|
900
|
+
const sourceMap = requested.endsWith(".map");
|
|
901
|
+
const fileName = sourceMap ? requested.slice(0, -4) : requested;
|
|
902
|
+
if (!CLIENT_CHUNK.test(fileName)) return void 0;
|
|
903
|
+
if (resourceUrl !== chunkUrl(record.entry.id, fileName, record.entry.rev, sourceMap)) return void 0;
|
|
904
|
+
return {
|
|
905
|
+
record,
|
|
906
|
+
fileName,
|
|
907
|
+
sourceMap,
|
|
908
|
+
resourceUrl
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
/** Build a package-local chunk response only when its URL is requested. */
|
|
913
|
+
chunkResponse(requestUrl) {
|
|
914
|
+
const request = this.chunkRequest(requestUrl);
|
|
915
|
+
if (request === void 0) return void 0;
|
|
916
|
+
const { record, fileName, sourceMap, resourceUrl } = request;
|
|
917
|
+
const clientPath = join(dirname(record.meta.clientPath), fileName);
|
|
918
|
+
if (!existsSync(clientPath)) return void 0;
|
|
919
|
+
const sourceMapUrl = chunkUrl(record.entry.id, fileName, record.entry.rev, true);
|
|
920
|
+
const resource = () => ({
|
|
921
|
+
id: record.entry.id,
|
|
922
|
+
rev: record.entry.rev,
|
|
923
|
+
clientPath,
|
|
924
|
+
fileName,
|
|
925
|
+
bundle: readFileSync(clientPath)
|
|
926
|
+
});
|
|
927
|
+
const response = sourceMap ? {
|
|
928
|
+
body: lazyBody(() => buildComboSourceMap([resource()], this.readSourceMap, fileName)),
|
|
929
|
+
contentType: "application/json; charset=utf-8"
|
|
930
|
+
} : {
|
|
931
|
+
body: lazyBody(() => buildComboScript([resource()], sourceMapUrl)),
|
|
932
|
+
contentType: "text/javascript; charset=utf-8"
|
|
933
|
+
};
|
|
934
|
+
this.responses.set(resourceUrl, response);
|
|
935
|
+
return response;
|
|
936
|
+
}
|
|
937
|
+
async bundleResource(method, url) {
|
|
858
938
|
if (method !== "GET" && method !== "HEAD") return { status: 405 };
|
|
859
939
|
const requestUrl = new URL(url, "http://x");
|
|
860
940
|
const resourceUrl = `${requestUrl.pathname}${requestUrl.search}`;
|
|
861
|
-
const response = this.responses.get(resourceUrl) ?? this.previousBatchResponses.get(resourceUrl);
|
|
941
|
+
const response = this.responses.get(resourceUrl) ?? this.previousBatchResponses.get(resourceUrl) ?? this.chunkResponse(requestUrl);
|
|
862
942
|
if (response !== void 0) return {
|
|
863
943
|
status: 200,
|
|
864
944
|
headers: {
|
|
865
945
|
"content-type": response.contentType,
|
|
866
946
|
"cache-control": IMMUTABLE_CACHE
|
|
867
947
|
},
|
|
868
|
-
...method === "HEAD" ? {} : { body: response.body }
|
|
948
|
+
...method === "HEAD" ? {} : { body: await response.body() }
|
|
869
949
|
};
|
|
870
950
|
return { status: 404 };
|
|
871
951
|
}
|
|
872
|
-
serveBundle = (req, res) => {
|
|
952
|
+
serveBundle = async (req, res) => {
|
|
873
953
|
/* v8 ignore next -- `?? '/'` arm: node:http always sets url on server requests. */
|
|
874
|
-
const response = this.bundleResource(req.method, req.url ?? "/");
|
|
954
|
+
const response = await this.bundleResource(req.method, req.url ?? "/");
|
|
875
955
|
res.writeHead(response.status, response.headers);
|
|
876
956
|
res.end(response.body);
|
|
877
957
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Loader } from '@deepseek-ai/cordis-plugin-loader';
|
|
2
|
+
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
|
|
3
|
+
import type { BootManifest, ClientModuleLoader } from './manifest.ts';
|
|
4
|
+
/** Page-local failures do not change the Host's bundle enablement. */
|
|
5
|
+
export interface ClientEntryState {
|
|
6
|
+
/** True while a snapshot, retry or code replacement is being applied. */
|
|
7
|
+
readonly syncing: boolean;
|
|
8
|
+
/** Package ids and errors from the latest reconciliation. */
|
|
9
|
+
readonly failures: readonly {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
}[];
|
|
13
|
+
}
|
|
14
|
+
/** Module-table capabilities used within serialized entry operations. */
|
|
15
|
+
interface ModuleIndex {
|
|
16
|
+
update(manifest: BootManifest, managed: Iterable<string>): void;
|
|
17
|
+
invalidateForReplacement(id: string, rev: string): void;
|
|
18
|
+
prune(roots: Iterable<string>): void;
|
|
19
|
+
}
|
|
20
|
+
/** Manages only entries created from the Host manifest; other Loader contributors retain ownership. */
|
|
21
|
+
export declare class ClientEntries {
|
|
22
|
+
private readonly modules;
|
|
23
|
+
private readonly index;
|
|
24
|
+
/** Stable observable consumed by page diagnostics through the renderer's injected hook. */
|
|
25
|
+
readonly state: ObservableSnapshot<ClientEntryState>;
|
|
26
|
+
private snapshot;
|
|
27
|
+
private readonly listeners;
|
|
28
|
+
private readonly managed;
|
|
29
|
+
private readonly revisions;
|
|
30
|
+
private loader;
|
|
31
|
+
private queue;
|
|
32
|
+
private desired;
|
|
33
|
+
private generation;
|
|
34
|
+
private stopped;
|
|
35
|
+
/**
|
|
36
|
+
* Construct the page controller before Cordis boot.
|
|
37
|
+
* @param modules - Module arrival and materialization owner.
|
|
38
|
+
* @param index - Private descriptor replacement and unused-module cleanup.
|
|
39
|
+
*/
|
|
40
|
+
constructor(modules: ClientModuleLoader, index: ModuleIndex);
|
|
41
|
+
/**
|
|
42
|
+
* Create the initial roster and retain its entry identities for subsequent reconciliation.
|
|
43
|
+
* @param loader - Page Loader, already configured with the module system.
|
|
44
|
+
* @param manifest - Initial roster audited by the boot caller.
|
|
45
|
+
* @returns after initial entries and their activation settle; boot owns its activation audit.
|
|
46
|
+
*/
|
|
47
|
+
start(loader: Loader, manifest: BootManifest): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Validate and apply the latest full Host graph. Changed targets cancel obsolete mounts; identical targets share pending loads.
|
|
50
|
+
* @param graph - JSON-decoded graph received from the Host.
|
|
51
|
+
* @returns after the queued reconciliation; per-package failures remain available in {@link state}.
|
|
52
|
+
*/
|
|
53
|
+
sync(graph: unknown): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Retry failed entries against the latest graph, including an unchanged revision.
|
|
56
|
+
* @returns after retry settlement, with remaining errors in {@link state}.
|
|
57
|
+
*/
|
|
58
|
+
retry(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Replace one entry's code in the same queue as graph updates; duplicate revisions are ignored.
|
|
61
|
+
* Entries missing after a failed import are reconciled; bootstrap replacement fails before teardown.
|
|
62
|
+
* @param id - Package id from a rebuilt frame.
|
|
63
|
+
* @param rev - Opaque revision selecting the rebuilt artifact.
|
|
64
|
+
* @returns after queued work; replacement errors reject, while per-package reconciliation errors remain in {@link state}.
|
|
65
|
+
*/
|
|
66
|
+
reload(id: string, rev: string): Promise<void>;
|
|
67
|
+
private publish;
|
|
68
|
+
private enqueue;
|
|
69
|
+
private current;
|
|
70
|
+
/** Keep ownership even when Loader rejects a module's plugin exports after inserting its entry. */
|
|
71
|
+
private create;
|
|
72
|
+
private replace;
|
|
73
|
+
private reconcile;
|
|
74
|
+
}
|
|
75
|
+
export {};
|
|
76
|
+
//# sourceMappingURL=entries.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Loader lifecycle operations shared by live graph reconciliation and code replacement. */
|
|
2
|
+
import type { Entry } from '@deepseek-ai/cordis-plugin-loader';
|
|
3
|
+
/**
|
|
4
|
+
* Release a runtime before clearing its entry fiber so Loader refresh can import new code.
|
|
5
|
+
* Registry deletion prevents Loader from treating replacement as a user disable.
|
|
6
|
+
* @param entry - Entry retained for code replacement.
|
|
7
|
+
*/
|
|
8
|
+
export declare function tearDownEntryFiber(entry: Entry): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Remove styles after their plugin's effect cleanup has settled.
|
|
11
|
+
* @param id - Package whose factory owns the style tags.
|
|
12
|
+
*/
|
|
13
|
+
export declare function removeOwnedStyles(id: string): void;
|
|
14
|
+
//# sourceMappingURL=entry-lifecycle.d.ts.map
|
|
@@ -6,24 +6,28 @@
|
|
|
6
6
|
* parser-preloads this ordinary client bundle into the pending registration
|
|
7
7
|
* queue. The HTML-installed loader facade materializes this bundle and calls
|
|
8
8
|
* its bootstrap export, which constructs the system and retains the same
|
|
9
|
-
* exports for this package's graph row. The plugin face
|
|
10
|
-
*
|
|
9
|
+
* exports for this package's graph row. The plugin face enrolls the module
|
|
10
|
+
* system attached to its own Loader as `ctx.modules`.
|
|
11
11
|
* @module @deepseek-ai/dsh-client-modules/client
|
|
12
12
|
*/
|
|
13
13
|
import type { Context } from '@deepseek-ai/cordis';
|
|
14
14
|
import { ClientModuleSystem } from './system.ts';
|
|
15
15
|
import type { ClientBootstrapModule, ClientModuleCreateOptions, ClientModuleLoaderTarget } from './manifest.ts';
|
|
16
16
|
export { ClientModuleSystem };
|
|
17
|
-
export {
|
|
17
|
+
export { tearDownEntryFiber } from './entry-lifecycle.ts';
|
|
18
|
+
export type { ClientEntries, ClientEntryState } from './entries.ts';
|
|
19
|
+
export { exactPackageSpecifier, parseBootManifest, parseDshClient, stripClientSuffix } from './manifest.ts';
|
|
18
20
|
export type { BootManifest, BootModuleRow, BootPluginRow, ClientBootstrapModule, ClientBundleRegistration, ClientModuleCreateOptions, ClientModuleLoader, ClientModuleLoaderTarget, ClientModuleRecord, ClientModuleSystemOptions, DshWindow, WebBootEntry, WebBootGraph, } from './manifest.ts';
|
|
19
21
|
/**
|
|
20
22
|
* Build the live module system from the HTML facade's materialized modules bundle.
|
|
21
23
|
* @param target - Stable registration facade whose pending queue becomes the live sink.
|
|
22
24
|
* @param bootstrapModule - This bundle's id and already-materialized exports.
|
|
23
25
|
* @param options - Raw boot graph, platform seed, and optional bundle transport.
|
|
24
|
-
* @returns The created module system
|
|
26
|
+
* @returns The created module system.
|
|
25
27
|
*/
|
|
26
28
|
export declare function createClientModuleSystem(target: ClientModuleLoaderTarget, bootstrapModule: ClientBootstrapModule, options: ClientModuleCreateOptions): ClientModuleSystem;
|
|
29
|
+
/** Required service: the Loader whose internal module system this plugin publishes. */
|
|
30
|
+
export declare const inject: string[];
|
|
27
31
|
/**
|
|
28
32
|
* Enroll the kernel-built module system as `ctx.modules`.
|
|
29
33
|
* @param ctx - client root context.
|