@deepseek-ai/dsh-client-modules 0.1.6-alpha.1 → 0.1.7-alpha.1
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 +17 -5
- package/README.zh.md +17 -5
- package/lib/client.js +407 -26
- package/lib/index.js +185 -97
- package/lib/invariant.js +2 -2
- 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 +2 -0
- package/lib/types/client/manifest.d.ts +31 -16
- package/lib/types/client/system.d.ts +13 -8
- package/lib/types/index.d.ts +17 -13
- package/package.json +8 -7
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { createHash
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
3
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { dirname, isAbsolute, join } from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -165,6 +165,8 @@ const COMBO_REVISION_PLACEHOLDER = "0".repeat(HASH_REVISION_LENGTH);
|
|
|
165
165
|
const SOURCE_MAP_TRAILER = /(?:\r?\n)?\/\/# sourceMappingURL=[^\r\n]*(?:\r?\n)?$/;
|
|
166
166
|
/** Debugger source name appended to page bundles in the WebWorker image. */
|
|
167
167
|
const SOURCE_URL_TRAILER = /(?:\r?\n)?\/\/# sourceURL=([^\r\n]+)(?:\r?\n)?$/;
|
|
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$/;
|
|
168
170
|
/** Resolve `exports["./client"]` to a relative path, accepting the string and one-level conditional forms. */
|
|
169
171
|
function clientExportOf(pkgName, exportsField) {
|
|
170
172
|
if (typeof exportsField !== "object" || exportsField === null) return void 0;
|
|
@@ -177,27 +179,58 @@ function clientExportOf(pkgName, exportsField) {
|
|
|
177
179
|
}
|
|
178
180
|
throw new Error(`client-modules: ${pkgName} exports["./client"] must be a string or an object with a string default`);
|
|
179
181
|
}
|
|
180
|
-
/** sha1
|
|
182
|
+
/** sha1 metadata hash shortened to 12 hex chars. */
|
|
181
183
|
function shortHash(input) {
|
|
182
184
|
return createHash("sha1").update(input).digest("hex").slice(0, HASH_REVISION_LENGTH);
|
|
183
185
|
}
|
|
184
186
|
/** Hash several response fields without allowing bytes to move across field boundaries. */
|
|
185
187
|
function framedHash(domain, parts) {
|
|
186
188
|
const hash = createHash("sha1").update(domain).update("\0");
|
|
187
|
-
for (const part of parts) hash.update(`${String(
|
|
189
|
+
for (const part of parts) hash.update(`${String(Buffer.byteLength(part))}:`).update(part);
|
|
188
190
|
return hash.digest("hex").slice(0, HASH_REVISION_LENGTH);
|
|
189
191
|
}
|
|
190
|
-
/**
|
|
191
|
-
function artifactRevision(
|
|
192
|
-
return framedHash("plugin-artifact",
|
|
192
|
+
/** Identify an entry's build from filesystem metadata without hashing its contents. */
|
|
193
|
+
function artifactRevision(baseline) {
|
|
194
|
+
return framedHash("plugin-artifact", [
|
|
195
|
+
String(baseline.mtimeMs),
|
|
196
|
+
String(baseline.ctimeMs),
|
|
197
|
+
String(baseline.size)
|
|
198
|
+
]);
|
|
193
199
|
}
|
|
194
|
-
/**
|
|
200
|
+
/** Absolute route prefix serving every plugin resource. */
|
|
201
|
+
const PLUGIN_ROUTE = "/plugins";
|
|
202
|
+
/** Combo query addressing one ordered plugin-file list. */
|
|
203
|
+
function comboSearch(ids, rev, sourceMap = false) {
|
|
204
|
+
return `??${ids.map((id) => `${id}/client.js${sourceMap ? ".map" : ""}`).join(",")}&rev=${rev}`;
|
|
205
|
+
}
|
|
206
|
+
/** Absolute route URL for one combo resource. */
|
|
195
207
|
function comboUrl(ids, rev, sourceMap = false) {
|
|
196
|
-
return
|
|
208
|
+
return `${PLUGIN_ROUTE}/${comboSearch(ids, rev, sourceMap)}`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Browser reference to one combo resource: app-owned browser routes are
|
|
212
|
+
* document-relative, so the route key's leading slash is stripped here, at the
|
|
213
|
+
* boundary between the two halves. The rule and its reasons are owned by
|
|
214
|
+
* .agents/notes/implemented/architecture/2026-09-14-web-document-relative-app-routes.md.
|
|
215
|
+
*/
|
|
216
|
+
function comboReference(ids, rev, sourceMap = false) {
|
|
217
|
+
return comboUrl(ids, rev, sourceMap).slice(1);
|
|
218
|
+
}
|
|
219
|
+
/** Absolute route URL for one package-local chunk. */
|
|
220
|
+
function chunkUrl(id, fileName, rev, sourceMap = false) {
|
|
221
|
+
return `${PLUGIN_ROUTE}/${id}/${fileName}${sourceMap ? ".map" : ""}?rev=${rev}`;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Source-map reference stamped into one chunk script. A script's map reference
|
|
225
|
+
* resolves against that script's own directory rather than the document, so
|
|
226
|
+
* this is the bare map file name, not the document-relative route.
|
|
227
|
+
*/
|
|
228
|
+
function chunkMapReference(fileName, rev) {
|
|
229
|
+
return `${fileName}.map?rev=${rev}`;
|
|
197
230
|
}
|
|
198
|
-
/** Measure the
|
|
231
|
+
/** Measure the longest browser-facing combo URL used to partition a startup resource list. */
|
|
199
232
|
function projectedComboUrlBytes(records) {
|
|
200
|
-
return Buffer.byteLength(
|
|
233
|
+
return Buffer.byteLength(comboReference(records.map((record) => record.entry.id), COMBO_REVISION_PLACEHOLDER, true));
|
|
201
234
|
}
|
|
202
235
|
/** Partition one phase in graph order without allowing a generated URL above the protocol limit. */
|
|
203
236
|
function partitionComboRecords(records) {
|
|
@@ -218,23 +251,23 @@ function partitionComboRecords(records) {
|
|
|
218
251
|
return chunks;
|
|
219
252
|
}
|
|
220
253
|
/** Remove bundle-local debug directives and retain their stable generated-file name. */
|
|
221
|
-
function
|
|
222
|
-
let source =
|
|
254
|
+
function prepareSource(resource) {
|
|
255
|
+
let source = resource.bundle.toString("utf8");
|
|
223
256
|
const sourceUrl = SOURCE_URL_TRAILER.exec(source)?.[1];
|
|
224
257
|
source = source.replace(SOURCE_URL_TRAILER, "").replace(SOURCE_MAP_TRAILER, "");
|
|
225
258
|
if (!source.endsWith("\n")) source += "\n";
|
|
226
|
-
const fallbackSource = sourceUrl === void 0 ? `/plugins/${
|
|
259
|
+
const fallbackSource = sourceUrl === void 0 ? `/plugins/${resource.id}/${resource.fileName}` : /^(?:[A-Za-z][A-Za-z\d+.-]*:|\/)/.test(sourceUrl) ? sourceUrl : `/${sourceUrl}`;
|
|
227
260
|
return {
|
|
228
261
|
source,
|
|
229
262
|
fallbackSource
|
|
230
263
|
};
|
|
231
264
|
}
|
|
232
|
-
/** Stamp a combo script's
|
|
265
|
+
/** Stamp a combo script's source-map reference onto its executable bytes. */
|
|
233
266
|
function comboScript(input, sourceMapUrl) {
|
|
234
267
|
return Buffer.from(sourceMapUrl === void 0 ? input : `${input}//# sourceMappingURL=${sourceMapUrl}\n`);
|
|
235
268
|
}
|
|
236
|
-
/**
|
|
237
|
-
function
|
|
269
|
+
/** Read and parse an optional source map when its combo-map endpoint is requested. */
|
|
270
|
+
function readSourceMap(clientPath) {
|
|
238
271
|
let body;
|
|
239
272
|
try {
|
|
240
273
|
body = readFileSync(`${clientPath}.map`);
|
|
@@ -245,10 +278,7 @@ function sourceMapSnapshot(clientPath) {
|
|
|
245
278
|
const value = JSON.parse(body.toString("utf8"));
|
|
246
279
|
const parsed = typeof value === "object" && value !== null ? value : void 0;
|
|
247
280
|
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`);
|
|
248
|
-
return
|
|
249
|
-
body,
|
|
250
|
-
parsed
|
|
251
|
-
};
|
|
281
|
+
return parsed;
|
|
252
282
|
}
|
|
253
283
|
/** Count generated lines while assembling indexed-map section offsets. */
|
|
254
284
|
function newlineCount(value) {
|
|
@@ -257,13 +287,10 @@ function newlineCount(value) {
|
|
|
257
287
|
return count;
|
|
258
288
|
}
|
|
259
289
|
/** Resolve section sources against their original per-plugin map URL before combo relocation. */
|
|
260
|
-
function comboSectionMap(
|
|
261
|
-
const original = record.sourceMap?.parsed;
|
|
262
|
-
/* v8 ignore next -- callers add sections only for records with a source map. */
|
|
263
|
-
if (original === void 0) throw new Error(`client-modules: source map missing for ${record.entry.id}`);
|
|
290
|
+
function comboSectionMap(resource, original) {
|
|
264
291
|
const sourcePaths = original.sources;
|
|
265
292
|
const sourceRoot = typeof original.sourceRoot === "string" ? original.sourceRoot : "";
|
|
266
|
-
const base = new URL(`/plugins/${
|
|
293
|
+
const base = new URL(`/plugins/${resource.id}/client.js.map`, "http://dsh.invalid");
|
|
267
294
|
const relocated = sourcePaths.map((source) => {
|
|
268
295
|
const separator = sourceRoot !== "" && !sourceRoot.endsWith("/") && !source.startsWith("/") ? "/" : "";
|
|
269
296
|
const resolved = new URL(`${sourceRoot}${separator}${source}`, base);
|
|
@@ -287,14 +314,35 @@ function identitySectionMap(source, sourceUrl) {
|
|
|
287
314
|
mappings
|
|
288
315
|
};
|
|
289
316
|
}
|
|
290
|
-
/**
|
|
291
|
-
function
|
|
317
|
+
/** Run one producer in the first requester's microtask, not off-thread, and share its settlement. */
|
|
318
|
+
function lazyBody(produce) {
|
|
319
|
+
let result;
|
|
320
|
+
return () => {
|
|
321
|
+
result ??= Promise.resolve().then(produce);
|
|
322
|
+
return result;
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/** Derive one combo revision from the ordered immutable row revisions. */
|
|
326
|
+
function comboRevision(resources) {
|
|
327
|
+
return framedHash("combo", resources.flatMap((resource) => [resource.id, resource.rev]));
|
|
328
|
+
}
|
|
329
|
+
/** Concatenate one or more factory registrations without reading or composing source maps. */
|
|
330
|
+
function buildComboScript(resources, sourceMapUrl) {
|
|
292
331
|
let source = "";
|
|
332
|
+
for (const resource of resources) source += `${prepareSource(resource).source};\n`;
|
|
333
|
+
return comboScript(source, sourceMapUrl);
|
|
334
|
+
}
|
|
335
|
+
/** Compose one indexed map from source-map files read only for this request. */
|
|
336
|
+
function buildComboSourceMap(resources, sourceMapOf, fileName = "client.js") {
|
|
293
337
|
const sections = [];
|
|
294
338
|
let line = 0;
|
|
295
|
-
for (const
|
|
296
|
-
const prepared =
|
|
297
|
-
const
|
|
339
|
+
for (const resource of resources) {
|
|
340
|
+
const prepared = prepareSource(resource);
|
|
341
|
+
const sourceMap = sourceMapOf(resource.clientPath);
|
|
342
|
+
let section = identitySectionMap(prepared.source, prepared.fallbackSource);
|
|
343
|
+
if (sourceMap !== void 0) try {
|
|
344
|
+
section = comboSectionMap(resource, sourceMap);
|
|
345
|
+
} catch {}
|
|
298
346
|
sections.push({
|
|
299
347
|
offset: {
|
|
300
348
|
line,
|
|
@@ -302,47 +350,52 @@ function buildCombo(records, revision) {
|
|
|
302
350
|
},
|
|
303
351
|
map: section
|
|
304
352
|
});
|
|
305
|
-
|
|
306
|
-
source += bundle;
|
|
307
|
-
line += newlineCount(bundle);
|
|
353
|
+
line += newlineCount(`${prepared.source};\n`);
|
|
308
354
|
}
|
|
309
|
-
|
|
355
|
+
return Buffer.from(`${JSON.stringify({
|
|
310
356
|
version: 3,
|
|
311
|
-
file:
|
|
357
|
+
file: fileName,
|
|
312
358
|
sections
|
|
313
359
|
})}\n`);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
360
|
+
}
|
|
361
|
+
/** Describe one combo and defer its executable and debug payloads independently. */
|
|
362
|
+
function buildCombo(records, sourceMapOf, revision) {
|
|
363
|
+
const resources = records.map((record) => ({
|
|
364
|
+
id: record.entry.id,
|
|
365
|
+
rev: record.entry.rev,
|
|
366
|
+
clientPath: record.meta.clientPath,
|
|
367
|
+
fileName: "client.js",
|
|
368
|
+
bundle: record.bundle
|
|
369
|
+
}));
|
|
370
|
+
const rev = revision ?? comboRevision(resources);
|
|
371
|
+
const entries = resources.map((resource) => resource.id);
|
|
319
372
|
return {
|
|
320
|
-
url,
|
|
373
|
+
url: comboUrl(entries, rev),
|
|
321
374
|
rev,
|
|
322
375
|
entries,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
376
|
+
sourceMapUrl: comboUrl(entries, rev, true),
|
|
377
|
+
scriptBody: lazyBody(() => buildComboScript(resources, comboSearch(entries, rev, true))),
|
|
378
|
+
sourceMapBody: lazyBody(() => buildComboSourceMap(resources, sourceMapOf))
|
|
326
379
|
};
|
|
327
380
|
}
|
|
328
381
|
/** Add initial-load scheduling metadata to a combo artifact. */
|
|
329
|
-
function buildBatch(phase, records) {
|
|
330
|
-
const artifact = buildCombo(records);
|
|
382
|
+
function buildBatch(phase, records, sourceMapOf) {
|
|
383
|
+
const artifact = buildCombo(records, sourceMapOf);
|
|
331
384
|
return {
|
|
332
385
|
...artifact,
|
|
333
386
|
descriptor: {
|
|
334
387
|
phase,
|
|
335
|
-
url: artifact.url,
|
|
388
|
+
url: artifact.url.slice(1),
|
|
336
389
|
rev: artifact.rev,
|
|
337
390
|
entries: artifact.entries
|
|
338
391
|
}
|
|
339
392
|
};
|
|
340
393
|
}
|
|
341
|
-
/** Graph row for one bundle rev (
|
|
394
|
+
/** Graph row for one bundle rev (the reference carries the rev as its cache-busting query). */
|
|
342
395
|
function graphRow(id, rev, fields) {
|
|
343
396
|
return {
|
|
344
397
|
id,
|
|
345
|
-
url:
|
|
398
|
+
url: comboReference([id], rev),
|
|
346
399
|
rev,
|
|
347
400
|
...fields.inject !== void 0 ? { inject: fields.inject } : {},
|
|
348
401
|
...fields.immediately ? { immediately: true } : {},
|
|
@@ -458,8 +511,6 @@ var ClientModuleRegistry = class extends Service {
|
|
|
458
511
|
rebuildListeners = /* @__PURE__ */ new Set();
|
|
459
512
|
graphListeners = /* @__PURE__ */ new Set();
|
|
460
513
|
dirty = /* @__PURE__ */ new Set();
|
|
461
|
-
initialRevisionNonce = randomBytes(8).toString("hex");
|
|
462
|
-
nextInitialRevision = 0;
|
|
463
514
|
responses = /* @__PURE__ */ new Map();
|
|
464
515
|
batchResponses = /* @__PURE__ */ new Map();
|
|
465
516
|
/** One prior graph generation covers a request racing the HMR recomposition that replaced its URL. */
|
|
@@ -494,7 +545,7 @@ var ClientModuleRegistry = class extends Service {
|
|
|
494
545
|
const registerWebCarrier = (webCtx) => {
|
|
495
546
|
webCtx.effect(() => webCtx.webServer.register({
|
|
496
547
|
kind: "prefix",
|
|
497
|
-
path:
|
|
548
|
+
path: PLUGIN_ROUTE,
|
|
498
549
|
handler: this.serveBundle
|
|
499
550
|
}), "client-modules: bundle route");
|
|
500
551
|
};
|
|
@@ -521,12 +572,13 @@ var ClientModuleRegistry = class extends Service {
|
|
|
521
572
|
/**
|
|
522
573
|
* Serve an advertised revisioned bundle or source map without a Web server.
|
|
523
574
|
* Unknown URLs return 404, unsupported methods return 405, and `HEAD`
|
|
524
|
-
* returns the same immutable headers without a body.
|
|
575
|
+
* returns the same immutable headers without materializing a body. Each body
|
|
576
|
+
* is built once on its first `GET`; script construction never reads maps.
|
|
525
577
|
* @param request - shell-carrier request for a `/plugins` resource.
|
|
526
578
|
* @returns the exact response also exposed by the optional Web route.
|
|
527
579
|
*/
|
|
528
|
-
fetchBundle(request) {
|
|
529
|
-
const resource = this.bundleResource(request.method, request.url);
|
|
580
|
+
async fetchBundle(request) {
|
|
581
|
+
const resource = await this.bundleResource(request.method, request.url);
|
|
530
582
|
const body = resource.body === void 0 ? null : Uint8Array.from(resource.body);
|
|
531
583
|
return new Response(body, {
|
|
532
584
|
status: resource.status,
|
|
@@ -546,24 +598,22 @@ var ClientModuleRegistry = class extends Service {
|
|
|
546
598
|
return baseline === void 0 ? void 0 : { ...baseline };
|
|
547
599
|
}
|
|
548
600
|
/**
|
|
549
|
-
*
|
|
550
|
-
* point through which
|
|
601
|
+
* Publish one completed bundle generation (the HMR watch's registration
|
|
602
|
+
* hook — the only entry point through which build changes reach the graph).
|
|
603
|
+
* Unchanged mtime, ctime and size preserve the graph without reading the bundle.
|
|
551
604
|
* @param id - entry id (package name).
|
|
552
|
-
* @returns the
|
|
605
|
+
* @returns the current artifact rev, or undefined for an unknown id.
|
|
553
606
|
*/
|
|
554
607
|
rebuilt(id) {
|
|
555
608
|
const record = this.table.get(id);
|
|
556
609
|
if (record === void 0) return void 0;
|
|
557
610
|
const baseline = this.captureArtifactBaseline(record.meta.clientPath);
|
|
611
|
+
const rev = artifactRevision(baseline);
|
|
612
|
+
if (rev === record.entry.rev) return rev;
|
|
558
613
|
const bundle = readFileSync(record.meta.clientPath);
|
|
559
|
-
const sourceMap = this.readSourceMapSnapshot(record.meta.clientPath);
|
|
560
|
-
const rev = artifactRevision(bundle, sourceMap);
|
|
561
614
|
record.baseline = baseline;
|
|
562
|
-
if (rev === record.entry.rev) return rev;
|
|
563
615
|
record.entry = graphRow(id, rev, record.meta);
|
|
564
616
|
record.bundle = bundle;
|
|
565
|
-
if (sourceMap === void 0) delete record.sourceMap;
|
|
566
|
-
else record.sourceMap = sourceMap;
|
|
567
617
|
this.composed = this.compose();
|
|
568
618
|
for (const notify of this.rebuildListeners) try {
|
|
569
619
|
notify(id, rev);
|
|
@@ -574,7 +624,7 @@ var ClientModuleRegistry = class extends Service {
|
|
|
574
624
|
return rev;
|
|
575
625
|
}
|
|
576
626
|
/**
|
|
577
|
-
* Subscribe to bundle rebuilds; fires only when
|
|
627
|
+
* Subscribe to bundle rebuilds; fires only when artifact metadata changes the rev.
|
|
578
628
|
* @param listener - receives the entry id and its new bundle rev.
|
|
579
629
|
* @returns the unsubscriber.
|
|
580
630
|
*/
|
|
@@ -602,31 +652,32 @@ var ClientModuleRegistry = class extends Service {
|
|
|
602
652
|
const bootstrapIds = new Set(bootstrap.map((record) => record.entry.id));
|
|
603
653
|
const application = entries.filter((entry) => !bootstrapIds.has(entry.id)).map((entry) => this.table.get(entry.id)).filter((record) => record !== void 0);
|
|
604
654
|
const artifacts = [];
|
|
605
|
-
for (const records of partitionComboRecords(bootstrap)) artifacts.push(buildBatch("bootstrap", records));
|
|
606
|
-
for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records));
|
|
655
|
+
for (const records of partitionComboRecords(bootstrap)) artifacts.push(buildBatch("bootstrap", records, this.readSourceMap));
|
|
656
|
+
for (const records of partitionComboRecords(application)) artifacts.push(buildBatch("application", records, this.readSourceMap));
|
|
607
657
|
const batchResponses = /* @__PURE__ */ new Map();
|
|
608
658
|
for (const artifact of artifacts) {
|
|
609
|
-
batchResponses.set(artifact.
|
|
610
|
-
body: artifact.
|
|
659
|
+
batchResponses.set(artifact.url, this.responses.get(artifact.url) ?? {
|
|
660
|
+
body: artifact.scriptBody,
|
|
611
661
|
contentType: "text/javascript; charset=utf-8"
|
|
612
662
|
});
|
|
613
|
-
batchResponses.set(artifact.sourceMapUrl, {
|
|
614
|
-
body: artifact.
|
|
663
|
+
batchResponses.set(artifact.sourceMapUrl, this.responses.get(artifact.sourceMapUrl) ?? {
|
|
664
|
+
body: artifact.sourceMapBody,
|
|
615
665
|
contentType: "application/json; charset=utf-8"
|
|
616
666
|
});
|
|
617
667
|
}
|
|
618
668
|
const responses = new Map(batchResponses);
|
|
619
669
|
for (const record of this.table.values()) {
|
|
620
|
-
const artifact = buildCombo([record], record.entry.rev);
|
|
621
|
-
responses.set(artifact.url, {
|
|
622
|
-
body: artifact.
|
|
670
|
+
const artifact = buildCombo([record], this.readSourceMap, record.entry.rev);
|
|
671
|
+
responses.set(artifact.url, responses.get(artifact.url) ?? this.responses.get(artifact.url) ?? {
|
|
672
|
+
body: artifact.scriptBody,
|
|
623
673
|
contentType: "text/javascript; charset=utf-8"
|
|
624
674
|
});
|
|
625
|
-
responses.set(artifact.sourceMapUrl, {
|
|
626
|
-
body: artifact.
|
|
675
|
+
responses.set(artifact.sourceMapUrl, responses.get(artifact.sourceMapUrl) ?? this.responses.get(artifact.sourceMapUrl) ?? {
|
|
676
|
+
body: artifact.sourceMapBody,
|
|
627
677
|
contentType: "application/json; charset=utf-8"
|
|
628
678
|
});
|
|
629
679
|
}
|
|
680
|
+
for (const [resourceUrl, response] of this.responses) if (this.chunkRequest(new URL(resourceUrl, "http://x")) !== void 0) responses.set(resourceUrl, response);
|
|
630
681
|
this.previousBatchResponses = this.batchResponses;
|
|
631
682
|
this.batchResponses = batchResponses;
|
|
632
683
|
this.responses = responses;
|
|
@@ -746,15 +797,12 @@ var ClientModuleRegistry = class extends Service {
|
|
|
746
797
|
return {
|
|
747
798
|
path: clientPath,
|
|
748
799
|
mtimeMs: bundle.mtimeMs,
|
|
800
|
+
ctimeMs: bundle.ctimeMs,
|
|
749
801
|
size: bundle.size
|
|
750
802
|
};
|
|
751
803
|
}
|
|
752
|
-
/** Allocate an opaque initial row revision without inspecting artifact bytes. */
|
|
753
|
-
allocateInitialRevision() {
|
|
754
|
-
return `${this.initialRevisionNonce}-${String(this.nextInitialRevision++)}`;
|
|
755
|
-
}
|
|
756
804
|
/**
|
|
757
|
-
* Read the activation-time bundle
|
|
805
|
+
* Read the activation-time bundle snapshot.
|
|
758
806
|
* @param pkgName - package that declares the client bundle.
|
|
759
807
|
* @param clientPath - absolute path of the built client artifact.
|
|
760
808
|
* @returns the immutable bytes plus the pre-read filesystem baseline.
|
|
@@ -763,27 +811,24 @@ var ClientModuleRegistry = class extends Service {
|
|
|
763
811
|
initialBundleSnapshot(pkgName, clientPath) {
|
|
764
812
|
try {
|
|
765
813
|
const baseline = this.captureArtifactBaseline(clientPath);
|
|
766
|
-
const bundle = readFileSync(clientPath);
|
|
767
|
-
const sourceMap = this.readSourceMapSnapshot(clientPath);
|
|
768
814
|
return {
|
|
769
|
-
bundle,
|
|
770
|
-
baseline
|
|
771
|
-
...sourceMap === void 0 ? {} : { sourceMap }
|
|
815
|
+
bundle: readFileSync(clientPath),
|
|
816
|
+
baseline
|
|
772
817
|
};
|
|
773
818
|
} catch (error) {
|
|
774
819
|
if (error.code !== "ENOENT") throw error;
|
|
775
820
|
throw new MissingClientBundleError(pkgName, clientPath, error);
|
|
776
821
|
}
|
|
777
822
|
}
|
|
778
|
-
/** Treat a missing, torn, or malformed development map as an identity
|
|
779
|
-
|
|
823
|
+
/** Treat a missing, torn, or malformed development map as an identity section. */
|
|
824
|
+
readSourceMap = (clientPath) => {
|
|
780
825
|
try {
|
|
781
|
-
return
|
|
826
|
+
return readSourceMap(clientPath);
|
|
782
827
|
} catch (error) {
|
|
783
828
|
this.ctx.logger.warn(error);
|
|
784
829
|
return;
|
|
785
830
|
}
|
|
786
|
-
}
|
|
831
|
+
};
|
|
787
832
|
/** Reconcile one entry name against the live Loader sources. @returns whether the table changed. */
|
|
788
833
|
processOne(entryName, onError) {
|
|
789
834
|
const nextSources = /* @__PURE__ */ new Map();
|
|
@@ -834,15 +879,14 @@ var ClientModuleRegistry = class extends Service {
|
|
|
834
879
|
if (source === void 0) return this.table.delete(packageName);
|
|
835
880
|
if (this.table.get(packageName)?.sourceKey === source.sourceKey) return false;
|
|
836
881
|
const snapshot = this.initialBundleSnapshot(packageName, source.meta.clientPath);
|
|
837
|
-
const rev =
|
|
882
|
+
const rev = artifactRevision(snapshot.baseline);
|
|
838
883
|
this.table.set(packageName, {
|
|
839
884
|
entry: graphRow(packageName, rev, source.meta),
|
|
840
885
|
loaderName: source.loaderName,
|
|
841
886
|
sourceKey: source.sourceKey,
|
|
842
887
|
meta: source.meta,
|
|
843
888
|
bundle: snapshot.bundle,
|
|
844
|
-
baseline: snapshot.baseline
|
|
845
|
-
...snapshot.sourceMap === void 0 ? {} : { sourceMap: snapshot.sourceMap }
|
|
889
|
+
baseline: snapshot.baseline
|
|
846
890
|
});
|
|
847
891
|
return true;
|
|
848
892
|
}
|
|
@@ -867,24 +911,68 @@ var ClientModuleRegistry = class extends Service {
|
|
|
867
911
|
this.composed = composed;
|
|
868
912
|
this.notifyGraphChanged();
|
|
869
913
|
}
|
|
870
|
-
|
|
914
|
+
/** Match an exact current-revision package-local chunk URL without reading its file. */
|
|
915
|
+
chunkRequest(requestUrl) {
|
|
916
|
+
const resourceUrl = `${requestUrl.pathname}${requestUrl.search}`;
|
|
917
|
+
for (const record of this.table.values()) {
|
|
918
|
+
const prefix = `/plugins/${record.entry.id}/`;
|
|
919
|
+
if (!requestUrl.pathname.startsWith(prefix)) continue;
|
|
920
|
+
const requested = requestUrl.pathname.slice(prefix.length);
|
|
921
|
+
const sourceMap = requested.endsWith(".map");
|
|
922
|
+
const fileName = sourceMap ? requested.slice(0, -4) : requested;
|
|
923
|
+
if (!CLIENT_CHUNK.test(fileName)) return void 0;
|
|
924
|
+
if (resourceUrl !== chunkUrl(record.entry.id, fileName, record.entry.rev, sourceMap)) return void 0;
|
|
925
|
+
return {
|
|
926
|
+
record,
|
|
927
|
+
fileName,
|
|
928
|
+
sourceMap,
|
|
929
|
+
resourceUrl
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/** Build a package-local chunk response only when its URL is requested. */
|
|
934
|
+
chunkResponse(requestUrl) {
|
|
935
|
+
const request = this.chunkRequest(requestUrl);
|
|
936
|
+
if (request === void 0) return void 0;
|
|
937
|
+
const { record, fileName, sourceMap, resourceUrl } = request;
|
|
938
|
+
const clientPath = join(dirname(record.meta.clientPath), fileName);
|
|
939
|
+
if (!existsSync(clientPath)) return void 0;
|
|
940
|
+
const sourceMapUrl = chunkMapReference(fileName, record.entry.rev);
|
|
941
|
+
const resource = () => ({
|
|
942
|
+
id: record.entry.id,
|
|
943
|
+
rev: record.entry.rev,
|
|
944
|
+
clientPath,
|
|
945
|
+
fileName,
|
|
946
|
+
bundle: readFileSync(clientPath)
|
|
947
|
+
});
|
|
948
|
+
const response = sourceMap ? {
|
|
949
|
+
body: lazyBody(() => buildComboSourceMap([resource()], this.readSourceMap, fileName)),
|
|
950
|
+
contentType: "application/json; charset=utf-8"
|
|
951
|
+
} : {
|
|
952
|
+
body: lazyBody(() => buildComboScript([resource()], sourceMapUrl)),
|
|
953
|
+
contentType: "text/javascript; charset=utf-8"
|
|
954
|
+
};
|
|
955
|
+
this.responses.set(resourceUrl, response);
|
|
956
|
+
return response;
|
|
957
|
+
}
|
|
958
|
+
async bundleResource(method, url) {
|
|
871
959
|
if (method !== "GET" && method !== "HEAD") return { status: 405 };
|
|
872
960
|
const requestUrl = new URL(url, "http://x");
|
|
873
961
|
const resourceUrl = `${requestUrl.pathname}${requestUrl.search}`;
|
|
874
|
-
const response = this.responses.get(resourceUrl) ?? this.previousBatchResponses.get(resourceUrl);
|
|
962
|
+
const response = this.responses.get(resourceUrl) ?? this.previousBatchResponses.get(resourceUrl) ?? this.chunkResponse(requestUrl);
|
|
875
963
|
if (response !== void 0) return {
|
|
876
964
|
status: 200,
|
|
877
965
|
headers: {
|
|
878
966
|
"content-type": response.contentType,
|
|
879
967
|
"cache-control": IMMUTABLE_CACHE
|
|
880
968
|
},
|
|
881
|
-
...method === "HEAD" ? {} : { body: response.body }
|
|
969
|
+
...method === "HEAD" ? {} : { body: await response.body() }
|
|
882
970
|
};
|
|
883
971
|
return { status: 404 };
|
|
884
972
|
}
|
|
885
|
-
serveBundle = (req, res) => {
|
|
973
|
+
serveBundle = async (req, res) => {
|
|
886
974
|
/* v8 ignore next -- `?? '/'` arm: node:http always sets url on server requests. */
|
|
887
|
-
const response = this.bundleResource(req.method, req.url ?? "/");
|
|
975
|
+
const response = await this.bundleResource(req.method, req.url ?? "/");
|
|
888
976
|
res.writeHead(response.status, response.headers);
|
|
889
977
|
res.end(response.body);
|
|
890
978
|
};
|
package/lib/invariant.js
CHANGED
|
@@ -11,8 +11,8 @@ const inject = ["invariants"];
|
|
|
11
11
|
/**
|
|
12
12
|
* Owned relation: the node half's boot entry graph must stay self-consistent
|
|
13
13
|
* — every row must resolve a clientPath under the same id (the
|
|
14
|
-
*
|
|
15
|
-
* that just received the graph). Checked on every scan trigger (cordis
|
|
14
|
+
* `plugins/<id>/client.js` reference it advertises would otherwise 404 in the
|
|
15
|
+
* browser that just received the graph). Checked on every scan trigger (cordis
|
|
16
16
|
* 'internal/plugin'): graph() and clientPath() read the same table object,
|
|
17
17
|
* so the relation holds at any instant — no need to wait out the node half's
|
|
18
18
|
* own microtask-debounced flush.
|
|
@@ -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
|
|
@@ -14,6 +14,8 @@ 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 { tearDownEntryFiber } from './entry-lifecycle.ts';
|
|
18
|
+
export type { ClientEntries, ClientEntryState } from './entries.ts';
|
|
17
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
|
/**
|