@hydraharness/harness-typert-loader 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 +21 -0
- package/README.md +22 -0
- package/lib/index.js +333 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +60 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +55 -0
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,22 @@
|
|
|
1
|
+
# @hydraharness/harness-typert-loader
|
|
2
|
+
|
|
3
|
+
Node-only Loader integration for generated Typert artifacts. The plugin requires `ctx.loader` and `ctx.typert`; it does not provide the registry itself.
|
|
4
|
+
|
|
5
|
+
During activation it scans existing Loader entries. It then follows Cordis `internal/plugin` lifecycle notifications, resolves each entry package's `package.json`, imports `./typert` when exported, validates its `TYPERT` manifest, and registers the contribution until the entry or this plugin unmounts. An import that settles after either owner is gone is discarded.
|
|
6
|
+
|
|
7
|
+
`packages` lists additional package artifacts to register for plugins nested behind another Loader entry. Cordis fibers do not retain those nested plugins' npm specifiers, so this boundary is explicit; every configured package must resolve from the config tree and export `./typert`.
|
|
8
|
+
|
|
9
|
+
Packages without the export are skipped. Package resolution and imported manifests are cached for the process lifetime, so adding an export requires a restart. A malformed artifact fails activation when already mounted; a later failure is logged without preventing unrelated packages from registering.
|
|
10
|
+
|
|
11
|
+
## Model Experience
|
|
12
|
+
|
|
13
|
+
None, as the loader only feeds [`ctx.typert`](../registry/README.md); consumers own any model-visible projection.
|
|
14
|
+
|
|
15
|
+
#### KV Cache effect
|
|
16
|
+
|
|
17
|
+
No direct effect.
|
|
18
|
+
|
|
19
|
+
## Known Limitations and Deferred Work
|
|
20
|
+
|
|
21
|
+
- Discovery imports only the host face; client runtimes need a separate composition owner before equivalent discovery is added.
|
|
22
|
+
- Loader entries are discovered automatically. Nested or non-Loader plugins require an explicit `packages` entry or direct `ctx.typert.register()` ownership.
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import z from "@hydraharness/schemastery";
|
|
6
|
+
//#region lib/types/index.js
|
|
7
|
+
/**
|
|
8
|
+
* Typert Loader integration: automatic registration for mounted plugin packages.
|
|
9
|
+
*
|
|
10
|
+
* When a loader entry mounts, this plugin resolves the entry's package.json; a
|
|
11
|
+
* package exporting `./typert` has its host face imported and its
|
|
12
|
+
* `TYPERT` manifest registered into `ctx.typert`, and the registration is
|
|
13
|
+
* withdrawn when the entry unmounts. Explicit `packages` cover plugins nested
|
|
14
|
+
* behind another Loader entry, whose Cordis fibers carry no resolvable package
|
|
15
|
+
* specifier. Packages without the export are skipped silently when discovered
|
|
16
|
+
* from Loader entries; an explicit package or declared artifact that is broken
|
|
17
|
+
* fails loud — aggregated into this plugin's activation throw for existing
|
|
18
|
+
* entries, contained to a logged error per package in steady state.
|
|
19
|
+
*
|
|
20
|
+
* Scanning is incremental per entry name, mirroring the client-modules node
|
|
21
|
+
* half: every cordis `internal/plugin` emission marks the fiber's entry name
|
|
22
|
+
* dirty and a microtask flush reconciles each dirty name against the live
|
|
23
|
+
* loader entries; the activation pass seeds the same dirty set with all
|
|
24
|
+
* current entries. Package verdicts and imported manifests are cached per
|
|
25
|
+
* package name and never expire — plugin-set changes take effect on restart.
|
|
26
|
+
*
|
|
27
|
+
* Manual `ctx.typert.register()` remains available for contributions
|
|
28
|
+
* that do not use a `./typert` artifact (hand-written wire schemas,
|
|
29
|
+
* tests, non-loader compositions).
|
|
30
|
+
*
|
|
31
|
+
* @module @hydraharness/harness-typert-loader
|
|
32
|
+
*/
|
|
33
|
+
var __rewriteRelativeImportExtension = function(path, preserveJsx) {
|
|
34
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
|
|
35
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
|
|
36
|
+
});
|
|
37
|
+
return path;
|
|
38
|
+
};
|
|
39
|
+
/** The package.json exports key naming a package's host-face typert artifact. */
|
|
40
|
+
const TYPERT_HOST_EXPORT = "./typert";
|
|
41
|
+
/** Cordis plugin name. */
|
|
42
|
+
const name = "typert-loader";
|
|
43
|
+
/** Services required before registration: the registry this plugin feeds and the Loader it observes. */
|
|
44
|
+
const inject = ["typert", "loader"];
|
|
45
|
+
/** Validate explicit package names and default to Loader-entry discovery only. */
|
|
46
|
+
const Config = z.object({ packages: z.array(z.string().min(1)).default([]) });
|
|
47
|
+
const MEMBER_KINDS = new Set([
|
|
48
|
+
"property",
|
|
49
|
+
"method",
|
|
50
|
+
"getter",
|
|
51
|
+
"setter",
|
|
52
|
+
"call",
|
|
53
|
+
"construct",
|
|
54
|
+
"index"
|
|
55
|
+
]);
|
|
56
|
+
/** Resolve the `./typert` export to a relative path, accepting the string and one-level conditional forms. */
|
|
57
|
+
function typertExportOf(pkgName, exportsField) {
|
|
58
|
+
if (typeof exportsField !== "object" || exportsField === null) return void 0;
|
|
59
|
+
const target = exportsField[TYPERT_HOST_EXPORT];
|
|
60
|
+
if (target === void 0) return void 0;
|
|
61
|
+
if (typeof target === "string") return target;
|
|
62
|
+
if (typeof target === "object" && target !== null) {
|
|
63
|
+
const fallback = target.default;
|
|
64
|
+
if (typeof fallback === "string") return fallback;
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`typert-loader: ${pkgName} exports["${TYPERT_HOST_EXPORT}"] must be a string or an object with a string default`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Narrow a dynamically imported typert module's `TYPERT` export to a
|
|
70
|
+
* contribution owned by `pkgName`. This is the module/file boundary: the
|
|
71
|
+
* manifest crosses from a build artifact into the typed registry, so every
|
|
72
|
+
* field is checked and every failure names the package and the defect.
|
|
73
|
+
* @param pkgName - the package whose typert face was imported.
|
|
74
|
+
* @param exported - the module's `TYPERT` export.
|
|
75
|
+
* @returns the validated contribution.
|
|
76
|
+
*/
|
|
77
|
+
function validateTypertManifest(pkgName, exported) {
|
|
78
|
+
if (typeof exported !== "object" || exported === null) throw new Error(`typert-loader: ${pkgName} exports "${TYPERT_HOST_EXPORT}" but its module has no TYPERT manifest object`);
|
|
79
|
+
const manifest = exported;
|
|
80
|
+
if (manifest.package !== pkgName) throw new Error(`typert-loader: ${pkgName} TYPERT manifest names package ${JSON.stringify(manifest.package)} — the manifest must be owned by the package that exports it`);
|
|
81
|
+
if (manifest.face !== "host") throw new Error(`typert-loader: ${pkgName} exports "${TYPERT_HOST_EXPORT}" but TYPERT.face is not "host"`);
|
|
82
|
+
if (!Array.isArray(manifest.schemas)) throw new Error(`typert-loader: ${pkgName} TYPERT.schemas must be an array`);
|
|
83
|
+
for (const value of manifest.schemas) {
|
|
84
|
+
if (typeof value !== "object" || value === null) throw new Error(`typert-loader: ${pkgName} TYPERT.schemas contains a non-object schema`);
|
|
85
|
+
const schema = value;
|
|
86
|
+
requireString(pkgName, schema, "name", "schema");
|
|
87
|
+
if (typeof schema.schema !== "object" || schema.schema === null || !("_zod" in schema.schema)) throw new Error(`typert-loader: ${pkgName} TYPERT schema "${schema.name}" is not a zod v4 schema instance`);
|
|
88
|
+
}
|
|
89
|
+
const model = requireObject(pkgName, manifest.model, "TYPERT.model");
|
|
90
|
+
const services = requireArray(pkgName, model.services, "TYPERT.model.services");
|
|
91
|
+
const events = requireArray(pkgName, model.events, "TYPERT.model.events");
|
|
92
|
+
const objects = requireArray(pkgName, model.objects, "TYPERT.model.objects");
|
|
93
|
+
for (const value of services) {
|
|
94
|
+
const service = requireObject(pkgName, value, "service");
|
|
95
|
+
requireDocumentation(pkgName, service, "service");
|
|
96
|
+
requireString(pkgName, service, "key", "service");
|
|
97
|
+
requireString(pkgName, service, "exportName", "service");
|
|
98
|
+
requireMembers(pkgName, service.members, `service "${service.key}"`);
|
|
99
|
+
requireTypes(pkgName, service.types, `service "${service.key}"`);
|
|
100
|
+
}
|
|
101
|
+
for (const value of events) {
|
|
102
|
+
const event = requireObject(pkgName, value, "event");
|
|
103
|
+
requireDocumentation(pkgName, event, "event");
|
|
104
|
+
requireString(pkgName, event, "name", "event");
|
|
105
|
+
requireString(pkgName, event, "signature", `event "${event.name}"`);
|
|
106
|
+
if (event.mode !== void 0 && typeof event.mode !== "string") throw new Error(`typert-loader: ${pkgName} event "${event.name}" mode must be a string`);
|
|
107
|
+
}
|
|
108
|
+
for (const value of objects) {
|
|
109
|
+
const object = requireObject(pkgName, value, "object");
|
|
110
|
+
requireDocumentation(pkgName, object, "object");
|
|
111
|
+
requireString(pkgName, object, "name", "object");
|
|
112
|
+
requireString(pkgName, object, "exportName", "object");
|
|
113
|
+
requireMembers(pkgName, object.members, `object "${object.name}"`);
|
|
114
|
+
requireTypes(pkgName, object.types, `object "${object.name}"`);
|
|
115
|
+
}
|
|
116
|
+
for (const value of requireArray(pkgName, manifest.invocations, "TYPERT.invocations")) requireInvocation(pkgName, value);
|
|
117
|
+
return manifest;
|
|
118
|
+
}
|
|
119
|
+
function requireObject(pkgName, value, subject) {
|
|
120
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`typert-loader: ${pkgName} ${subject} must be an object`);
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
function requireArray(pkgName, value, subject) {
|
|
124
|
+
if (!Array.isArray(value)) throw new Error(`typert-loader: ${pkgName} ${subject} must be an array`);
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function requireString(pkgName, value, key, subject) {
|
|
128
|
+
if (typeof value[key] !== "string" || value[key].length === 0) throw new Error(`typert-loader: ${pkgName} ${subject} has a missing or empty ${key}`);
|
|
129
|
+
}
|
|
130
|
+
function requireDocumentation(pkgName, value, subject) {
|
|
131
|
+
requireArray(pkgName, value.tags, `${subject}.tags`);
|
|
132
|
+
for (const key of [
|
|
133
|
+
"description",
|
|
134
|
+
"summary",
|
|
135
|
+
"jsDoc"
|
|
136
|
+
]) if (value[key] !== void 0 && typeof value[key] !== "string") throw new Error(`typert-loader: ${pkgName} ${subject}.${key} must be a string`);
|
|
137
|
+
}
|
|
138
|
+
function requireMembers(pkgName, value, subject) {
|
|
139
|
+
for (const item of requireArray(pkgName, value, `${subject}.members`)) {
|
|
140
|
+
const member = requireObject(pkgName, item, `${subject} member`);
|
|
141
|
+
requireString(pkgName, member, "name", `${subject} member`);
|
|
142
|
+
requireString(pkgName, member, "signature", `${subject} member`);
|
|
143
|
+
if (typeof member.kind !== "string" || !MEMBER_KINDS.has(member.kind)) throw new Error(`typert-loader: ${pkgName} ${subject} member "${member.name}" has invalid kind`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function requireTypes(pkgName, value, subject) {
|
|
147
|
+
for (const item of requireArray(pkgName, value, `${subject}.types`)) {
|
|
148
|
+
const type = requireObject(pkgName, item, `${subject} type`);
|
|
149
|
+
requireString(pkgName, type, "name", `${subject} type`);
|
|
150
|
+
requireString(pkgName, type, "declaration", `${subject} type`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function requireInvocation(pkgName, value) {
|
|
154
|
+
const invocation = requireObject(pkgName, value, "invocation");
|
|
155
|
+
for (const key of [
|
|
156
|
+
"id",
|
|
157
|
+
"service",
|
|
158
|
+
"namespace",
|
|
159
|
+
"method"
|
|
160
|
+
]) requireString(pkgName, invocation, key, "invocation");
|
|
161
|
+
const id = invocation.id;
|
|
162
|
+
const receiver = requireObject(pkgName, invocation.invocation, `invocation "${id}" receiver`);
|
|
163
|
+
if (receiver.kind === "context") {
|
|
164
|
+
requireString(pkgName, receiver, "context", `invocation "${id}" Context receiver`);
|
|
165
|
+
requireString(pkgName, receiver, "wire", `invocation "${id}" Context receiver`);
|
|
166
|
+
requireStrictCodec(pkgName, receiver.codec, `invocation "${id}" Context codec`);
|
|
167
|
+
} else if (receiver.kind !== "direct") throw new Error(`typert-loader: ${pkgName} invocation "${id}" receiver kind must be "direct" or "context"`);
|
|
168
|
+
const wires = /* @__PURE__ */ new Set();
|
|
169
|
+
const parameters = /* @__PURE__ */ new Map();
|
|
170
|
+
let lookupCount = 0;
|
|
171
|
+
for (const valueParameter of requireArray(pkgName, invocation.parameters, `invocation "${id}" parameters`)) {
|
|
172
|
+
const parameter = requireObject(pkgName, valueParameter, `invocation "${id}" parameter`);
|
|
173
|
+
requireString(pkgName, parameter, "name", `invocation "${id}" parameter`);
|
|
174
|
+
requireString(pkgName, parameter, "wire", `invocation "${id}" parameter`);
|
|
175
|
+
const wire = parameter.wire;
|
|
176
|
+
if (wires.has(wire)) throw new Error(`typert-loader: ${pkgName} invocation "${id}" repeats wire field "${wire}"`);
|
|
177
|
+
wires.add(wire);
|
|
178
|
+
if (parameter.source === "lookup") {
|
|
179
|
+
lookupCount += 1;
|
|
180
|
+
requireString(pkgName, parameter, "lookup", `invocation "${id}" lookup parameter`);
|
|
181
|
+
} else if (parameter.source === "json") {
|
|
182
|
+
if (parameter.lookup !== void 0) throw new Error(`typert-loader: ${pkgName} invocation "${id}" JSON parameter declares a lookup`);
|
|
183
|
+
} else throw new Error(`typert-loader: ${pkgName} invocation "${id}" parameter source must be "json" or "lookup"`);
|
|
184
|
+
parameters.set(wire, parameter);
|
|
185
|
+
requireStrictCodec(pkgName, parameter.codec, `invocation "${id}" parameter codec`);
|
|
186
|
+
}
|
|
187
|
+
if (invocation.cancellation !== void 0) {
|
|
188
|
+
if (requireObject(pkgName, invocation.cancellation, `invocation "${id}" cancellation`).parameter !== "signal") throw new Error(`typert-loader: ${pkgName} invocation "${id}" cancellation parameter must be "signal"`);
|
|
189
|
+
}
|
|
190
|
+
if (invocation.scope !== void 0) {
|
|
191
|
+
if (receiver.kind !== "direct") throw new Error(`typert-loader: ${pkgName} invocation "${id}" Context receiver cannot declare a direct scope projection`);
|
|
192
|
+
const scope = requireObject(pkgName, invocation.scope, `invocation "${id}" scope`);
|
|
193
|
+
requireString(pkgName, scope, "context", `invocation "${id}" scope`);
|
|
194
|
+
requireString(pkgName, scope, "wire", `invocation "${id}" scope`);
|
|
195
|
+
const parameter = parameters.get(scope.wire);
|
|
196
|
+
if (lookupCount !== 1 || parameter?.source !== "lookup" || parameter.lookup !== scope.context) throw new Error(`typert-loader: ${pkgName} invocation "${id}" scope wire "${scope.wire}" must select its only lookup parameter`);
|
|
197
|
+
}
|
|
198
|
+
if (receiver.kind === "context" && wires.has(receiver.wire)) throw new Error(`typert-loader: ${pkgName} invocation "${id}" repeats Context wire field "${receiver.wire}"`);
|
|
199
|
+
requireStrictCodec(pkgName, invocation.result, `invocation "${id}" result codec`);
|
|
200
|
+
if (invocation.sourceLocation !== void 0) {
|
|
201
|
+
const location = requireObject(pkgName, invocation.sourceLocation, `invocation "${id}" sourceLocation`);
|
|
202
|
+
requireString(pkgName, location, "file", `invocation "${id}" sourceLocation`);
|
|
203
|
+
for (const key of ["line", "column"]) if (!Number.isInteger(location[key]) || location[key] < 1) throw new Error(`typert-loader: ${pkgName} invocation "${id}" sourceLocation.${key} must be a positive integer`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function requireStrictCodec(pkgName, value, subject) {
|
|
207
|
+
const codec = requireObject(pkgName, value, subject);
|
|
208
|
+
if (codec.mode !== "strict") throw new Error(`typert-loader: ${pkgName} ${subject} must use a strict codec`);
|
|
209
|
+
requireString(pkgName, codec, "typeSymbol", subject);
|
|
210
|
+
if (typeof codec.schema !== "object" || codec.schema === null || !("_zod" in codec.schema) || typeof codec.schema.parse !== "function") throw new Error(`typert-loader: ${pkgName} ${subject} is not backed by a zod v4 schema`);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Scan current Loader entries during activation, then follow entry mounts and
|
|
214
|
+
* unmounts for this plugin's lifetime.
|
|
215
|
+
* @param ctx - plugin context carrying `typert` and `loader`.
|
|
216
|
+
* @param config - explicit package artifacts in addition to Loader entries.
|
|
217
|
+
*/
|
|
218
|
+
async function apply(ctx, config) {
|
|
219
|
+
if (ctx.baseUrl === void 0) throw new Error("typert-loader: ctx.baseUrl is unset — the loader needs the config-tree anchor to resolve plugin packages");
|
|
220
|
+
const require = createRequire(ctx.baseUrl);
|
|
221
|
+
const configured = new Set(config.packages);
|
|
222
|
+
const registered = /* @__PURE__ */ new Map();
|
|
223
|
+
const pending = /* @__PURE__ */ new Map();
|
|
224
|
+
const artifactPath = /* @__PURE__ */ new Map();
|
|
225
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
226
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
227
|
+
let flushQueued = false;
|
|
228
|
+
let active = true;
|
|
229
|
+
ctx.effect(function* () {
|
|
230
|
+
yield () => {
|
|
231
|
+
active = false;
|
|
232
|
+
dirty.clear();
|
|
233
|
+
};
|
|
234
|
+
}, "typert loader lifetime");
|
|
235
|
+
const resolveArtifact = (pkgName) => {
|
|
236
|
+
const cached = artifactPath.get(pkgName);
|
|
237
|
+
if (cached !== void 0) return cached;
|
|
238
|
+
let pkgPath;
|
|
239
|
+
try {
|
|
240
|
+
pkgPath = require.resolve(`${pkgName}/package.json`);
|
|
241
|
+
} catch (cause) {
|
|
242
|
+
if (configured.has(pkgName)) throw new Error(`typert-loader: configured package "${pkgName}" cannot be resolved from the config tree — add it to the composition package dependencies or remove it from packages`, { cause });
|
|
243
|
+
artifactPath.set(pkgName, null);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const rel = typertExportOf(pkgName, JSON.parse(readFileSync(pkgPath, "utf8")).exports);
|
|
247
|
+
if (rel === void 0 && configured.has(pkgName)) throw new Error(`typert-loader: configured package "${pkgName}" does not export "${TYPERT_HOST_EXPORT}"`);
|
|
248
|
+
const resolved = rel === void 0 ? null : join(dirname(pkgPath), rel);
|
|
249
|
+
artifactPath.set(pkgName, resolved);
|
|
250
|
+
return resolved;
|
|
251
|
+
};
|
|
252
|
+
const loadManifest = (pkgName, path) => {
|
|
253
|
+
let loading = manifests.get(pkgName);
|
|
254
|
+
if (loading === void 0) {
|
|
255
|
+
loading = import(__rewriteRelativeImportExtension(pathToFileURL(path).href)).then((mod) => validateTypertManifest(pkgName, mod.TYPERT), (cause) => {
|
|
256
|
+
throw new Error(`typert-loader: ${pkgName} exports "${TYPERT_HOST_EXPORT}" but importing ${path} failed: ${String(cause)}`);
|
|
257
|
+
});
|
|
258
|
+
manifests.set(pkgName, loading);
|
|
259
|
+
}
|
|
260
|
+
return loading;
|
|
261
|
+
};
|
|
262
|
+
const qualifies = (entryName) => {
|
|
263
|
+
if (configured.has(entryName)) return true;
|
|
264
|
+
for (const entry of ctx.loader.entries()) if (entry.options.name === entryName && entry.fiber !== void 0 && !entry.disabled) return true;
|
|
265
|
+
return false;
|
|
266
|
+
};
|
|
267
|
+
/** Reconcile one entry name against the live loader entries; a mount returns its async task. */
|
|
268
|
+
const processOne = (entryName) => {
|
|
269
|
+
if (!qualifies(entryName)) {
|
|
270
|
+
const dispose = registered.get(entryName);
|
|
271
|
+
if (dispose !== void 0) {
|
|
272
|
+
registered.delete(entryName);
|
|
273
|
+
return dispose();
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (registered.has(entryName) || pending.has(entryName)) return void 0;
|
|
278
|
+
const path = resolveArtifact(entryName);
|
|
279
|
+
if (path === null) return void 0;
|
|
280
|
+
const task = loadManifest(entryName, path).then((manifest) => {
|
|
281
|
+
if (!active || !qualifies(entryName) || registered.has(entryName)) return;
|
|
282
|
+
registered.set(entryName, ctx.typert.register(manifest));
|
|
283
|
+
});
|
|
284
|
+
pending.set(entryName, task);
|
|
285
|
+
const settle = () => {
|
|
286
|
+
pending.delete(entryName);
|
|
287
|
+
};
|
|
288
|
+
task.then(settle, settle);
|
|
289
|
+
return task;
|
|
290
|
+
};
|
|
291
|
+
const flush = (onError) => {
|
|
292
|
+
const tasks = [];
|
|
293
|
+
for (const entryName of [...dirty]) {
|
|
294
|
+
dirty.delete(entryName);
|
|
295
|
+
try {
|
|
296
|
+
const task = processOne(entryName);
|
|
297
|
+
if (task !== void 0) tasks.push(task.catch((error) => {
|
|
298
|
+
onError(toError(error));
|
|
299
|
+
}));
|
|
300
|
+
} catch (error) {
|
|
301
|
+
onError(toError(error));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return tasks;
|
|
305
|
+
};
|
|
306
|
+
ctx.on("internal/plugin", (fiber) => {
|
|
307
|
+
const entryName = fiber.entry?.options.name;
|
|
308
|
+
if (entryName === void 0) return;
|
|
309
|
+
dirty.add(entryName);
|
|
310
|
+
if (flushQueued) return;
|
|
311
|
+
flushQueued = true;
|
|
312
|
+
queueMicrotask(() => {
|
|
313
|
+
flushQueued = false;
|
|
314
|
+
if (!active) return;
|
|
315
|
+
for (const task of flush((err) => {
|
|
316
|
+
ctx.logger.error(err);
|
|
317
|
+
}));
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
for (const packageName of configured) dirty.add(packageName);
|
|
321
|
+
for (const entry of ctx.loader.entries()) dirty.add(entry.options.name);
|
|
322
|
+
const failures = [];
|
|
323
|
+
await Promise.all(flush((err) => {
|
|
324
|
+
failures.push(err);
|
|
325
|
+
}));
|
|
326
|
+
if (failures.length > 0) throw new AggregateError(failures, `typert-loader: ${String(failures.length)} typert contributor(s) failed to register:\n${failures.map((e) => ` - ${e.message}`).join("\n")}`);
|
|
327
|
+
}
|
|
328
|
+
/** Normalize an arbitrary import or manifest failure to an Error. */
|
|
329
|
+
function toError(error) {
|
|
330
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
export { Config, TYPERT_HOST_EXPORT, apply, inject, name, validateTypertManifest };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@hydraharness/harness-typert-loader`.
|
|
4
|
+
* @module @hydraharness/harness-typert-loader/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@hydraharness/harness-typert-loader";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "typert-loader-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: the Loader entry lifecycle directly owns each exact
|
|
13
|
+
* registry disposer, and integration tests observe registration and removal.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typert Loader integration: automatic registration for mounted plugin packages.
|
|
3
|
+
*
|
|
4
|
+
* When a loader entry mounts, this plugin resolves the entry's package.json; a
|
|
5
|
+
* package exporting `./typert` has its host face imported and its
|
|
6
|
+
* `TYPERT` manifest registered into `ctx.typert`, and the registration is
|
|
7
|
+
* withdrawn when the entry unmounts. Explicit `packages` cover plugins nested
|
|
8
|
+
* behind another Loader entry, whose Cordis fibers carry no resolvable package
|
|
9
|
+
* specifier. Packages without the export are skipped silently when discovered
|
|
10
|
+
* from Loader entries; an explicit package or declared artifact that is broken
|
|
11
|
+
* fails loud — aggregated into this plugin's activation throw for existing
|
|
12
|
+
* entries, contained to a logged error per package in steady state.
|
|
13
|
+
*
|
|
14
|
+
* Scanning is incremental per entry name, mirroring the client-modules node
|
|
15
|
+
* half: every cordis `internal/plugin` emission marks the fiber's entry name
|
|
16
|
+
* dirty and a microtask flush reconciles each dirty name against the live
|
|
17
|
+
* loader entries; the activation pass seeds the same dirty set with all
|
|
18
|
+
* current entries. Package verdicts and imported manifests are cached per
|
|
19
|
+
* package name and never expire — plugin-set changes take effect on restart.
|
|
20
|
+
*
|
|
21
|
+
* Manual `ctx.typert.register()` remains available for contributions
|
|
22
|
+
* that do not use a `./typert` artifact (hand-written wire schemas,
|
|
23
|
+
* tests, non-loader compositions).
|
|
24
|
+
*
|
|
25
|
+
* @module @hydraharness/harness-typert-loader
|
|
26
|
+
*/
|
|
27
|
+
import type { Context } from '@hydraharness/cordis';
|
|
28
|
+
import z from '@hydraharness/schemastery';
|
|
29
|
+
import type { TypertContribution } from '@hydraharness/harness-typert-registry/types';
|
|
30
|
+
/** The package.json exports key naming a package's host-face typert artifact. */
|
|
31
|
+
export declare const TYPERT_HOST_EXPORT = "./typert";
|
|
32
|
+
/** Cordis plugin name. */
|
|
33
|
+
export declare const name = "typert-loader";
|
|
34
|
+
/** Services required before registration: the registry this plugin feeds and the Loader it observes. */
|
|
35
|
+
export declare const inject: string[];
|
|
36
|
+
/** Additional package artifacts whose owning plugins are nested behind another Loader entry. */
|
|
37
|
+
export interface Config {
|
|
38
|
+
/** Exact npm package names that must resolve and export `./typert`. */
|
|
39
|
+
packages?: string[];
|
|
40
|
+
}
|
|
41
|
+
/** Validate explicit package names and default to Loader-entry discovery only. */
|
|
42
|
+
export declare const Config: z<Config>;
|
|
43
|
+
/**
|
|
44
|
+
* Narrow a dynamically imported typert module's `TYPERT` export to a
|
|
45
|
+
* contribution owned by `pkgName`. This is the module/file boundary: the
|
|
46
|
+
* manifest crosses from a build artifact into the typed registry, so every
|
|
47
|
+
* field is checked and every failure names the package and the defect.
|
|
48
|
+
* @param pkgName - the package whose typert face was imported.
|
|
49
|
+
* @param exported - the module's `TYPERT` export.
|
|
50
|
+
* @returns the validated contribution.
|
|
51
|
+
*/
|
|
52
|
+
export declare function validateTypertManifest(pkgName: string, exported: unknown): TypertContribution;
|
|
53
|
+
/**
|
|
54
|
+
* Scan current Loader entries during activation, then follow entry mounts and
|
|
55
|
+
* unmounts for this plugin's lifetime.
|
|
56
|
+
* @param ctx - plugin context carrying `typert` and `loader`.
|
|
57
|
+
* @param config - explicit package artifacts in addition to Loader entries.
|
|
58
|
+
*/
|
|
59
|
+
export declare function apply(ctx: Context, config: Config): Promise<void>;
|
|
60
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@hydraharness/harness-typert-loader`.
|
|
3
|
+
* @module @hydraharness/harness-typert-loader/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@hydraharness/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "typert-loader-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydraharness/harness-typert-loader",
|
|
3
|
+
"description": "Loader integration for generated Typert package contributions",
|
|
4
|
+
"hydra": {
|
|
5
|
+
"plugin": {
|
|
6
|
+
"application": "Load the runtime API definitions published by configured plugin packages."
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.1-rc.6",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
|
|
16
|
+
"directory": "packages/typert/loader"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "lib/index.js",
|
|
20
|
+
"types": "lib/types/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./lib/types/index.d.ts",
|
|
24
|
+
"default": "./lib/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./invariant": {
|
|
27
|
+
"types": "./lib/types/invariant.d.ts",
|
|
28
|
+
"default": "./lib/invariant.js"
|
|
29
|
+
},
|
|
30
|
+
"./src/*": "./src/*",
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib/index.js",
|
|
35
|
+
"lib/invariant.js",
|
|
36
|
+
"lib/types/**/*.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@hydraharness/cordis-plugin-loader": "^1.0.3",
|
|
41
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.6",
|
|
42
|
+
"@hydraharness/harness-typert-registry": "^0.1.1-rc.6",
|
|
43
|
+
"@hydraharness/cordis": "^4.0.2"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@hydraharness/schemastery": "^3.18.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"zod": "^4.4.3",
|
|
50
|
+
"@hydraharness/cordis-plugin-loader": "^1.0.3",
|
|
51
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.6",
|
|
52
|
+
"@hydraharness/harness-typert-registry": "^0.1.1-rc.6",
|
|
53
|
+
"@hydraharness/cordis": "^4.0.2"
|
|
54
|
+
}
|
|
55
|
+
}
|