@zhin.js/runtime 1.0.2 → 1.0.3
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/config-composer.js +8 -0
- package/lib/config-patch-planner.js +42 -6
- package/lib/feature-projector.d.ts +4 -4
- package/lib/feature-projector.js +10 -9
- package/lib/generation-assets.d.ts +4 -4
- package/lib/generation-assets.js +29 -7
- package/lib/hmr-coordinator.d.ts +2 -0
- package/lib/hmr-coordinator.js +5 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/manifest.d.ts +6 -0
- package/lib/manifest.js +9 -1
- package/lib/native-development-runtime.js +26 -2
- package/lib/package-resolver.d.ts +4 -1
- package/lib/package-resolver.js +31 -12
- package/lib/platform-features.d.ts +25 -0
- package/lib/platform-features.js +38 -0
- package/lib/plugin-scope-assembler.d.ts +3 -1
- package/lib/plugin-scope-assembler.js +31 -3
- package/lib/project-graph.d.ts +4 -3
- package/lib/project-graph.js +92 -19
- package/lib/root-runtime.js +77 -33
- package/lib/setup-capabilities.d.ts +5 -0
- package/lib/setup-capabilities.js +44 -0
- package/lib/slot-generation-preparer.js +12 -4
- package/lib/source-ownership.js +2 -0
- package/lib/subtree-generation-preparer.js +9 -4
- package/lib/topology-generation-preparer.js +9 -4
- package/package.json +13 -13
package/lib/project-graph.d.ts
CHANGED
|
@@ -18,13 +18,14 @@ export interface ProjectGraph {
|
|
|
18
18
|
readonly packages: ReadonlyMap<string, ResolvedPackage>;
|
|
19
19
|
readonly buildOrder: readonly ResolvedPackage[];
|
|
20
20
|
}
|
|
21
|
+
export interface ProjectGraphServiceOptions {
|
|
22
|
+
readonly engineVersion?: string;
|
|
23
|
+
}
|
|
21
24
|
export declare class ProjectGraphError extends Error {
|
|
22
25
|
constructor(message: string);
|
|
23
26
|
}
|
|
24
27
|
export declare class ProjectGraphService {
|
|
25
28
|
#private;
|
|
26
|
-
|
|
27
|
-
private readonly engineVersion;
|
|
28
|
-
constructor(resolver: PackageResolver, engineVersion?: string);
|
|
29
|
+
constructor(resolver: PackageResolver, engineVersionOrOptions?: string | ProjectGraphServiceOptions);
|
|
29
30
|
inspect(projectRoot: string): Promise<ProjectGraph>;
|
|
30
31
|
}
|
package/lib/project-graph.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { childPluginId, rootPluginId } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import { assertFeatureApi, assertPackageEngine, runtimeEngineVersion, } from './compatibility.js';
|
|
3
3
|
import { PackageResolutionError, } from './package-resolver.js';
|
|
4
|
+
import { PLATFORM_FEATURE_CARRIER, PLATFORM_FEATURE_FACADE, declaredPackageDependency, mergeChildPluginReferences, mergeFeatureReferences, } from './platform-features.js';
|
|
4
5
|
export class ProjectGraphError extends Error {
|
|
5
6
|
constructor(message) {
|
|
6
7
|
super(message);
|
|
@@ -8,18 +9,20 @@ export class ProjectGraphError extends Error {
|
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
export class ProjectGraphService {
|
|
11
|
-
resolver;
|
|
12
|
-
engineVersion;
|
|
13
|
-
constructor(resolver,
|
|
14
|
-
this
|
|
15
|
-
this
|
|
12
|
+
#resolver;
|
|
13
|
+
#engineVersion;
|
|
14
|
+
constructor(resolver, engineVersionOrOptions = runtimeEngineVersion) {
|
|
15
|
+
this.#resolver = resolver;
|
|
16
|
+
this.#engineVersion = typeof engineVersionOrOptions === 'string'
|
|
17
|
+
? engineVersionOrOptions
|
|
18
|
+
: (engineVersionOrOptions.engineVersion ?? runtimeEngineVersion);
|
|
16
19
|
}
|
|
17
20
|
async inspect(projectRoot) {
|
|
18
|
-
const rootPackage = await this
|
|
21
|
+
const rootPackage = await this.#resolver.root(projectRoot);
|
|
19
22
|
assertPackageType(rootPackage, 'plugin');
|
|
20
23
|
const packages = new Map();
|
|
21
|
-
const root = await this.#visitPlugin(rootPackage, rootPluginId(), 'root', undefined, [], packages);
|
|
22
|
-
for (const pkg of this
|
|
24
|
+
const root = await this.#visitPlugin(rootPackage, rootPluginId(), 'root', undefined, [], packages, true);
|
|
25
|
+
for (const pkg of this.#resolver.workspacePackages())
|
|
23
26
|
addPackage(packages, pkg);
|
|
24
27
|
return Object.freeze({
|
|
25
28
|
root,
|
|
@@ -27,39 +30,62 @@ export class ProjectGraphService {
|
|
|
27
30
|
buildOrder: topologicalBuildOrder(packages),
|
|
28
31
|
});
|
|
29
32
|
}
|
|
30
|
-
async #visitPlugin(pkg, id, instanceKey, parent, ancestors, packages) {
|
|
33
|
+
async #visitPlugin(pkg, id, instanceKey, parent, ancestors, packages, isRoot) {
|
|
31
34
|
if (ancestors.includes(pkg.root)) {
|
|
32
35
|
throw new ProjectGraphError(`Plugin cycle detected: ${[...ancestors, pkg.root].join(' -> ')}`);
|
|
33
36
|
}
|
|
34
37
|
const manifest = assertPackageType(pkg, 'plugin');
|
|
35
|
-
assertPackageEngine(pkg, this
|
|
38
|
+
assertPackageEngine(pkg, this.#engineVersion);
|
|
36
39
|
addPackage(packages, pkg);
|
|
40
|
+
let featureCarrier;
|
|
41
|
+
let pluginFacade;
|
|
42
|
+
let featureRefs = manifest.features;
|
|
43
|
+
let pluginRefs = manifest.plugins;
|
|
44
|
+
if (isRoot && manifest.platformFeatures !== false) {
|
|
45
|
+
const platform = await tryResolvePlatform(this.#resolver, pkg);
|
|
46
|
+
featureCarrier = platform.featureCarrier;
|
|
47
|
+
pluginFacade = platform.pluginFacade;
|
|
48
|
+
if (featureCarrier) {
|
|
49
|
+
addPackage(packages, featureCarrier);
|
|
50
|
+
const carrierManifest = featureCarrier.packageJson.zhin;
|
|
51
|
+
const inherited = carrierManifest.type === 'plugin' ? carrierManifest.features : [];
|
|
52
|
+
featureRefs = mergeFeatureReferences(manifest.features, inherited);
|
|
53
|
+
}
|
|
54
|
+
if (pluginFacade) {
|
|
55
|
+
addPackage(packages, pluginFacade);
|
|
56
|
+
const facadeManifest = pluginFacade.packageJson.zhin;
|
|
57
|
+
const inheritedPlugins = facadeManifest.type === 'plugin' ? facadeManifest.plugins : [];
|
|
58
|
+
pluginRefs = mergeChildPluginReferences(manifest.plugins, inheritedPlugins);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
37
61
|
const featurePackages = new Set();
|
|
38
|
-
const features = await Promise.all(
|
|
62
|
+
const features = await Promise.all(featureRefs.map(async (reference) => {
|
|
39
63
|
if (featurePackages.has(reference.package)) {
|
|
40
64
|
throw new ProjectGraphError(`Duplicate Feature requirement ${reference.package} in ${pkg.name}`);
|
|
41
65
|
}
|
|
42
66
|
featurePackages.add(reference.package);
|
|
43
|
-
const resolved = await
|
|
67
|
+
const resolved = await this.#resolveFeatureReference(pkg, reference, featureCarrier);
|
|
44
68
|
if (!resolved)
|
|
45
69
|
return undefined;
|
|
46
70
|
assertPackageType(resolved, 'feature');
|
|
47
|
-
assertPackageEngine(resolved, this
|
|
71
|
+
assertPackageEngine(resolved, this.#engineVersion);
|
|
48
72
|
assertFeatureApi(pkg, reference, resolved);
|
|
49
73
|
addPackage(packages, resolved);
|
|
50
74
|
return Object.freeze({ reference, package: resolved });
|
|
51
75
|
}));
|
|
76
|
+
const ownPluginKeys = new Set(manifest.plugins.map((item) => item.instanceKey));
|
|
52
77
|
const instanceKeys = new Set();
|
|
53
|
-
const children = await Promise.all(
|
|
78
|
+
const children = await Promise.all(pluginRefs.map(async (reference) => {
|
|
54
79
|
if (instanceKeys.has(reference.instanceKey)) {
|
|
55
80
|
throw new ProjectGraphError(`Duplicate child instanceKey ${reference.instanceKey} in ${pkg.name}`);
|
|
56
81
|
}
|
|
57
82
|
instanceKeys.add(reference.instanceKey);
|
|
58
|
-
const
|
|
83
|
+
const resolveFrom = ownPluginKeys.has(reference.instanceKey) ? pkg : (pluginFacade ?? pkg);
|
|
84
|
+
const resolved = await resolveReference(this.#resolver, resolveFrom, reference);
|
|
59
85
|
if (!resolved)
|
|
60
86
|
return undefined;
|
|
61
87
|
assertPackageType(resolved, 'plugin');
|
|
62
|
-
return this.#visitPlugin(resolved, childPluginId(id, reference.instanceKey), reference.instanceKey, id, [...ancestors, pkg.root], packages);
|
|
88
|
+
return this.#visitPlugin(resolved, childPluginId(id, reference.instanceKey), reference.instanceKey, id, [...ancestors, pkg.root], packages, false);
|
|
63
89
|
}));
|
|
64
90
|
return Object.freeze({
|
|
65
91
|
id,
|
|
@@ -70,15 +96,62 @@ export class ProjectGraphService {
|
|
|
70
96
|
children: Object.freeze(children.filter(isDefined)),
|
|
71
97
|
});
|
|
72
98
|
}
|
|
99
|
+
async #resolveFeatureReference(pkg, reference, featureCarrier) {
|
|
100
|
+
try {
|
|
101
|
+
return await this.#resolver.resolve(reference.package, pkg);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// 非解析类错误(损坏的 package.json 等)不回退、不容错,直接上抛。
|
|
105
|
+
if (!(error instanceof PackageResolutionError))
|
|
106
|
+
throw error;
|
|
107
|
+
// carrier 回退:继承自 @zhin.js/core 的 Feature 引用声明在 carrier 上,
|
|
108
|
+
// 从引用包解析失败(未声明 / 未安装 / workspace 链接缺失)时改从 carrier 解析。
|
|
109
|
+
if (featureCarrier)
|
|
110
|
+
return resolveReference(this.#resolver, featureCarrier, reference);
|
|
111
|
+
// 无 carrier 时按统一规则处理:optional 引用容错,其余上抛。
|
|
112
|
+
if (reference.optional)
|
|
113
|
+
return undefined;
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function tryResolvePlatform(resolver, from) {
|
|
119
|
+
let featureCarrier;
|
|
120
|
+
let pluginFacade;
|
|
121
|
+
const directCore = declaredPackageDependency(from.packageJson.dependencies, from.packageJson.optionalDependencies, PLATFORM_FEATURE_CARRIER);
|
|
122
|
+
if (directCore) {
|
|
123
|
+
try {
|
|
124
|
+
featureCarrier = await resolver.resolve(PLATFORM_FEATURE_CARRIER, from);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (!(error instanceof PackageResolutionError))
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const facadeDecl = declaredPackageDependency(from.packageJson.dependencies, from.packageJson.optionalDependencies, PLATFORM_FEATURE_FACADE);
|
|
132
|
+
if (facadeDecl) {
|
|
133
|
+
try {
|
|
134
|
+
pluginFacade = await resolver.resolve(PLATFORM_FEATURE_FACADE, from);
|
|
135
|
+
if (!featureCarrier) {
|
|
136
|
+
featureCarrier = await resolver.resolve(PLATFORM_FEATURE_CARRIER, pluginFacade);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!(error instanceof PackageResolutionError))
|
|
141
|
+
throw error;
|
|
142
|
+
pluginFacade = undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { featureCarrier, pluginFacade };
|
|
73
146
|
}
|
|
74
147
|
async function resolveReference(resolver, from, reference) {
|
|
75
148
|
try {
|
|
76
149
|
return await resolver.resolve(reference.package, from);
|
|
77
150
|
}
|
|
78
151
|
catch (error) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
152
|
+
// optional 引用对所有 PackageResolutionError 统一容错(未声明 / 未安装 /
|
|
153
|
+
// workspace 链接缺失);非解析类错误(损坏的 package.json 等)继续上抛。
|
|
154
|
+
if (reference.optional && error instanceof PackageResolutionError)
|
|
82
155
|
return undefined;
|
|
83
156
|
throw error;
|
|
84
157
|
}
|
package/lib/root-runtime.js
CHANGED
|
@@ -12,11 +12,12 @@ import { GenerationAssets } from './generation-assets.js';
|
|
|
12
12
|
import { NodeDiscoveryHost } from './node-discovery-host.js';
|
|
13
13
|
import { NodePackageResolver } from './package-resolver.js';
|
|
14
14
|
import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
|
|
15
|
-
import { ProjectGraphService } from './project-graph.js';
|
|
15
|
+
import { ProjectGraphService, } from './project-graph.js';
|
|
16
16
|
import { HmrCoordinator } from './hmr-coordinator.js';
|
|
17
17
|
import { RootProcessRestartExecutor, } from './process-restart.js';
|
|
18
18
|
import { SlotGenerationPreparer } from './slot-generation-preparer.js';
|
|
19
19
|
import { SourceOwnershipIndex } from './source-ownership.js';
|
|
20
|
+
import { addCapabilitySlot, featureSetupAliases, mergeSetupCapabilities, } from './setup-capabilities.js';
|
|
20
21
|
import { SubtreeGenerationPreparer, SubtreeTopologyChangedError, } from './subtree-generation-preparer.js';
|
|
21
22
|
import { TopologyGenerationPreparer } from './topology-generation-preparer.js';
|
|
22
23
|
import { RestartBoundaryPlanner } from './restart-boundary.js';
|
|
@@ -168,6 +169,7 @@ export class RootRuntime {
|
|
|
168
169
|
async #inspectProject() {
|
|
169
170
|
const resolver = await NodePackageResolver.create(this.#projectRoot);
|
|
170
171
|
const graph = await new ProjectGraphService(resolver).inspect(this.#projectRoot);
|
|
172
|
+
await this.#refreshConfigDocument();
|
|
171
173
|
if (this.#configResolver) {
|
|
172
174
|
return {
|
|
173
175
|
graph,
|
|
@@ -182,6 +184,22 @@ export class RootRuntime {
|
|
|
182
184
|
primaryConfigDocument: composed.document,
|
|
183
185
|
};
|
|
184
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* The config file itself is watched, so an external edit triggers a full
|
|
189
|
+
* reload. Re-read through the port before composing: without this the reload
|
|
190
|
+
* would rebuild the whole generation from the stale in-memory document read
|
|
191
|
+
* at start, the edit would silently not apply, and the next patchConfig
|
|
192
|
+
* would hit a revision conflict. On drift the file is authoritative.
|
|
193
|
+
*/
|
|
194
|
+
async #refreshConfigDocument() {
|
|
195
|
+
if (!this.#configPort)
|
|
196
|
+
return;
|
|
197
|
+
const snapshot = await this.#configPort.read();
|
|
198
|
+
if (snapshot.revision === this.#configSnapshot?.revision)
|
|
199
|
+
return;
|
|
200
|
+
this.#configSnapshot = snapshot;
|
|
201
|
+
this.#configDocument = structuredClone(snapshot.document);
|
|
202
|
+
}
|
|
185
203
|
#configViewResolver(views) {
|
|
186
204
|
const env = createEnvStore(rootPluginId(), this.#environment, this.#environmentLayers);
|
|
187
205
|
return (node) => {
|
|
@@ -195,7 +213,10 @@ export class RootRuntime {
|
|
|
195
213
|
if (!this.#configDocument) {
|
|
196
214
|
throw new Error('Config patches require a document-backed RootRuntime config');
|
|
197
215
|
}
|
|
198
|
-
|
|
216
|
+
// Adopt any external edit before planning so the port's revision check
|
|
217
|
+
// cannot conflict and the patch applies on top of the on-disk document.
|
|
218
|
+
await this.#refreshConfigDocument();
|
|
219
|
+
const currentDocument = requireConfigDocument(this.#configDocument);
|
|
199
220
|
let plan;
|
|
200
221
|
let prepared;
|
|
201
222
|
let documentTransaction;
|
|
@@ -207,37 +228,51 @@ export class RootRuntime {
|
|
|
207
228
|
plan = planned;
|
|
208
229
|
if (!planned.documentChanged)
|
|
209
230
|
return undefined;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
231
|
+
try {
|
|
232
|
+
if (this.#configPort) {
|
|
233
|
+
const currentSnapshot = requireConfigDocumentSnapshot(this.#configSnapshot);
|
|
234
|
+
// Port preparation must remain inert; validation and shadow setup can
|
|
235
|
+
// still reject this candidate without touching the backing document.
|
|
236
|
+
documentTransaction = await this.#configPort.prepare(currentSnapshot, patches);
|
|
237
|
+
if (!isDeepStrictEqual(documentTransaction.document, planned.candidate)) {
|
|
238
|
+
throw new ConfigDocumentDivergenceError();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Host-level sections (http/database/ai/...) never land in a Plugin
|
|
242
|
+
// view, so their patches plan zero roots. With a Root Resource
|
|
243
|
+
// installer the whole generation must still be rebuilt; only an
|
|
244
|
+
// installer-less runtime may take the commit-only shortcut.
|
|
245
|
+
if (!this.#installResources && planned.roots.length === 0) {
|
|
246
|
+
if (documentTransaction)
|
|
247
|
+
committedDocument = await documentTransaction.commit();
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const inspected = {
|
|
251
|
+
graph,
|
|
252
|
+
configResolver: this.#configViewResolver(planned.views),
|
|
253
|
+
primaryConfigDocument: planned.document,
|
|
254
|
+
};
|
|
255
|
+
// Root resources may consume any Primary Config section. Reinstall them
|
|
256
|
+
// when present so a committed patch cannot leave Host services on the
|
|
257
|
+
// previous generation's document.
|
|
258
|
+
if (!this.#installResources && this.#model && !planned.roots.includes(rootPluginId())) {
|
|
259
|
+
prepared = await this.#prepareSubtrees(current, inspected, planned.roots);
|
|
217
260
|
}
|
|
261
|
+
else {
|
|
262
|
+
prepared = await this.#prepareInspected(current, inspected);
|
|
263
|
+
}
|
|
264
|
+
return documentTransaction
|
|
265
|
+
? withConfigDocumentHandoff(prepared.generation, documentTransaction, (committed) => { committedDocument = committed; })
|
|
266
|
+
: prepared.generation;
|
|
218
267
|
}
|
|
219
|
-
|
|
268
|
+
catch (error) {
|
|
269
|
+
// A prepared document transaction is inert until handoff; roll it back
|
|
270
|
+
// before any shadow-phase failure escapes so the port never leaks a
|
|
271
|
+
// pending write.
|
|
220
272
|
if (documentTransaction)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
const inspected = {
|
|
225
|
-
graph,
|
|
226
|
-
configResolver: this.#configViewResolver(planned.views),
|
|
227
|
-
primaryConfigDocument: planned.document,
|
|
228
|
-
};
|
|
229
|
-
// Root resources may consume any Primary Config section. Reinstall them
|
|
230
|
-
// when present so a committed patch cannot leave Host services on the
|
|
231
|
-
// previous generation's document.
|
|
232
|
-
if (!this.#installResources && this.#model && !planned.roots.includes(rootPluginId())) {
|
|
233
|
-
prepared = await this.#prepareSubtrees(current, inspected, planned.roots);
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
prepared = await this.#prepareInspected(current, inspected);
|
|
273
|
+
await documentTransaction.rollback().catch(() => undefined);
|
|
274
|
+
throw error;
|
|
237
275
|
}
|
|
238
|
-
return documentTransaction
|
|
239
|
-
? withConfigDocumentHandoff(prepared.generation, documentTransaction, (committed) => { committedDocument = committed; })
|
|
240
|
-
: prepared.generation;
|
|
241
276
|
});
|
|
242
277
|
const completed = requireConfigPatchPlan(plan);
|
|
243
278
|
if (prepared)
|
|
@@ -289,6 +324,11 @@ function requireConfigDocumentSnapshot(snapshot) {
|
|
|
289
324
|
throw new Error('ConfigDocumentPort has not been read');
|
|
290
325
|
return snapshot;
|
|
291
326
|
}
|
|
327
|
+
function requireConfigDocument(document) {
|
|
328
|
+
if (!document)
|
|
329
|
+
throw new Error('Config patches require a document-backed RootRuntime config');
|
|
330
|
+
return document;
|
|
331
|
+
}
|
|
292
332
|
function cloneConfigPatches(patches) {
|
|
293
333
|
return Object.freeze(patches.map((patch) => Object.freeze(patch.op === 'set'
|
|
294
334
|
? { ...patch, path: Object.freeze([...patch.path]), value: structuredClone(patch.value) }
|
|
@@ -313,7 +353,7 @@ class GenerationAssembler {
|
|
|
313
353
|
#catalog = new FeatureCatalog();
|
|
314
354
|
#rootsByFeature = new Map();
|
|
315
355
|
#featureIdsByPackageRoot = new Map();
|
|
316
|
-
#projectionDisposers =
|
|
356
|
+
#projectionDisposers = new Map();
|
|
317
357
|
#host;
|
|
318
358
|
#plugins;
|
|
319
359
|
constructor(graph, modules, configResolver, primaryConfigDocument, generation, environment, installResources, environmentLayers = {}, isolation) {
|
|
@@ -334,11 +374,14 @@ class GenerationAssembler {
|
|
|
334
374
|
// Prepare is deliberately ordered: providers define discovery, setup
|
|
335
375
|
// creates owner scopes, then definitions can be projected against both.
|
|
336
376
|
await this.#loadProviders(this.graph.root);
|
|
377
|
+
this.#plugins.installSetupFeatureAliases(featureSetupAliases(this.#catalog.values()));
|
|
337
378
|
await this.#plugins.setupTree(this.graph.root);
|
|
338
379
|
await this.#discover();
|
|
339
380
|
const projected = await new FeatureProjector(this.#catalog.values())
|
|
340
381
|
.project(this.generation, this.#projectionState());
|
|
341
|
-
|
|
382
|
+
for (const [feature, dispose] of projected.disposers) {
|
|
383
|
+
this.#projectionDisposers.set(feature, dispose);
|
|
384
|
+
}
|
|
342
385
|
const state = projected.state;
|
|
343
386
|
const snapshot = createSnapshotView(this.generation, state);
|
|
344
387
|
const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, this.#featureIdsByPackageRoot);
|
|
@@ -364,7 +407,7 @@ class GenerationAssembler {
|
|
|
364
407
|
};
|
|
365
408
|
}
|
|
366
409
|
catch (error) {
|
|
367
|
-
await disposePreparedParts(this.#plugins.createdScopeDisposers().map(([, dispose]) => dispose), this.#projectionDisposers, error);
|
|
410
|
+
await disposePreparedParts(this.#plugins.createdScopeDisposers().map(([, dispose]) => dispose), [...this.#projectionDisposers.values()], error);
|
|
368
411
|
throw error;
|
|
369
412
|
}
|
|
370
413
|
}
|
|
@@ -391,12 +434,13 @@ class GenerationAssembler {
|
|
|
391
434
|
await this.#loadProviders(child);
|
|
392
435
|
}
|
|
393
436
|
async #discover() {
|
|
437
|
+
mergeSetupCapabilities(this.#capabilities, this.#plugins.setupCapabilities(), new Map(this.#catalog.values().map((provider) => [provider.id, provider])), this.#rootsByFeature);
|
|
394
438
|
const discovery = new FeatureDiscovery(this.#host);
|
|
395
439
|
for (const provider of this.#catalog.values()) {
|
|
396
440
|
const roots = this.#rootsByFeature.get(provider.id) ?? [];
|
|
397
441
|
const slots = await discovery.discover(provider, roots);
|
|
398
442
|
for (const slot of slots)
|
|
399
|
-
this.#capabilities
|
|
443
|
+
addCapabilitySlot(this.#capabilities, slot);
|
|
400
444
|
}
|
|
401
445
|
}
|
|
402
446
|
#projectionState() {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CapabilityId, type CapabilitySlot, type FeatureId, type SetupCapabilityRegistration } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { CapabilityRoot, FeatureProvider } from '@zhin.js/feature-kit';
|
|
3
|
+
export declare function mergeSetupCapabilities(capabilities: Map<CapabilityId, CapabilitySlot>, registrations: readonly Readonly<SetupCapabilityRegistration>[], providers: ReadonlyMap<FeatureId, FeatureProvider>, rootsByFeature: ReadonlyMap<FeatureId, readonly CapabilityRoot[]>): void;
|
|
4
|
+
export declare function addCapabilitySlot(capabilities: Map<CapabilityId, CapabilitySlot>, slot: Readonly<CapabilitySlot>): void;
|
|
5
|
+
export declare function featureSetupAliases(providers: Iterable<FeatureProvider>): ReadonlyMap<string, FeatureId>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createCapabilitySlot, } from '@zhin.js/plugin-runtime';
|
|
2
|
+
export function mergeSetupCapabilities(capabilities, registrations, providers, rootsByFeature) {
|
|
3
|
+
for (const registration of registrations) {
|
|
4
|
+
const provider = providers.get(registration.feature);
|
|
5
|
+
const mounted = rootsByFeature.get(registration.feature)?.some((root) => root.owner === registration.owner);
|
|
6
|
+
if (!provider || !mounted) {
|
|
7
|
+
throw new Error(`Feature ${registration.feature} is not mounted for Plugin ${registration.owner}`);
|
|
8
|
+
}
|
|
9
|
+
const definition = provider.authoring.validate(registration.definition, {
|
|
10
|
+
owner: registration.owner,
|
|
11
|
+
feature: registration.feature,
|
|
12
|
+
localName: registration.localName,
|
|
13
|
+
source: registration.source,
|
|
14
|
+
});
|
|
15
|
+
addCapabilitySlot(capabilities, createCapabilitySlot({
|
|
16
|
+
owner: registration.owner,
|
|
17
|
+
feature: registration.feature,
|
|
18
|
+
localName: registration.localName,
|
|
19
|
+
source: registration.source,
|
|
20
|
+
definition,
|
|
21
|
+
origin: 'setup',
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function addCapabilitySlot(capabilities, slot) {
|
|
26
|
+
if (capabilities.has(slot.id)) {
|
|
27
|
+
throw new Error(`Duplicate Capability Slot: ${slot.id}`);
|
|
28
|
+
}
|
|
29
|
+
capabilities.set(slot.id, slot);
|
|
30
|
+
}
|
|
31
|
+
export function featureSetupAliases(providers) {
|
|
32
|
+
const aliases = new Map();
|
|
33
|
+
for (const provider of providers) {
|
|
34
|
+
const method = provider.authoring.setupMethod;
|
|
35
|
+
if (!method)
|
|
36
|
+
continue;
|
|
37
|
+
const existing = aliases.get(method);
|
|
38
|
+
if (existing && existing !== provider.id) {
|
|
39
|
+
throw new Error(`Duplicate Feature setup method ${method}: ${existing}, ${provider.id}`);
|
|
40
|
+
}
|
|
41
|
+
aliases.set(method, provider.id);
|
|
42
|
+
}
|
|
43
|
+
return aliases;
|
|
44
|
+
}
|
|
@@ -24,17 +24,25 @@ export class SlotGenerationPreparer {
|
|
|
24
24
|
for (const slot of replacements)
|
|
25
25
|
capabilities.set(slot.id, slot);
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
// Capability-file HMR projects only the owning Features. The complete
|
|
28
|
+
// snapshot below is assembled by retaining every other projection.
|
|
29
|
+
const providers = [...selectedByFeature.keys()].map((feature) => {
|
|
30
|
+
const provider = this.model.providers.get(feature);
|
|
31
|
+
if (!provider)
|
|
32
|
+
throw new Error(`Missing Feature provider for ${feature}`);
|
|
33
|
+
return provider;
|
|
34
|
+
});
|
|
35
|
+
const projected = await new FeatureProjector(providers).project(current.generation + 1, {
|
|
28
36
|
root: current.root,
|
|
29
37
|
tree: current.tree,
|
|
30
38
|
config: current.config,
|
|
31
39
|
resources: current.resources,
|
|
32
40
|
capabilities,
|
|
33
|
-
});
|
|
41
|
+
}, current.projections);
|
|
34
42
|
try {
|
|
35
43
|
const snapshot = createSnapshotView(current.generation + 1, projected.state);
|
|
36
44
|
const ownership = SourceOwnershipIndex.fromGeneration(this.model.graph, snapshot, this.model.featureIdsByPackageRoot);
|
|
37
|
-
const assets = this.model.assets.
|
|
45
|
+
const assets = this.model.assets.replaceProjections(selectedByFeature.keys(), projected.disposers);
|
|
38
46
|
return {
|
|
39
47
|
generation: {
|
|
40
48
|
snapshot: projected.state,
|
|
@@ -46,7 +54,7 @@ export class SlotGenerationPreparer {
|
|
|
46
54
|
};
|
|
47
55
|
}
|
|
48
56
|
catch (error) {
|
|
49
|
-
await disposeProjections(projected.disposers, error);
|
|
57
|
+
await disposeProjections(projected.disposers.values(), error);
|
|
50
58
|
throw error;
|
|
51
59
|
}
|
|
52
60
|
}
|
package/lib/source-ownership.js
CHANGED
|
@@ -4,6 +4,7 @@ import { FeatureProjector, composeGenerationHandoffs } from './feature-projector
|
|
|
4
4
|
import { NodeDiscoveryHost } from './node-discovery-host.js';
|
|
5
5
|
import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
|
|
6
6
|
import { SourceOwnershipIndex } from './source-ownership.js';
|
|
7
|
+
import { addCapabilitySlot, featureSetupAliases, mergeSetupCapabilities, } from './setup-capabilities.js';
|
|
7
8
|
export class SubtreeTopologyChangedError extends Error {
|
|
8
9
|
constructor(message) {
|
|
9
10
|
super(message);
|
|
@@ -41,8 +42,9 @@ export class SubtreeGenerationPreparer {
|
|
|
41
42
|
config: current.config,
|
|
42
43
|
resources: current.resources,
|
|
43
44
|
}, this.isolation);
|
|
45
|
+
plugins.installSetupFeatureAliases(featureSetupAliases(this.model.providers.values()));
|
|
44
46
|
plugins.removeSubtrees(roots);
|
|
45
|
-
const projectionDisposers =
|
|
47
|
+
const projectionDisposers = new Map();
|
|
46
48
|
try {
|
|
47
49
|
for (const root of roots) {
|
|
48
50
|
const node = nodes.get(root);
|
|
@@ -55,6 +57,7 @@ export class SubtreeGenerationPreparer {
|
|
|
55
57
|
if (roots.some((root) => isWithin(slot.owner, root)))
|
|
56
58
|
capabilities.delete(id);
|
|
57
59
|
}
|
|
60
|
+
mergeSetupCapabilities(capabilities, plugins.setupCapabilities(), this.model.providers, this.model.rootsByFeature);
|
|
58
61
|
const discovery = new FeatureDiscovery(new NodeDiscoveryHost(this.modules));
|
|
59
62
|
for (const provider of this.model.providers.values()) {
|
|
60
63
|
const affectedRoots = (this.model.rootsByFeature.get(provider.id) ?? [])
|
|
@@ -63,7 +66,7 @@ export class SubtreeGenerationPreparer {
|
|
|
63
66
|
continue;
|
|
64
67
|
const slots = await discovery.discover(provider, affectedRoots);
|
|
65
68
|
for (const slot of slots)
|
|
66
|
-
capabilities
|
|
69
|
+
addCapabilitySlot(capabilities, slot);
|
|
67
70
|
}
|
|
68
71
|
const projected = await new FeatureProjector(this.model.providers.values()).project(current.generation + 1, {
|
|
69
72
|
root: current.root,
|
|
@@ -72,7 +75,9 @@ export class SubtreeGenerationPreparer {
|
|
|
72
75
|
resources: plugins.resources,
|
|
73
76
|
capabilities,
|
|
74
77
|
});
|
|
75
|
-
|
|
78
|
+
for (const [feature, dispose] of projected.disposers) {
|
|
79
|
+
projectionDisposers.set(feature, dispose);
|
|
80
|
+
}
|
|
76
81
|
const snapshot = createSnapshotView(current.generation + 1, projected.state);
|
|
77
82
|
const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, this.model.featureIdsByPackageRoot);
|
|
78
83
|
const replacements = new Map(plugins.createdScopeDisposers());
|
|
@@ -95,7 +100,7 @@ export class SubtreeGenerationPreparer {
|
|
|
95
100
|
};
|
|
96
101
|
}
|
|
97
102
|
catch (error) {
|
|
98
|
-
await rollback(plugins.createdScopeDisposers().map(([, dispose]) => dispose), projectionDisposers, error);
|
|
103
|
+
await rollback(plugins.createdScopeDisposers().map(([, dispose]) => dispose), [...projectionDisposers.values()], error);
|
|
99
104
|
throw error;
|
|
100
105
|
}
|
|
101
106
|
}
|
|
@@ -6,6 +6,7 @@ import { FeatureProjector, composeGenerationHandoffs } from './feature-projector
|
|
|
6
6
|
import { NodeDiscoveryHost } from './node-discovery-host.js';
|
|
7
7
|
import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
|
|
8
8
|
import { SourceOwnershipIndex } from './source-ownership.js';
|
|
9
|
+
import { addCapabilitySlot, featureSetupAliases, mergeSetupCapabilities, } from './setup-capabilities.js';
|
|
9
10
|
import { TopologyTransactionPlanner, collapseRoots, graphNodes, graphOrder, isWithin, } from './topology-transaction.js';
|
|
10
11
|
/** Prepares manifest topology changes without rebuilding stable Plugin Scopes. */
|
|
11
12
|
export class TopologyGenerationPreparer {
|
|
@@ -47,6 +48,7 @@ export class TopologyGenerationPreparer {
|
|
|
47
48
|
config: current.config,
|
|
48
49
|
resources: current.resources,
|
|
49
50
|
}, this.isolation);
|
|
51
|
+
plugins.installSetupFeatureAliases(featureSetupAliases(featureTopology.providers.values()));
|
|
50
52
|
const setupRoots = collapseRoots([
|
|
51
53
|
...plan.addedPluginRoots,
|
|
52
54
|
...plan.replacedPluginRoots,
|
|
@@ -56,7 +58,7 @@ export class TopologyGenerationPreparer {
|
|
|
56
58
|
...plan.replacedPluginRoots,
|
|
57
59
|
]);
|
|
58
60
|
plugins.removeSubtrees(removalRoots);
|
|
59
|
-
const projectionDisposers =
|
|
61
|
+
const projectionDisposers = new Map();
|
|
60
62
|
try {
|
|
61
63
|
for (const root of setupRoots) {
|
|
62
64
|
const node = nextNodes.get(root);
|
|
@@ -68,6 +70,7 @@ export class TopologyGenerationPreparer {
|
|
|
68
70
|
// remove, move, or reorder operations.
|
|
69
71
|
plugins.synchronizeTree(this.graph.root);
|
|
70
72
|
const capabilities = await this.#prepareCapabilities(current, plan, setupRoots, featureTopology);
|
|
73
|
+
mergeSetupCapabilities(capabilities, plugins.setupCapabilities(), featureTopology.providers, featureTopology.rootsByFeature);
|
|
71
74
|
const projected = await new FeatureProjector(featureTopology.providers.values()).project(current.generation + 1, {
|
|
72
75
|
root: current.root,
|
|
73
76
|
tree: plugins.tree,
|
|
@@ -75,7 +78,9 @@ export class TopologyGenerationPreparer {
|
|
|
75
78
|
resources: plugins.resources,
|
|
76
79
|
capabilities,
|
|
77
80
|
});
|
|
78
|
-
|
|
81
|
+
for (const [feature, dispose] of projected.disposers) {
|
|
82
|
+
projectionDisposers.set(feature, dispose);
|
|
83
|
+
}
|
|
79
84
|
const snapshot = createSnapshotView(current.generation + 1, projected.state);
|
|
80
85
|
const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, featureTopology.featureIdsByPackageRoot);
|
|
81
86
|
const replacements = new Map(plugins.createdScopeDisposers());
|
|
@@ -98,7 +103,7 @@ export class TopologyGenerationPreparer {
|
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
105
|
catch (error) {
|
|
101
|
-
await rollback(plugins.createdScopeDisposers().map(([, dispose]) => dispose), projectionDisposers, error);
|
|
106
|
+
await rollback(plugins.createdScopeDisposers().map(([, dispose]) => dispose), [...projectionDisposers.values()], error);
|
|
102
107
|
throw error;
|
|
103
108
|
}
|
|
104
109
|
}
|
|
@@ -177,7 +182,7 @@ export class TopologyGenerationPreparer {
|
|
|
177
182
|
if (!provider)
|
|
178
183
|
throw new Error(`Missing Feature provider for ${feature}`);
|
|
179
184
|
for (const slot of await discovery.discover(provider, selected)) {
|
|
180
|
-
capabilities
|
|
185
|
+
addCapabilitySlot(capabilities, slot);
|
|
181
186
|
}
|
|
182
187
|
}
|
|
183
188
|
return capabilities;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/runtime",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Static Plugin graph, generation transaction and HMR Root runtime for Zhin.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -18,22 +18,22 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"ajv": "8.18.0",
|
|
20
20
|
"semver": "7.8.5",
|
|
21
|
-
"@zhin.js/feature-kit": "1.0.
|
|
22
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
21
|
+
"@zhin.js/feature-kit": "1.0.3",
|
|
22
|
+
"@zhin.js/plugin-runtime": "1.1.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^26.1.0",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
|
-
"@zhin.js/adapter": "1.1.
|
|
28
|
-
"@zhin.js/
|
|
29
|
-
"@zhin.js/
|
|
30
|
-
"@zhin.js/component": "1.0.
|
|
31
|
-
"@zhin.js/layout": "1.0.
|
|
32
|
-
"@zhin.js/mcp-feature": "1.0.
|
|
33
|
-
"@zhin.js/middleware": "1.0.
|
|
34
|
-
"@zhin.js/
|
|
35
|
-
"@zhin.js/
|
|
36
|
-
"@zhin.js/
|
|
27
|
+
"@zhin.js/adapter": "1.1.1",
|
|
28
|
+
"@zhin.js/agent-feature": "1.0.3",
|
|
29
|
+
"@zhin.js/command": "1.0.3",
|
|
30
|
+
"@zhin.js/component": "1.0.3",
|
|
31
|
+
"@zhin.js/layout": "1.0.3",
|
|
32
|
+
"@zhin.js/mcp-feature": "1.0.3",
|
|
33
|
+
"@zhin.js/middleware": "1.0.3",
|
|
34
|
+
"@zhin.js/page": "1.0.3",
|
|
35
|
+
"@zhin.js/skill": "1.0.3",
|
|
36
|
+
"@zhin.js/tool": "1.0.3"
|
|
37
37
|
},
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|