@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/client.js
CHANGED
|
@@ -50,6 +50,43 @@ window.__ModuleLoader__.load({
|
|
|
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
|
|
@@ -135,6 +172,274 @@ window.__ModuleLoader__.load({
|
|
|
135
172
|
};
|
|
136
173
|
}
|
|
137
174
|
//#endregion
|
|
175
|
+
//#region lib/types/client/entry-lifecycle.js
|
|
176
|
+
/**
|
|
177
|
+
* Release a runtime before clearing its entry fiber so Loader refresh can import new code.
|
|
178
|
+
* Registry deletion prevents Loader from treating replacement as a user disable.
|
|
179
|
+
* @param entry - Entry retained for code replacement.
|
|
180
|
+
*/
|
|
181
|
+
async function tearDownEntryFiber(entry) {
|
|
182
|
+
const fiber = entry.fiber;
|
|
183
|
+
if (fiber === void 0) return;
|
|
184
|
+
const runtime = fiber.runtime;
|
|
185
|
+
/* v8 ignore next -- Loader entries own plugin fibers; only the root context has a null runtime. */
|
|
186
|
+
if (runtime !== null) entry.ctx.registry.delete(runtime.callback);
|
|
187
|
+
while (fiber.inertia !== void 0) await fiber.inertia;
|
|
188
|
+
delete entry.fiber;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Remove styles after their plugin's effect cleanup has settled.
|
|
192
|
+
* @param id - Package whose factory owns the style tags.
|
|
193
|
+
*/
|
|
194
|
+
function removeOwnedStyles(id) {
|
|
195
|
+
if (typeof document === "undefined") return;
|
|
196
|
+
for (const el of document.querySelectorAll("style[data-plugin]")) if (el.getAttribute("data-plugin") === id) el.remove();
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region lib/types/client/entries.js
|
|
200
|
+
/** Numeric values mirror Cordis's const enum, which bundle loaders cannot import as a runtime object. */
|
|
201
|
+
const ACTIVE = 2;
|
|
202
|
+
const FAILED = 3;
|
|
203
|
+
/** Revisions and requests identify desired code; URLs only select its immutable delivery resource. */
|
|
204
|
+
function entryTargets(manifest) {
|
|
205
|
+
return JSON.stringify(manifest.modules.map((row) => [
|
|
206
|
+
row.id,
|
|
207
|
+
row.rev,
|
|
208
|
+
row.inject,
|
|
209
|
+
row.external
|
|
210
|
+
]));
|
|
211
|
+
}
|
|
212
|
+
/** Manages only entries created from the Host manifest; other Loader contributors retain ownership. */
|
|
213
|
+
var ClientEntries = class {
|
|
214
|
+
modules;
|
|
215
|
+
index;
|
|
216
|
+
/** Stable observable consumed by page diagnostics through the renderer's injected hook. */
|
|
217
|
+
state = {
|
|
218
|
+
getSnapshot: () => this.snapshot,
|
|
219
|
+
subscribe: (listener) => {
|
|
220
|
+
this.listeners.add(listener);
|
|
221
|
+
return () => {
|
|
222
|
+
this.listeners.delete(listener);
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
snapshot = {
|
|
227
|
+
syncing: false,
|
|
228
|
+
failures: []
|
|
229
|
+
};
|
|
230
|
+
listeners = /* @__PURE__ */ new Set();
|
|
231
|
+
managed = /* @__PURE__ */ new Map();
|
|
232
|
+
revisions = /* @__PURE__ */ new Map();
|
|
233
|
+
loader;
|
|
234
|
+
queue = Promise.resolve();
|
|
235
|
+
desired;
|
|
236
|
+
generation = 0;
|
|
237
|
+
stopped = false;
|
|
238
|
+
/**
|
|
239
|
+
* Construct the page controller before Cordis boot.
|
|
240
|
+
* @param modules - Module arrival and materialization owner.
|
|
241
|
+
* @param index - Private descriptor replacement and unused-module cleanup.
|
|
242
|
+
*/
|
|
243
|
+
constructor(modules, index) {
|
|
244
|
+
this.modules = modules;
|
|
245
|
+
this.index = index;
|
|
246
|
+
this.desired = modules.manifest;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Create the initial roster and retain its entry identities for subsequent reconciliation.
|
|
250
|
+
* @param loader - Page Loader, already configured with the module system.
|
|
251
|
+
* @param manifest - Initial roster audited by the boot caller.
|
|
252
|
+
* @returns after initial entries and their activation settle; boot owns its activation audit.
|
|
253
|
+
*/
|
|
254
|
+
start(loader, manifest) {
|
|
255
|
+
if (this.loader !== void 0) throw new Error("client-modules: entries already started");
|
|
256
|
+
this.loader = loader;
|
|
257
|
+
this.desired = manifest;
|
|
258
|
+
loader.ctx.effect(() => () => {
|
|
259
|
+
this.stopped = true;
|
|
260
|
+
this.generation++;
|
|
261
|
+
return this.queue;
|
|
262
|
+
}, "client-modules: entry reconciliation");
|
|
263
|
+
return this.enqueue(async () => {
|
|
264
|
+
await Promise.all(this.desired.plugins.map(async ({ id }) => {
|
|
265
|
+
await this.create(loader, id);
|
|
266
|
+
}));
|
|
267
|
+
await loader.await();
|
|
268
|
+
for (const row of this.modules.manifest.modules) this.revisions.set(row.id, row.rev);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Validate and apply the latest full Host graph. Changed targets cancel obsolete mounts; identical targets share pending loads.
|
|
273
|
+
* @param graph - JSON-decoded graph received from the Host.
|
|
274
|
+
* @returns after the queued reconciliation; per-package failures remain available in {@link state}.
|
|
275
|
+
*/
|
|
276
|
+
sync(graph) {
|
|
277
|
+
const manifest = parseBootManifest(graph);
|
|
278
|
+
if (entryTargets(manifest) !== entryTargets(this.desired)) this.generation++;
|
|
279
|
+
this.desired = manifest;
|
|
280
|
+
const generation = this.generation;
|
|
281
|
+
return this.enqueue(() => this.reconcile(generation));
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Retry failed entries against the latest graph, including an unchanged revision.
|
|
285
|
+
* @returns after retry settlement, with remaining errors in {@link state}.
|
|
286
|
+
*/
|
|
287
|
+
retry() {
|
|
288
|
+
const generation = ++this.generation;
|
|
289
|
+
return this.enqueue(() => this.reconcile(generation));
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Replace one entry's code in the same queue as graph updates; duplicate revisions are ignored.
|
|
293
|
+
* Entries missing after a failed import are reconciled; bootstrap replacement fails before teardown.
|
|
294
|
+
* @param id - Package id from a rebuilt frame.
|
|
295
|
+
* @param rev - Opaque revision selecting the rebuilt artifact.
|
|
296
|
+
* @returns after queued work; replacement errors reject, while per-package reconciliation errors remain in {@link state}.
|
|
297
|
+
*/
|
|
298
|
+
reload(id, rev) {
|
|
299
|
+
this.desired = {
|
|
300
|
+
...this.desired,
|
|
301
|
+
modules: this.desired.modules.map((row) => row.id === id ? {
|
|
302
|
+
...row,
|
|
303
|
+
rev
|
|
304
|
+
} : row)
|
|
305
|
+
};
|
|
306
|
+
return this.enqueue(async () => {
|
|
307
|
+
const desired = this.desired.modules.find((row) => row.id === id);
|
|
308
|
+
if (this.stopped || desired === void 0) return;
|
|
309
|
+
const entry = this.managed.get(id);
|
|
310
|
+
if (entry === void 0) {
|
|
311
|
+
this.modules.invalidate(id, desired.rev);
|
|
312
|
+
removeOwnedStyles(id);
|
|
313
|
+
await this.reconcile(this.generation);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (this.revisions.get(id) === rev) return;
|
|
317
|
+
this.publish({
|
|
318
|
+
syncing: true,
|
|
319
|
+
failures: this.snapshot.failures.filter((failure) => failure.id !== id)
|
|
320
|
+
});
|
|
321
|
+
await this.replace(entry, id, rev, this.generation);
|
|
322
|
+
this.publish({
|
|
323
|
+
syncing: false,
|
|
324
|
+
failures: this.snapshot.failures
|
|
325
|
+
});
|
|
326
|
+
}, id);
|
|
327
|
+
}
|
|
328
|
+
publish(snapshot) {
|
|
329
|
+
this.snapshot = snapshot;
|
|
330
|
+
for (const listener of [...this.listeners]) try {
|
|
331
|
+
listener();
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error("client-modules: synchronization subscriber failed", error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
enqueue(task, subject = "graph") {
|
|
337
|
+
const run = this.queue.then(task);
|
|
338
|
+
this.queue = run.then(() => void 0, (error) => {
|
|
339
|
+
this.publish({
|
|
340
|
+
syncing: false,
|
|
341
|
+
failures: [...this.snapshot.failures.filter((failure) => failure.id !== subject), {
|
|
342
|
+
id: subject,
|
|
343
|
+
message: String(error)
|
|
344
|
+
}]
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
return run;
|
|
348
|
+
}
|
|
349
|
+
current(generation) {
|
|
350
|
+
return !this.stopped && generation === this.generation;
|
|
351
|
+
}
|
|
352
|
+
/** Keep ownership even when Loader rejects a module's plugin exports after inserting its entry. */
|
|
353
|
+
async create(loader, id) {
|
|
354
|
+
const options = { name: id };
|
|
355
|
+
const entryId = loader.ensureId(options);
|
|
356
|
+
try {
|
|
357
|
+
await loader.create(options);
|
|
358
|
+
} finally {
|
|
359
|
+
this.managed.set(id, loader.resolve(entryId));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async replace(entry, id, rev, generation) {
|
|
363
|
+
this.index.invalidateForReplacement(id, rev);
|
|
364
|
+
await this.modules.prefetch(id);
|
|
365
|
+
if (!this.current(generation)) return;
|
|
366
|
+
await tearDownEntryFiber(entry);
|
|
367
|
+
removeOwnedStyles(id);
|
|
368
|
+
if (!this.current(generation)) return;
|
|
369
|
+
await this.modules.import(id, "", {});
|
|
370
|
+
if (!this.current(generation)) return;
|
|
371
|
+
await entry.refresh();
|
|
372
|
+
await entry.fiber?.await();
|
|
373
|
+
if (entry.fiber === void 0) throw new Error(`client-modules: ${id} import failed (see console)`);
|
|
374
|
+
this.revisions.set(id, rev);
|
|
375
|
+
}
|
|
376
|
+
async reconcile(generation) {
|
|
377
|
+
if (!this.current(generation)) return;
|
|
378
|
+
const loader = this.loader;
|
|
379
|
+
if (loader === void 0) throw new Error("client-modules: entries have not started");
|
|
380
|
+
const manifest = this.desired;
|
|
381
|
+
this.publish({
|
|
382
|
+
syncing: true,
|
|
383
|
+
failures: []
|
|
384
|
+
});
|
|
385
|
+
const failures = [];
|
|
386
|
+
this.index.update(manifest, this.managed.keys());
|
|
387
|
+
const wanted = new Set(manifest.plugins.map((row) => row.id));
|
|
388
|
+
for (const [id, entry] of this.managed) {
|
|
389
|
+
if (wanted.has(id)) continue;
|
|
390
|
+
const fiber = entry.fiber;
|
|
391
|
+
loader.remove(entry.id);
|
|
392
|
+
this.managed.delete(id);
|
|
393
|
+
this.revisions.delete(id);
|
|
394
|
+
while (fiber?.inertia !== void 0) await fiber.inertia;
|
|
395
|
+
}
|
|
396
|
+
for (const row of manifest.modules) {
|
|
397
|
+
if (!this.current(generation)) break;
|
|
398
|
+
try {
|
|
399
|
+
const entry = this.managed.get(row.id);
|
|
400
|
+
if (entry === void 0) {
|
|
401
|
+
await this.modules.prefetch(row.id);
|
|
402
|
+
if (!this.current(generation)) break;
|
|
403
|
+
await this.modules.import(row.id, "", {});
|
|
404
|
+
if (!this.current(generation)) break;
|
|
405
|
+
await this.create(loader, row.id);
|
|
406
|
+
this.revisions.set(row.id, row.rev);
|
|
407
|
+
} else if (this.revisions.get(row.id) !== row.rev) await this.replace(entry, row.id, row.rev, generation);
|
|
408
|
+
else if (entry.fiber === void 0) await this.replace(entry, row.id, row.rev, generation);
|
|
409
|
+
else if (entry.fiber.state === FAILED) entry.fiber.update(entry.options.config);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
failures.push({
|
|
412
|
+
id: row.id,
|
|
413
|
+
message: String(error)
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
await loader.await();
|
|
418
|
+
for (const [id, entry] of this.managed) {
|
|
419
|
+
if (failures.some((failure) => failure.id === id)) continue;
|
|
420
|
+
if (entry.fiber?.state === ACTIVE) continue;
|
|
421
|
+
try {
|
|
422
|
+
if (entry.fiber === void 0) throw new Error(`client-modules: ${id} import failed (see console)`);
|
|
423
|
+
await entry.fiber.await();
|
|
424
|
+
failures.push({
|
|
425
|
+
id,
|
|
426
|
+
message: `client-modules: ${id} is waiting for activation`
|
|
427
|
+
});
|
|
428
|
+
} catch (error) {
|
|
429
|
+
failures.push({
|
|
430
|
+
id,
|
|
431
|
+
message: String(error)
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
this.index.prune([...loader.entries()].map((entry) => entry.options.name));
|
|
436
|
+
if (this.current(generation)) this.publish({
|
|
437
|
+
syncing: false,
|
|
438
|
+
failures
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
//#endregion
|
|
138
443
|
//#region lib/types/client/system.js
|
|
139
444
|
/**
|
|
140
445
|
* ClientModuleSystem — the implementation behind the {@link ClientModuleLoader}
|
|
@@ -162,6 +467,19 @@ window.__ModuleLoader__.load({
|
|
|
162
467
|
if (!/[?&]rev=[^&#]*/.test(url)) throw new Error(`client-modules: bundle URL ${url} has no revision`);
|
|
163
468
|
return url.replace(/([?&]rev=)[^&#]*/, `$1${encodeURIComponent(rev)}`);
|
|
164
469
|
}
|
|
470
|
+
const CLIENT_CHUNK = /^client\.[A-Za-z0-9][A-Za-z0-9._-]*\.js$/;
|
|
471
|
+
/** Internal module-table key for one package-local chunk. */
|
|
472
|
+
function chunkId(ownerId, fileName) {
|
|
473
|
+
return `${ownerId}/${fileName}`;
|
|
474
|
+
}
|
|
475
|
+
/** Resolve a sibling chunk against the package's one-resource URL and current revision. */
|
|
476
|
+
function chunkUrl(row, fileName, rev) {
|
|
477
|
+
const url = atRevision(row.url, rev);
|
|
478
|
+
const resourceStart = url.indexOf("/??");
|
|
479
|
+
const revisionStart = url.indexOf("&rev=", resourceStart + 3);
|
|
480
|
+
if ((resourceStart < 0 || revisionStart < 0 ? void 0 : url.slice(resourceStart + 3, revisionStart)) !== `${row.id}/client.js`) throw new Error(`client-modules: cannot resolve chunk ${JSON.stringify(fileName)} from bundle URL ${url}`);
|
|
481
|
+
return `${url.slice(0, resourceStart)}/${row.id}/${fileName}?${url.slice(revisionStart + 1)}`;
|
|
482
|
+
}
|
|
165
483
|
/**
|
|
166
484
|
* Claim and inventory the <style> tags a factory injected during
|
|
167
485
|
* materialization: preset-emitted tags arrive pre-tagged with data-plugin;
|
|
@@ -184,14 +502,17 @@ window.__ModuleLoader__.load({
|
|
|
184
502
|
var ClientModuleSystem = class {
|
|
185
503
|
version = "client";
|
|
186
504
|
manifest;
|
|
505
|
+
entries;
|
|
187
506
|
loadCache = /* @__PURE__ */ new Map();
|
|
188
507
|
seed;
|
|
189
508
|
factories = /* @__PURE__ */ new Map();
|
|
190
509
|
bootstrapIds = /* @__PURE__ */ new Set();
|
|
191
510
|
/** In-flight script transport per URL; every row in one batch shares it. */
|
|
192
511
|
pendingArrival = /* @__PURE__ */ new Map();
|
|
512
|
+
/** Owner generation captured by in-flight chunk requests and advanced on invalidation. */
|
|
513
|
+
generations = /* @__PURE__ */ new Map();
|
|
193
514
|
/** Single-resource combo URL selected by HMR after invalidating one row. */
|
|
194
|
-
|
|
515
|
+
reloadTargets = /* @__PURE__ */ new Map();
|
|
195
516
|
/** Materialization re-entrancy guard: factory-form CJS cannot deliver partial exports, so a cycle is fatal. */
|
|
196
517
|
materializing = /* @__PURE__ */ new Set();
|
|
197
518
|
graphRows = /* @__PURE__ */ new Map();
|
|
@@ -202,12 +523,21 @@ window.__ModuleLoader__.load({
|
|
|
202
523
|
*/
|
|
203
524
|
constructor(options) {
|
|
204
525
|
this.manifest = options.manifest;
|
|
526
|
+
this.entries = new ClientEntries(this, {
|
|
527
|
+
update: (manifest, managed) => {
|
|
528
|
+
this.updateManifest(manifest, managed);
|
|
529
|
+
},
|
|
530
|
+
invalidateForReplacement: (id, rev) => {
|
|
531
|
+
if (this.bootstrapIds.has(id)) throw new Error(`client-modules: replacing bootstrap module ${id} requires a page reload`);
|
|
532
|
+
this.invalidate(id, rev);
|
|
533
|
+
},
|
|
534
|
+
prune: (roots) => {
|
|
535
|
+
this.prune(roots);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
205
538
|
this.seed = new Map(Object.entries(options.staticModules));
|
|
206
539
|
this.loadBundle = options.loadBundle ?? defaultLoadBundle;
|
|
207
|
-
for (const row of options.manifest.modules)
|
|
208
|
-
if (this.graphRows.has(row.id)) throw new Error(`client-modules: duplicate graph entry "${row.id}"`);
|
|
209
|
-
this.graphRows.set(row.id, row);
|
|
210
|
-
}
|
|
540
|
+
for (const row of options.manifest.modules) this.graphRows.set(row.id, row);
|
|
211
541
|
const bootstrapId = stripClientSuffix(options.bootstrapModule.id);
|
|
212
542
|
this.bootstrapIds.add(bootstrapId);
|
|
213
543
|
this.loadCache.set(bootstrapId, {
|
|
@@ -227,16 +557,24 @@ window.__ModuleLoader__.load({
|
|
|
227
557
|
}
|
|
228
558
|
/** Register one bundle factory, rejecting a script that executes twice without invalidation. */
|
|
229
559
|
register(registration) {
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
232
|
-
|
|
560
|
+
const ownerId = stripClientSuffix(registration.id);
|
|
561
|
+
if (registration.chunk !== void 0 && !CLIENT_CHUNK.test(registration.chunk)) throw new Error(`client-modules: invalid package-local chunk ${JSON.stringify(registration.chunk)}`);
|
|
562
|
+
const id = registration.chunk === void 0 ? ownerId : chunkId(ownerId, registration.chunk);
|
|
563
|
+
if (this.bootstrapIds.has(id) || this.factories.has(id)) {
|
|
564
|
+
const registrationName = registration.chunk === void 0 ? registration.id : id;
|
|
565
|
+
throw new Error(`client-modules: duplicate factory registration for "${registrationName}" (bundle executed twice without invalidate?)`);
|
|
566
|
+
}
|
|
567
|
+
this.factories.set(id, {
|
|
568
|
+
factory: registration.factory,
|
|
569
|
+
rev: this.reloadTargets.get(ownerId)?.rev ?? this.graphRows.get(ownerId)?.rev
|
|
570
|
+
});
|
|
233
571
|
}
|
|
234
572
|
/** Load one graph row so its factory is registered (idempotent per in-flight arrival). */
|
|
235
573
|
arrive(row) {
|
|
236
574
|
const { id } = row;
|
|
237
575
|
if (this.loadCache.has(id) || this.factories.has(id)) return Promise.resolve();
|
|
238
|
-
const
|
|
239
|
-
const url =
|
|
576
|
+
const reload = this.reloadTargets.get(id);
|
|
577
|
+
const url = reload?.url ?? row.initialUrl;
|
|
240
578
|
let transport = this.pendingArrival.get(url);
|
|
241
579
|
if (transport === void 0) {
|
|
242
580
|
transport = this.loadBundle(url).finally(() => {
|
|
@@ -246,7 +584,7 @@ window.__ModuleLoader__.load({
|
|
|
246
584
|
}
|
|
247
585
|
return transport.then(() => {
|
|
248
586
|
if (!this.factories.has(id)) throw new Error(`client-modules: bundle ${url} loaded without registering "${id}" via __ModuleLoader__.load`);
|
|
249
|
-
if (
|
|
587
|
+
if (reload !== void 0 && this.reloadTargets.get(id) === reload) this.reloadTargets.delete(id);
|
|
250
588
|
});
|
|
251
589
|
}
|
|
252
590
|
/** Register each injected package and unresolved dynamic request before its consumer. */
|
|
@@ -269,7 +607,7 @@ window.__ModuleLoader__.load({
|
|
|
269
607
|
await this.arrive(row);
|
|
270
608
|
}
|
|
271
609
|
/** Materialize a registered factory (synchronous; memoized in loadCache). */
|
|
272
|
-
materialize(id) {
|
|
610
|
+
materialize(id, ownerId = id) {
|
|
273
611
|
const existing = this.loadCache.get(id);
|
|
274
612
|
if (existing !== void 0) return existing;
|
|
275
613
|
const registered = this.factories.get(id);
|
|
@@ -281,24 +619,22 @@ window.__ModuleLoader__.load({
|
|
|
281
619
|
const edges = /* @__PURE__ */ new Set();
|
|
282
620
|
const record = {
|
|
283
621
|
id,
|
|
284
|
-
exports: registered(this.makeRequire(edges)),
|
|
285
|
-
styles: claimStyles(
|
|
622
|
+
exports: registered.factory(this.makeRequire(ownerId, edges)),
|
|
623
|
+
styles: claimStyles(ownerId),
|
|
286
624
|
edges
|
|
287
625
|
};
|
|
288
626
|
this.loadCache.set(id, record);
|
|
289
627
|
return record;
|
|
628
|
+
} catch (error) {
|
|
629
|
+
removeOwnedStyles(ownerId);
|
|
630
|
+
throw error;
|
|
290
631
|
} finally {
|
|
291
632
|
this.materializing.delete(id);
|
|
292
633
|
}
|
|
293
634
|
}
|
|
294
|
-
/**
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
* from here; an external dynamic package must have arrived before its
|
|
298
|
-
* consumer materializes.
|
|
299
|
-
*/
|
|
300
|
-
makeRequire(edges) {
|
|
301
|
-
return (spec) => {
|
|
635
|
+
/** Build the synchronous module-table require and its asynchronous chunk operation. */
|
|
636
|
+
makeRequire(ownerId, edges) {
|
|
637
|
+
const require = (spec) => {
|
|
302
638
|
edges.add(spec);
|
|
303
639
|
if (this.seed.has(spec)) return this.seed.get(spec);
|
|
304
640
|
const id = stripClientSuffix(spec);
|
|
@@ -307,6 +643,41 @@ window.__ModuleLoader__.load({
|
|
|
307
643
|
if (this.factories.has(id)) return this.materialize(id).exports;
|
|
308
644
|
throw new Error(`client-modules: require("${spec}") missed the module table — not a platform seed word, not a materialized module, and no registered package factory (a build-time externals drift, or a dynamic dependency that did not arrive)`);
|
|
309
645
|
};
|
|
646
|
+
require.async = async (spec) => {
|
|
647
|
+
edges.add(spec);
|
|
648
|
+
if (!spec.startsWith("./")) return await this.import(spec);
|
|
649
|
+
const fileName = spec.slice(2);
|
|
650
|
+
if (!CLIENT_CHUNK.test(fileName)) throw new Error(`client-modules: invalid relative chunk request ${JSON.stringify(spec)}`);
|
|
651
|
+
return await this.importChunk(ownerId, fileName);
|
|
652
|
+
};
|
|
653
|
+
return require;
|
|
654
|
+
}
|
|
655
|
+
/** Load, register, and materialize one package-local dynamic chunk. */
|
|
656
|
+
async importChunk(ownerId, fileName) {
|
|
657
|
+
const id = chunkId(ownerId, fileName);
|
|
658
|
+
const existing = this.loadCache.get(id);
|
|
659
|
+
if (existing !== void 0) return existing.exports;
|
|
660
|
+
if (!this.factories.has(id)) {
|
|
661
|
+
const generation = this.generations.get(ownerId) ?? 0;
|
|
662
|
+
const row = this.graphRows.get(ownerId);
|
|
663
|
+
if (row === void 0) throw new Error(`client-modules: chunk owner "${ownerId}" is not a boot graph entry`);
|
|
664
|
+
const url = chunkUrl(row, fileName, this.factories.get(ownerId)?.rev ?? this.reloadTargets.get(ownerId)?.rev ?? row.rev);
|
|
665
|
+
let transport = this.pendingArrival.get(url);
|
|
666
|
+
if (transport === void 0) {
|
|
667
|
+
transport = this.loadBundle(url).finally(() => {
|
|
668
|
+
this.pendingArrival.delete(url);
|
|
669
|
+
});
|
|
670
|
+
this.pendingArrival.set(url, transport);
|
|
671
|
+
}
|
|
672
|
+
await transport;
|
|
673
|
+
if ((this.generations.get(ownerId) ?? 0) !== generation) {
|
|
674
|
+
this.factories.delete(id);
|
|
675
|
+
this.loadCache.delete(id);
|
|
676
|
+
return await this.importChunk(ownerId, fileName);
|
|
677
|
+
}
|
|
678
|
+
if (!this.factories.has(id)) throw new Error(`client-modules: bundle ${url} loaded without registering "${id}" via __ModuleLoader__.load`);
|
|
679
|
+
}
|
|
680
|
+
return this.materialize(id, ownerId).exports;
|
|
310
681
|
}
|
|
311
682
|
async import(specifier) {
|
|
312
683
|
if (this.seed.has(specifier)) return this.seed.get(specifier);
|
|
@@ -325,50 +696,101 @@ window.__ModuleLoader__.load({
|
|
|
325
696
|
if (row === void 0) throw new Error(`client-modules: prefetch("${id}") — not a graph entry`);
|
|
326
697
|
await this.arriveGraphRow(row);
|
|
327
698
|
}
|
|
699
|
+
/** Refresh descriptors and unowned factory revisions before any entry imports its dependencies. */
|
|
700
|
+
updateManifest(manifest, managed) {
|
|
701
|
+
for (const id of this.bootstrapIds) if (this.manifest.modules.some((row) => row.id === id) && !manifest.modules.some((row) => row.id === id)) throw new Error(`client-modules: removing bootstrap module ${id} requires a page reload`);
|
|
702
|
+
const owned = new Set(managed);
|
|
703
|
+
for (const row of manifest.modules) {
|
|
704
|
+
this.graphRows.set(row.id, {
|
|
705
|
+
...row,
|
|
706
|
+
initialUrl: row.url
|
|
707
|
+
});
|
|
708
|
+
const cachedRevision = this.factories.get(row.id)?.rev ?? this.reloadTargets.get(row.id)?.rev;
|
|
709
|
+
if (!owned.has(row.id) && cachedRevision !== void 0 && cachedRevision !== row.rev) {
|
|
710
|
+
this.invalidate(row.id, row.rev);
|
|
711
|
+
removeOwnedStyles(row.id);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
this.manifest = manifest;
|
|
715
|
+
}
|
|
716
|
+
/** Retain live Loader modules and their transitive requests before evicting unreferenced graph records. */
|
|
717
|
+
prune(roots) {
|
|
718
|
+
const retained = new Set(this.bootstrapIds);
|
|
719
|
+
const visit = (specifier) => {
|
|
720
|
+
const id = stripClientSuffix(specifier);
|
|
721
|
+
if (retained.has(id)) return;
|
|
722
|
+
retained.add(id);
|
|
723
|
+
const row = this.graphRows.get(id);
|
|
724
|
+
for (const request of [
|
|
725
|
+
...row?.external ?? [],
|
|
726
|
+
...row?.inject ?? [],
|
|
727
|
+
...this.loadCache.get(id)?.edges ?? []
|
|
728
|
+
]) visit(request);
|
|
729
|
+
};
|
|
730
|
+
for (const row of this.manifest.modules) visit(row.id);
|
|
731
|
+
for (const id of roots) visit(id);
|
|
732
|
+
for (const id of this.graphRows.keys()) {
|
|
733
|
+
if (retained.has(id)) continue;
|
|
734
|
+
this.graphRows.delete(id);
|
|
735
|
+
this.invalidate(id);
|
|
736
|
+
removeOwnedStyles(id);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
328
739
|
invalidate(id, rev) {
|
|
329
740
|
const normalized = stripClientSuffix(id);
|
|
330
741
|
if (this.bootstrapIds.has(normalized)) return;
|
|
742
|
+
this.generations.set(normalized, (this.generations.get(normalized) ?? 0) + 1);
|
|
331
743
|
const row = this.graphRows.get(normalized);
|
|
332
|
-
if (row !== void 0)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
744
|
+
if (row !== void 0) {
|
|
745
|
+
const revision = rev ?? row.rev;
|
|
746
|
+
this.reloadTargets.set(normalized, {
|
|
747
|
+
url: atRevision(row.url, revision),
|
|
748
|
+
rev: revision
|
|
749
|
+
});
|
|
750
|
+
} else this.reloadTargets.delete(normalized);
|
|
751
|
+
for (const key of this.factories.keys()) if (key === normalized || key.startsWith(`${normalized}/client.`)) this.factories.delete(key);
|
|
752
|
+
for (const key of this.loadCache.keys()) if (key === normalized || key.startsWith(`${normalized}/client.`)) this.loadCache.delete(key);
|
|
336
753
|
}
|
|
337
754
|
};
|
|
338
755
|
//#endregion
|
|
339
756
|
//#region lib/types/client/index.js
|
|
340
|
-
let moduleSystem;
|
|
341
757
|
/**
|
|
342
758
|
* Build the live module system from the HTML facade's materialized modules bundle.
|
|
343
759
|
* @param target - Stable registration facade whose pending queue becomes the live sink.
|
|
344
760
|
* @param bootstrapModule - This bundle's id and already-materialized exports.
|
|
345
761
|
* @param options - Raw boot graph, platform seed, and optional bundle transport.
|
|
346
|
-
* @returns The created module system
|
|
762
|
+
* @returns The created module system.
|
|
347
763
|
*/
|
|
348
764
|
function createClientModuleSystem(target, bootstrapModule, options) {
|
|
349
|
-
|
|
765
|
+
return new ClientModuleSystem({
|
|
350
766
|
manifest: parseBootManifest(options.boot),
|
|
351
767
|
staticModules: options.staticModules,
|
|
352
768
|
registrationTarget: target,
|
|
353
769
|
bootstrapModule,
|
|
354
770
|
...options.loadBundle === void 0 ? {} : { loadBundle: options.loadBundle }
|
|
355
771
|
});
|
|
356
|
-
return moduleSystem;
|
|
357
772
|
}
|
|
773
|
+
/** Required service: the Loader whose internal module system this plugin publishes. */
|
|
774
|
+
const inject = ["loader"];
|
|
358
775
|
/**
|
|
359
776
|
* Enroll the kernel-built module system as `ctx.modules`.
|
|
360
777
|
* @param ctx - client root context.
|
|
361
778
|
*/
|
|
362
779
|
function apply(ctx) {
|
|
363
|
-
|
|
364
|
-
|
|
780
|
+
const modules = ctx.loader.internal;
|
|
781
|
+
if (modules?.version !== "client") throw new Error("client-modules: the Loader has no client module system");
|
|
782
|
+
ctx.reflect.provide("modules", modules);
|
|
365
783
|
}
|
|
366
784
|
//#endregion
|
|
367
785
|
exports.ClientModuleSystem = ClientModuleSystem;
|
|
368
786
|
exports.apply = apply;
|
|
369
787
|
exports.createClientModuleSystem = createClientModuleSystem;
|
|
788
|
+
exports.exactPackageSpecifier = exactPackageSpecifier;
|
|
789
|
+
exports.inject = inject;
|
|
370
790
|
exports.parseBootManifest = parseBootManifest;
|
|
791
|
+
exports.parseDshClient = parseDshClient;
|
|
371
792
|
exports.stripClientSuffix = stripClientSuffix;
|
|
793
|
+
exports.tearDownEntryFiber = tearDownEntryFiber;
|
|
372
794
|
return module.exports;
|
|
373
795
|
}
|
|
374
796
|
});
|