@crazx/dsh-client-modules 0.1.1-rc.2.zw.1 → 0.1.2-alpha.3.zw.2

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