@hydraharness/harness-client-modules 0.1.1-rc.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @hydraharness/harness-client-modules
2
+
3
+ Client module system: the browser peer of Node's internal ESM loader, built as a lazy CJS table. The web shell mounts the vendored cordis Loader for entry governance (fiber lifecycle, inject waiting, update/refresh) and injects this package's `ClientModuleLoader` through its `internal` contract — the vendored side's only consumption point is `EntryTree.import`, so replacing `internal` replaces exactly "how plugin code arrives" and nothing else.
4
+
5
+ Lazy CJS model (web2): executing a plugin bundle only REGISTERS its factory (`window.__ModuleLoader__.load({id, factory})`); every module body side effect — CSS injection included — lives in the factory closure and runs at materialization (`factory(require)` → exports, memoized in `loadCache`), not at script execution. A factory that requires another registered-but-unmaterialized module materializes it recursively; graph composition places declared dynamic requests before their consumers, and require cycles throw because factory-form CJS cannot deliver partial exports. `<id>/client` and the bare id resolve to the same exports (a plugin bundle IS its package's client half).
6
+
7
+ The Host installs `window.__ModuleLoader__` before parser preloads run. Its queue-mode `load()` retains early registrations; `create()` materializes this package's factory with an external-rejecting bootstrap require and calls its `createClientModuleSystem` export. Construction caches those same exports as the modules row, switches the same facade to live registration, and drains the remaining queue. The bundle retains the resulting system in a module closure, so its later Cordis `apply()` provides the identical instance as `ctx.modules` without another page global.
8
+
9
+ Resolution branch order (`import(specifier)`): platform seed word → shell instance; memoized record → exports; graph row (`window.__HYDRA_BOOT__`) → register its classic-script factory; registered factory → materialize; anything else throws — the runtime mirror of the build-time bundle purity gate. The synchronous `require` handed to factories walks the same order minus the asynchronous graph-row load and records observed edges into the module record. `prefetch` is the stage-one arrival hook (script load and factory registration only; concurrent calls share one in-flight task); `invalidate` drops a non-bootstrap factory and materialized record so the next prefetch/import reloads the script (the HMR hook).
10
+
11
+ The Node half scans enabled Loader entries for web `hydra.client` packages, resolves each `exports["./client"]`, hashes the built bundle into the boot graph, carries package-specific `hydra.client.external` requests, orders dynamic providers before consumers, and serves each bundle with its source map under `/plugins`. Source launch maps host imports to TypeScript source but still consumes this built client export; missing files share one build instruction followed by a package/path list, while unrelated filesystem errors remain separate failures.
12
+
13
+ `hydra.client.external` is an optional exact-specifier request list beyond the implicit baseline: shell-seeded React, Cordis, and static UI libraries plus parser-preloaded runtime. A request is answered by the dynamic package row it names or an exact static-table key; only a trailing `/client` aliases a package row, and there is no provider-alias declaration. Type-only imports are erased and create no request. Composition rejects malformed requests, missing suppliers, self-requests, and synchronous request cycles; import and prefetch recursively register dynamic suppliers before their consumers materialize. See [shared modules and the module graph](../AGENTS.md#shared-modules-and-the-module-graph).
14
+
15
+ ## Model Experience
16
+
17
+ None, as the module loader is browser-side kernel machinery; nothing here reaches a model request.
18
+
19
+ #### KV Cache effect
20
+
21
+ None; this package neither assembles nor sends a provider request.
22
+
23
+ ## Known Limitations and Deferred Work
24
+
25
+ - **Flat module graph by design** — every bundle is one module node whose edges point only at table leaves; the interface (`loadCache`/`edges`/`invalidate`) already supports a general module graph, so the externalization granularity can change without an interface change.
26
+ - **No unload bookkeeping of its own** — style removal and fiber teardown ordering live with the HMR driver (`@hydraharness/harness-client-hmr`); the loader only inventories owned style tag ids per record.
package/lib/client.js ADDED
@@ -0,0 +1,324 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@hydraharness/harness-client-modules",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region lib/types/client/manifest.js
8
+ /**
9
+ * Client module system: the browser peer of Node's internal ESM loader, built
10
+ * as a lazy CJS table. The vendored cordis Loader consumes this object
11
+ * through its `internal` contract (the only call site is `EntryTree.import` →
12
+ * `internal.import`), which keeps entry governance (fiber lifecycle, inject
13
+ * waiting, update/refresh) entirely on the vendored side while this package
14
+ * owns code arrival.
15
+ *
16
+ * Lazy CJS model: executing a plugin bundle only REGISTERS its
17
+ * factory (`window.__ModuleLoader__.load({id, factory})`); every module body
18
+ * side effect — including CSS injection — lives inside the factory closure
19
+ * and runs at materialization, not at script execution. Materialization
20
+ * (factory(require) → exports) happens on first import/require and is
21
+ * memoized in {@link ClientModuleLoader.loadCache}; a factory that requires
22
+ * another registered-but-unmaterialized module materializes it recursively,
23
+ * so load order needs no external sequencing.
24
+ *
25
+ * Resolution branch order (import): seed word → shell instance; memoized
26
+ * record → exports; graph row → register its dependency factories and own
27
+ * factory; registered factory → materialize; anything else → throw (loud —
28
+ * the runtime mirror of the build-time bundle purity gate).
29
+ * The synchronous `require` handed to factories walks the same order minus
30
+ * the load branch. Loading is async, so a requested dynamic package must have
31
+ * registered its factory before a consumer materializes.
32
+ *
33
+ * This file is the browser-safe contract face (zero node imports): the
34
+ * `__HYDRA_BOOT__` wire types, the boot-manifest parser, and the boundaries around
35
+ * {@link ClientModuleSystem}. The package root is the host-side service that
36
+ * composes the wire.
37
+ */
38
+ /**
39
+ * Validate an optional string-array field read from a `hydra.client` declaration
40
+ * or from the boot wire.
41
+ * @param subject - diagnostic prefix naming the package or the wire row.
42
+ * @param field - field name as it appears in the diagnostic.
43
+ * @param value - the raw field value.
44
+ * @returns the validated array, or undefined when the field is absent.
45
+ * @throws {Error} when the value is present but is not an array of strings.
46
+ */
47
+ function optionalStringArray(subject, field, value) {
48
+ if (value === void 0) return void 0;
49
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new Error(`client-modules: ${subject} ${field} must be a string array`);
50
+ return value;
51
+ }
52
+ /**
53
+ * Normalize a module specifier onto the graph row that owns it: a plugin bundle
54
+ * IS its package's client half, so `<id>/client` (the exports subpath external
55
+ * bundles emit) and the bare package name resolve to the same exports. Both the
56
+ * require path and graph composition normalize here, which is what lets each
57
+ * importing package request the subpath its own code imports.
58
+ * @param spec - module specifier as a bundle requires it or a declaration spells it.
59
+ * @returns the specifier with a trailing `/client` removed.
60
+ */
61
+ function stripClientSuffix(spec) {
62
+ return spec.endsWith("/client") ? spec.slice(0, -7) : spec;
63
+ }
64
+ /**
65
+ * Parse `window.__HYDRA_BOOT__` into the two consumer views. Wire boundary:
66
+ * a missing or malformed graph throws (the shell shows the loud failure —
67
+ * a page without a valid manifest cannot boot anything).
68
+ * @param wire - the raw `window.__HYDRA_BOOT__` value.
69
+ * @returns the manifest with optional plugin-view fields normalized.
70
+ */
71
+ function parseBootManifest(wire) {
72
+ if (typeof wire !== "object" || wire === null) throw new Error("client-modules: window.__HYDRA_BOOT__ is missing or not an object");
73
+ const graph = wire;
74
+ if (typeof graph.rev !== "string") throw new Error("client-modules: boot manifest rev must be a string");
75
+ if (!Array.isArray(graph.entries)) throw new Error("client-modules: boot manifest entries must be an array");
76
+ const modules = [];
77
+ const plugins = [];
78
+ for (const value of graph.entries) {
79
+ if (typeof value !== "object" || value === null) throw new Error("client-modules: boot manifest entry is not an object");
80
+ const row = value;
81
+ const where = typeof row.id === "string" ? `"${row.id}"` : JSON.stringify(row);
82
+ if (typeof row.id !== "string" || typeof row.url !== "string" || typeof row.rev !== "string") throw new Error(`client-modules: boot manifest entry ${where} must carry string id/url/rev`);
83
+ const subject = `boot manifest entry ${where}`;
84
+ const inject = optionalStringArray(subject, "inject", row.inject);
85
+ const external = optionalStringArray(subject, "external", row.external);
86
+ if (row.immediately !== void 0 && typeof row.immediately !== "boolean") throw new Error(`client-modules: boot manifest entry ${where} immediately must be a boolean`);
87
+ modules.push({
88
+ id: row.id,
89
+ url: row.url,
90
+ rev: row.rev,
91
+ external: external === void 0 ? [] : [...external]
92
+ });
93
+ plugins.push({
94
+ id: row.id,
95
+ inject: inject === void 0 ? [] : [...inject],
96
+ immediately: row.immediately === true
97
+ });
98
+ }
99
+ return {
100
+ rev: graph.rev,
101
+ modules,
102
+ plugins
103
+ };
104
+ }
105
+ //#endregion
106
+ //#region lib/types/client/system.js
107
+ /**
108
+ * ClientModuleSystem — the implementation behind the {@link ClientModuleLoader}
109
+ * contract. The conceptual contract (lazy CJS model, resolution branch order) is
110
+ * documented on the public interfaces in `./manifest.ts`; this file owns the
111
+ * state tables and the load/materialize machinery.
112
+ */
113
+ /** Default bundle-load hook: same-origin external classic script. */
114
+ const defaultLoadBundle = (url) => new Promise((resolve, reject) => {
115
+ const el = document.createElement("script");
116
+ el.async = true;
117
+ el.src = url;
118
+ el.addEventListener("load", () => {
119
+ el.remove();
120
+ resolve();
121
+ }, { once: true });
122
+ el.addEventListener("error", () => {
123
+ el.remove();
124
+ reject(/* @__PURE__ */ new Error(`client-modules: bundle script ${url} failed to load`));
125
+ }, { once: true });
126
+ document.head.append(el);
127
+ });
128
+ /**
129
+ * Claim and inventory the <style> tags a factory injected during
130
+ * materialization: preset-emitted tags arrive pre-tagged with data-plugin;
131
+ * any untagged tag is claimed for the materializing plugin (HMR bookkeeping).
132
+ */
133
+ const claimStyles = (id) => {
134
+ if (typeof document === "undefined") return [];
135
+ for (const el of document.querySelectorAll("style:not([data-plugin])")) el.setAttribute("data-plugin", id);
136
+ const owned = [];
137
+ for (const el of document.querySelectorAll(`style[data-plugin=${JSON.stringify(id)}]`)) owned.push(el.getAttribute("data-plugin-css") ?? id);
138
+ return owned;
139
+ };
140
+ /**
141
+ * The client module system: state tables plus the arrival/materialization
142
+ * machinery implementing {@link ClientModuleLoader} (whose members carry the
143
+ * contract documentation). Construction indexes the boot rows, retains the
144
+ * already-materialized bootstrap module, and switches the HTML-installed
145
+ * loader facade from its pending queue to live registration.
146
+ */
147
+ var ClientModuleSystem = class {
148
+ version = "client";
149
+ manifest;
150
+ loadCache = /* @__PURE__ */ new Map();
151
+ seed;
152
+ factories = /* @__PURE__ */ new Map();
153
+ bootstrapIds = /* @__PURE__ */ new Set();
154
+ /** In-flight prefetch (script load) per id; concurrent callers share it. */
155
+ pendingArrival = /* @__PURE__ */ new Map();
156
+ /** Materialization re-entrancy guard: factory-form CJS cannot deliver partial exports, so a cycle is fatal. */
157
+ materializing = /* @__PURE__ */ new Set();
158
+ graphRows = /* @__PURE__ */ new Map();
159
+ loadBundle;
160
+ /**
161
+ * Build the module system over the parsed boot rows.
162
+ * @param options - Parsed graph, platform seed, bootstrap module, registration facade, and transport.
163
+ */
164
+ constructor(options) {
165
+ this.manifest = options.manifest;
166
+ this.seed = new Map(Object.entries(options.staticModules));
167
+ this.loadBundle = options.loadBundle ?? defaultLoadBundle;
168
+ for (const row of options.manifest.modules) {
169
+ if (this.graphRows.has(row.id)) throw new Error(`client-modules: duplicate graph entry "${row.id}"`);
170
+ this.graphRows.set(row.id, row);
171
+ }
172
+ const bootstrapId = stripClientSuffix(options.bootstrapModule.id);
173
+ this.bootstrapIds.add(bootstrapId);
174
+ this.loadCache.set(bootstrapId, {
175
+ id: bootstrapId,
176
+ exports: options.bootstrapModule.exports,
177
+ styles: [],
178
+ edges: /* @__PURE__ */ new Set()
179
+ });
180
+ const target = options.registrationTarget;
181
+ if (target.mode !== "queue") throw new Error("client-modules: window.__ModuleLoader__.create called after module-system boot");
182
+ const pending = target.pendingQueue.splice(0);
183
+ target.mode = "live";
184
+ target.load = (registration) => {
185
+ this.register(registration);
186
+ };
187
+ for (const registration of pending) target.load(registration);
188
+ }
189
+ /** Register one bundle factory, rejecting a script that executes twice without invalidation. */
190
+ register(registration) {
191
+ const id = stripClientSuffix(registration.id);
192
+ if (this.bootstrapIds.has(id) || this.factories.has(id)) throw new Error(`client-modules: duplicate factory registration for "${registration.id}" (bundle executed twice without invalidate?)`);
193
+ this.factories.set(id, registration.factory);
194
+ }
195
+ /** Load one graph row so its factory is registered (idempotent per in-flight arrival). */
196
+ arrive(row) {
197
+ const { id, url } = row;
198
+ const pending = this.pendingArrival.get(id);
199
+ if (pending !== void 0) return pending;
200
+ if (this.loadCache.has(id) || this.factories.has(id)) return Promise.resolve();
201
+ const task = this.loadBundle(url).then(() => {
202
+ if (!this.factories.has(id)) throw new Error(`client-modules: bundle ${url} loaded without registering "${id}" via __ModuleLoader__.load`);
203
+ }).finally(() => {
204
+ this.pendingArrival.delete(id);
205
+ });
206
+ this.pendingArrival.set(id, task);
207
+ return task;
208
+ }
209
+ /** Register each unresolved dynamic request before registering its consumer. */
210
+ async arriveGraphRow(row, open = []) {
211
+ const cycleStart = open.indexOf(row.id);
212
+ if (cycleStart !== -1) throw new Error(`client-modules: module arrival cycle ${[...open.slice(cycleStart), row.id].join(" -> ")} (the host must reject this graph before serving it)`);
213
+ const next = [...open, row.id];
214
+ for (const request of row.external) {
215
+ const id = stripClientSuffix(request);
216
+ if (this.seed.has(request) || this.loadCache.has(id)) continue;
217
+ const dependency = this.graphRows.get(id);
218
+ if (dependency !== void 0) await this.arriveGraphRow(dependency, next);
219
+ }
220
+ await this.arrive(row);
221
+ }
222
+ /** Materialize a registered factory (synchronous; memoized in loadCache). */
223
+ materialize(id) {
224
+ const existing = this.loadCache.get(id);
225
+ if (existing !== void 0) return existing;
226
+ const registered = this.factories.get(id);
227
+ /* v8 ignore next -- callers check the factory branch before dispatching here. */
228
+ if (registered === void 0) throw new Error(`client-modules: no registered factory for "${id}"`);
229
+ if (this.materializing.has(id)) throw new Error(`client-modules: require cycle through "${id}" (factory-form CJS cannot deliver partial exports)`);
230
+ this.materializing.add(id);
231
+ try {
232
+ const edges = /* @__PURE__ */ new Set();
233
+ const record = {
234
+ id,
235
+ exports: registered(this.makeRequire(edges)),
236
+ styles: claimStyles(id),
237
+ edges
238
+ };
239
+ this.loadCache.set(id, record);
240
+ return record;
241
+ } finally {
242
+ this.materializing.delete(id);
243
+ }
244
+ }
245
+ /**
246
+ * The synchronous require answered to factories: seed → memoized record →
247
+ * registered factory. Fetching is async and therefore unreachable
248
+ * from here; an external dynamic package must have arrived before its
249
+ * consumer materializes.
250
+ */
251
+ makeRequire(edges) {
252
+ return (spec) => {
253
+ edges.add(spec);
254
+ if (this.seed.has(spec)) return this.seed.get(spec);
255
+ const id = stripClientSuffix(spec);
256
+ const record = this.loadCache.get(id);
257
+ if (record !== void 0) return record.exports;
258
+ if (this.factories.has(id)) return this.materialize(id).exports;
259
+ 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)`);
260
+ };
261
+ }
262
+ async import(specifier) {
263
+ if (this.seed.has(specifier)) return this.seed.get(specifier);
264
+ const id = stripClientSuffix(specifier);
265
+ const existing = this.loadCache.get(id);
266
+ if (existing !== void 0) return existing.exports;
267
+ const row = this.graphRows.get(id);
268
+ if (row !== void 0) await this.arriveGraphRow(row);
269
+ else if (!this.factories.has(id)) throw new Error(`client-modules: cannot resolve "${specifier}" — not a seed word, not a materialized module, and not a row in the boot graph (the runtime mirror of the bundle purity gate)`);
270
+ return this.materialize(id).exports;
271
+ }
272
+ async prefetch(id) {
273
+ const normalized = stripClientSuffix(id);
274
+ if (this.loadCache.has(normalized)) return;
275
+ const row = this.graphRows.get(normalized);
276
+ if (row === void 0) throw new Error(`client-modules: prefetch("${id}") — not a graph entry`);
277
+ await this.arriveGraphRow(row);
278
+ }
279
+ invalidate(id) {
280
+ const normalized = stripClientSuffix(id);
281
+ if (this.bootstrapIds.has(normalized)) return;
282
+ this.factories.delete(normalized);
283
+ this.loadCache.delete(normalized);
284
+ }
285
+ };
286
+ //#endregion
287
+ //#region lib/types/client/index.js
288
+ let moduleSystem;
289
+ /**
290
+ * Build the live module system from the HTML facade's materialized modules bundle.
291
+ * @param target - Stable registration facade whose pending queue becomes the live sink.
292
+ * @param bootstrapModule - This bundle's id and already-materialized exports.
293
+ * @param options - Raw boot graph, platform seed, and optional bundle transport.
294
+ * @returns The created module system, also published for this package's Cordis plugin face.
295
+ */
296
+ function createClientModuleSystem(target, bootstrapModule, options) {
297
+ moduleSystem = new ClientModuleSystem({
298
+ manifest: parseBootManifest(options.boot),
299
+ staticModules: options.staticModules,
300
+ registrationTarget: target,
301
+ bootstrapModule,
302
+ ...options.loadBundle === void 0 ? {} : { loadBundle: options.loadBundle }
303
+ });
304
+ return moduleSystem;
305
+ }
306
+ /**
307
+ * Enroll the kernel-built module system as `ctx.modules`.
308
+ * @param ctx - client root context.
309
+ */
310
+ function apply(ctx) {
311
+ if (moduleSystem === void 0) throw new Error("client-modules: createClientModuleSystem must run before plugin boot");
312
+ ctx.reflect.provide("modules", moduleSystem);
313
+ }
314
+ //#endregion
315
+ exports.ClientModuleSystem = ClientModuleSystem;
316
+ exports.apply = apply;
317
+ exports.createClientModuleSystem = createClientModuleSystem;
318
+ exports.parseBootManifest = parseBootManifest;
319
+ exports.stripClientSuffix = stripClientSuffix;
320
+ return module.exports;
321
+ }
322
+ });
323
+
324
+ //# sourceMappingURL=client.js.map