@zhin.js/runtime 1.0.0
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 +39 -0
- package/lib/compatibility.d.ts +10 -0
- package/lib/compatibility.js +45 -0
- package/lib/config-composer.d.ts +27 -0
- package/lib/config-composer.js +212 -0
- package/lib/config-document.d.ts +19 -0
- package/lib/config-document.js +6 -0
- package/lib/config-patch-planner.d.ts +25 -0
- package/lib/config-patch-planner.js +126 -0
- package/lib/environment-store.d.ts +56 -0
- package/lib/environment-store.js +300 -0
- package/lib/environment.d.ts +8 -0
- package/lib/environment.js +13 -0
- package/lib/feature-projector.d.ts +15 -0
- package/lib/feature-projector.js +57 -0
- package/lib/generation-assets.d.ts +9 -0
- package/lib/generation-assets.js +67 -0
- package/lib/hmr-coordinator.d.ts +23 -0
- package/lib/hmr-coordinator.js +110 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.js +21 -0
- package/lib/invalidation-planner.d.ts +29 -0
- package/lib/invalidation-planner.js +106 -0
- package/lib/isolation.d.ts +28 -0
- package/lib/isolation.js +1 -0
- package/lib/manifest.d.ts +45 -0
- package/lib/manifest.js +195 -0
- package/lib/module-runtime.d.ts +17 -0
- package/lib/module-runtime.js +7 -0
- package/lib/native-development-runtime.d.ts +22 -0
- package/lib/native-development-runtime.js +190 -0
- package/lib/node-discovery-host.d.ts +10 -0
- package/lib/node-discovery-host.js +37 -0
- package/lib/package-resolver.d.ts +23 -0
- package/lib/package-resolver.js +121 -0
- package/lib/plugin-scope-assembler.d.ts +38 -0
- package/lib/plugin-scope-assembler.js +167 -0
- package/lib/process-restart.d.ts +15 -0
- package/lib/process-restart.js +29 -0
- package/lib/project-graph.d.ts +30 -0
- package/lib/project-graph.js +129 -0
- package/lib/restart-boundary.d.ts +6 -0
- package/lib/restart-boundary.js +68 -0
- package/lib/root-runtime.d.ts +37 -0
- package/lib/root-runtime.js +432 -0
- package/lib/runtime-generation.d.ts +18 -0
- package/lib/runtime-generation.js +1 -0
- package/lib/slot-generation-preparer.d.ts +9 -0
- package/lib/slot-generation-preparer.js +76 -0
- package/lib/source-ownership.d.ts +19 -0
- package/lib/source-ownership.js +105 -0
- package/lib/subtree-generation-preparer.d.ts +24 -0
- package/lib/subtree-generation-preparer.js +152 -0
- package/lib/topology-generation-preparer.d.ts +22 -0
- package/lib/topology-generation-preparer.js +249 -0
- package/lib/topology-transaction.d.ts +26 -0
- package/lib/topology-transaction.js +142 -0
- package/lib/typescript-specifier-remap.d.ts +23 -0
- package/lib/typescript-specifier-remap.js +92 -0
- package/package.json +57 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { DisposeStack, GenerationHandoffStack, RootController, createSnapshotView, rootPluginId, } from '@zhin.js/plugin-runtime';
|
|
4
|
+
import { FeatureCatalog, FeatureDiscovery, } from '@zhin.js/feature-kit';
|
|
5
|
+
import { ConfigComposer } from './config-composer.js';
|
|
6
|
+
import { ConfigDocumentDivergenceError, } from './config-document.js';
|
|
7
|
+
import { ConfigPatchPlanner, } from './config-patch-planner.js';
|
|
8
|
+
import { defineRuntimeEnvironment } from './environment.js';
|
|
9
|
+
import { createEnvStore, defineEnvironmentLayers, } from './environment-store.js';
|
|
10
|
+
import { FeatureProjector, composeGenerationHandoffs, } from './feature-projector.js';
|
|
11
|
+
import { GenerationAssets } from './generation-assets.js';
|
|
12
|
+
import { NodeDiscoveryHost } from './node-discovery-host.js';
|
|
13
|
+
import { NodePackageResolver } from './package-resolver.js';
|
|
14
|
+
import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
|
|
15
|
+
import { ProjectGraphService } from './project-graph.js';
|
|
16
|
+
import { HmrCoordinator } from './hmr-coordinator.js';
|
|
17
|
+
import { RootProcessRestartExecutor, } from './process-restart.js';
|
|
18
|
+
import { SlotGenerationPreparer } from './slot-generation-preparer.js';
|
|
19
|
+
import { SourceOwnershipIndex } from './source-ownership.js';
|
|
20
|
+
import { SubtreeGenerationPreparer, SubtreeTopologyChangedError, } from './subtree-generation-preparer.js';
|
|
21
|
+
import { TopologyGenerationPreparer } from './topology-generation-preparer.js';
|
|
22
|
+
import { RestartBoundaryPlanner } from './restart-boundary.js';
|
|
23
|
+
export class RootRuntime {
|
|
24
|
+
controller;
|
|
25
|
+
#projectRoot;
|
|
26
|
+
#modules;
|
|
27
|
+
#environment;
|
|
28
|
+
#environmentLayers;
|
|
29
|
+
#configResolver;
|
|
30
|
+
#configPort;
|
|
31
|
+
#configSnapshot;
|
|
32
|
+
#configDocument;
|
|
33
|
+
#installResources;
|
|
34
|
+
#isolation;
|
|
35
|
+
#ownership = SourceOwnershipIndex.empty();
|
|
36
|
+
#model;
|
|
37
|
+
#configPatchTail = Promise.resolve();
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.#projectRoot = resolve(options.projectRoot);
|
|
40
|
+
this.#modules = options.modules;
|
|
41
|
+
this.#environment = defineRuntimeEnvironment(options.environment);
|
|
42
|
+
this.#environmentLayers = defineEnvironmentLayers(options.environmentVariables);
|
|
43
|
+
if (typeof options.config === 'function')
|
|
44
|
+
this.#configResolver = options.config;
|
|
45
|
+
else if (isConfigDocumentPort(options.config))
|
|
46
|
+
this.#configPort = options.config;
|
|
47
|
+
else
|
|
48
|
+
this.#configDocument = structuredClone(options.config ?? {});
|
|
49
|
+
this.#installResources = options.installResources;
|
|
50
|
+
this.#isolation = options.isolation;
|
|
51
|
+
this.controller = new RootController(emptyState(), options.onControlError);
|
|
52
|
+
}
|
|
53
|
+
get snapshot() {
|
|
54
|
+
return this.controller.snapshots.current;
|
|
55
|
+
}
|
|
56
|
+
get sourceOwnership() {
|
|
57
|
+
return this.#ownership;
|
|
58
|
+
}
|
|
59
|
+
async start() {
|
|
60
|
+
if (this.#configPort) {
|
|
61
|
+
const snapshot = await this.#configPort.read();
|
|
62
|
+
this.#configSnapshot = snapshot;
|
|
63
|
+
this.#configDocument = structuredClone(snapshot.document);
|
|
64
|
+
}
|
|
65
|
+
let prepared;
|
|
66
|
+
const snapshot = await this.controller.start(async (current) => {
|
|
67
|
+
prepared = await this.#prepare(current);
|
|
68
|
+
return prepared.generation;
|
|
69
|
+
});
|
|
70
|
+
this.#accept(requirePrepared(prepared));
|
|
71
|
+
return snapshot;
|
|
72
|
+
}
|
|
73
|
+
async reload(target = rootPluginId()) {
|
|
74
|
+
let prepared;
|
|
75
|
+
const snapshot = await this.controller.reload(target, async (current) => {
|
|
76
|
+
prepared = await this.#prepare(current);
|
|
77
|
+
return prepared.generation;
|
|
78
|
+
});
|
|
79
|
+
this.#accept(requirePrepared(prepared));
|
|
80
|
+
return snapshot;
|
|
81
|
+
}
|
|
82
|
+
patchConfig(patches) {
|
|
83
|
+
const operations = cloneConfigPatches(patches);
|
|
84
|
+
const result = this.#configPatchTail.then(() => this.#applyConfigPatches(operations), () => this.#applyConfigPatches(operations));
|
|
85
|
+
this.#configPatchTail = result.catch(() => undefined);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
createHmrCoordinator(options) {
|
|
89
|
+
return new HmrCoordinator({
|
|
90
|
+
...options,
|
|
91
|
+
modules: this.#modules,
|
|
92
|
+
ownership: () => this.#ownership,
|
|
93
|
+
runtime: {
|
|
94
|
+
reload: async (plan) => {
|
|
95
|
+
const result = await this.#reloadPlan(plan);
|
|
96
|
+
return isProcessPlan(result) ? result : undefined;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
createProcessRestartExecutor(adapter) {
|
|
102
|
+
return new RootProcessRestartExecutor(this, adapter);
|
|
103
|
+
}
|
|
104
|
+
async stop() {
|
|
105
|
+
try {
|
|
106
|
+
await this.controller.stop();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await this.#modules.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async #reloadPlan(plan) {
|
|
113
|
+
let prepared;
|
|
114
|
+
let restart;
|
|
115
|
+
const snapshot = await this.controller.reload(plan.subtrees[0] ?? plan.slots[0] ?? rootPluginId(), async (current) => {
|
|
116
|
+
if (this.#model && this.#isManifestTopologyPlan(plan)) {
|
|
117
|
+
const inspected = await this.#inspectProject();
|
|
118
|
+
restart = new RestartBoundaryPlanner().plan(this.#model.graph, inspected.graph, plan.changed);
|
|
119
|
+
if (restart)
|
|
120
|
+
return undefined;
|
|
121
|
+
prepared = await new TopologyGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current);
|
|
122
|
+
}
|
|
123
|
+
else if (plan.subtrees.length === 0 && plan.slots.length > 0 && this.#model) {
|
|
124
|
+
prepared = await new SlotGenerationPreparer(this.#modules, this.#model)
|
|
125
|
+
.prepare(current, plan.slots);
|
|
126
|
+
}
|
|
127
|
+
else if (this.#model && this.#canPrepareSubtrees(plan)) {
|
|
128
|
+
const inspected = await this.#inspectProject();
|
|
129
|
+
prepared = await this.#prepareSubtrees(current, inspected, plan.subtrees);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
prepared = await this.#prepare(current);
|
|
133
|
+
}
|
|
134
|
+
return prepared?.generation;
|
|
135
|
+
});
|
|
136
|
+
if (restart)
|
|
137
|
+
return restart;
|
|
138
|
+
if (prepared)
|
|
139
|
+
this.#accept(prepared);
|
|
140
|
+
return snapshot;
|
|
141
|
+
}
|
|
142
|
+
#isManifestTopologyPlan(plan) {
|
|
143
|
+
let manifest = false;
|
|
144
|
+
for (const source of plan.changed) {
|
|
145
|
+
const records = this.#ownership.recordsFor(source);
|
|
146
|
+
if (records.some((record) => record.role === 'manifest'))
|
|
147
|
+
manifest = true;
|
|
148
|
+
if (records.some((record) => record.role !== 'manifest'))
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return manifest;
|
|
152
|
+
}
|
|
153
|
+
#accept(prepared) {
|
|
154
|
+
this.#ownership = prepared.ownership;
|
|
155
|
+
this.#model = prepared.model;
|
|
156
|
+
}
|
|
157
|
+
#canPrepareSubtrees(plan) {
|
|
158
|
+
if (plan.subtrees.length === 0 || plan.subtrees.includes(rootPluginId()))
|
|
159
|
+
return false;
|
|
160
|
+
return plan.changed.every((source) => {
|
|
161
|
+
const records = this.#ownership.recordsFor(source);
|
|
162
|
+
return records.length > 0 && records.every((record) => record.role === 'plugin' || record.role === 'schema');
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async #prepare(current) {
|
|
166
|
+
return this.#prepareInspected(current, await this.#inspectProject());
|
|
167
|
+
}
|
|
168
|
+
async #inspectProject() {
|
|
169
|
+
const resolver = await NodePackageResolver.create(this.#projectRoot);
|
|
170
|
+
const graph = await new ProjectGraphService(resolver).inspect(this.#projectRoot);
|
|
171
|
+
const configResolver = this.#configResolver
|
|
172
|
+
? this.#configResolver
|
|
173
|
+
: await new ConfigComposer()
|
|
174
|
+
.compose(graph, this.#configDocument)
|
|
175
|
+
.then((composed) => this.#configViewResolver(composed.views));
|
|
176
|
+
return { graph, configResolver };
|
|
177
|
+
}
|
|
178
|
+
#configViewResolver(views) {
|
|
179
|
+
const env = createEnvStore(rootPluginId(), this.#environment, this.#environmentLayers);
|
|
180
|
+
return (node) => {
|
|
181
|
+
const view = views.get(node.id);
|
|
182
|
+
if (view === undefined)
|
|
183
|
+
return undefined;
|
|
184
|
+
return env.expandMissingAsEmpty(view);
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async #applyConfigPatches(patches) {
|
|
188
|
+
if (!this.#configDocument) {
|
|
189
|
+
throw new Error('Config patches require a document-backed RootRuntime config');
|
|
190
|
+
}
|
|
191
|
+
const currentDocument = this.#configDocument;
|
|
192
|
+
let plan;
|
|
193
|
+
let prepared;
|
|
194
|
+
let documentTransaction;
|
|
195
|
+
let committedDocument;
|
|
196
|
+
const snapshot = await this.controller.reload(rootPluginId(), async (current) => {
|
|
197
|
+
const resolver = await NodePackageResolver.create(this.#projectRoot);
|
|
198
|
+
const graph = await new ProjectGraphService(resolver).inspect(this.#projectRoot);
|
|
199
|
+
const planned = await new ConfigPatchPlanner().plan(graph, currentDocument, patches);
|
|
200
|
+
plan = planned;
|
|
201
|
+
if (!planned.documentChanged)
|
|
202
|
+
return undefined;
|
|
203
|
+
if (this.#configPort) {
|
|
204
|
+
const currentSnapshot = requireConfigDocumentSnapshot(this.#configSnapshot);
|
|
205
|
+
// Port preparation must remain inert; validation and shadow setup can
|
|
206
|
+
// still reject this candidate without touching the backing document.
|
|
207
|
+
documentTransaction = await this.#configPort.prepare(currentSnapshot, patches);
|
|
208
|
+
if (!isDeepStrictEqual(documentTransaction.document, planned.candidate)) {
|
|
209
|
+
throw new ConfigDocumentDivergenceError();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (planned.roots.length === 0) {
|
|
213
|
+
if (documentTransaction)
|
|
214
|
+
committedDocument = await documentTransaction.commit();
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
const inspected = {
|
|
218
|
+
graph,
|
|
219
|
+
configResolver: this.#configViewResolver(planned.views),
|
|
220
|
+
};
|
|
221
|
+
if (this.#model && !planned.roots.includes(rootPluginId())) {
|
|
222
|
+
prepared = await this.#prepareSubtrees(current, inspected, planned.roots);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
prepared = await this.#prepareInspected(current, inspected);
|
|
226
|
+
}
|
|
227
|
+
return documentTransaction
|
|
228
|
+
? withConfigDocumentHandoff(prepared.generation, documentTransaction, (committed) => { committedDocument = committed; })
|
|
229
|
+
: prepared.generation;
|
|
230
|
+
});
|
|
231
|
+
const completed = requireConfigPatchPlan(plan);
|
|
232
|
+
if (prepared)
|
|
233
|
+
this.#accept(prepared);
|
|
234
|
+
this.#configDocument = completed.candidate;
|
|
235
|
+
if (committedDocument)
|
|
236
|
+
this.#configSnapshot = committedDocument;
|
|
237
|
+
return snapshot;
|
|
238
|
+
}
|
|
239
|
+
#prepareInspected(current, inspected) {
|
|
240
|
+
const assembler = new GenerationAssembler(inspected.graph, this.#modules, inspected.configResolver, current.generation + 1, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation);
|
|
241
|
+
return assembler.prepare();
|
|
242
|
+
}
|
|
243
|
+
async #prepareSubtrees(current, inspected, roots) {
|
|
244
|
+
if (!this.#model)
|
|
245
|
+
return this.#prepareInspected(current, inspected);
|
|
246
|
+
try {
|
|
247
|
+
return await new SubtreeGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current, roots);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (!(error instanceof SubtreeTopologyChangedError))
|
|
251
|
+
throw error;
|
|
252
|
+
return this.#prepareInspected(current, inspected);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function withConfigDocumentHandoff(generation, document, committed) {
|
|
257
|
+
const handoffs = new GenerationHandoffStack();
|
|
258
|
+
if (generation.handoff)
|
|
259
|
+
handoffs.add(generation.handoff);
|
|
260
|
+
// File commit follows Resource activation, so reverse compensation restores
|
|
261
|
+
// the document before it deactivates the shadow generation.
|
|
262
|
+
handoffs.add({
|
|
263
|
+
async activateNext() {
|
|
264
|
+
committed(await document.commit());
|
|
265
|
+
},
|
|
266
|
+
deactivateNext: () => document.rollback(),
|
|
267
|
+
});
|
|
268
|
+
return { ...generation, handoff: handoffs.seal() };
|
|
269
|
+
}
|
|
270
|
+
function isConfigDocumentPort(value) {
|
|
271
|
+
if (!value || typeof value !== 'object')
|
|
272
|
+
return false;
|
|
273
|
+
const candidate = value;
|
|
274
|
+
return typeof candidate.read === 'function' && typeof candidate.prepare === 'function';
|
|
275
|
+
}
|
|
276
|
+
function requireConfigDocumentSnapshot(snapshot) {
|
|
277
|
+
if (!snapshot)
|
|
278
|
+
throw new Error('ConfigDocumentPort has not been read');
|
|
279
|
+
return snapshot;
|
|
280
|
+
}
|
|
281
|
+
function cloneConfigPatches(patches) {
|
|
282
|
+
return Object.freeze(patches.map((patch) => Object.freeze(patch.op === 'set'
|
|
283
|
+
? { ...patch, path: Object.freeze([...patch.path]), value: structuredClone(patch.value) }
|
|
284
|
+
: { ...patch, path: Object.freeze([...patch.path]) })));
|
|
285
|
+
}
|
|
286
|
+
function requireConfigPatchPlan(plan) {
|
|
287
|
+
if (!plan)
|
|
288
|
+
throw new Error('RootController completed without a Config patch plan');
|
|
289
|
+
return plan;
|
|
290
|
+
}
|
|
291
|
+
class GenerationAssembler {
|
|
292
|
+
graph;
|
|
293
|
+
modules;
|
|
294
|
+
configResolver;
|
|
295
|
+
generation;
|
|
296
|
+
environment;
|
|
297
|
+
installResources;
|
|
298
|
+
environmentLayers;
|
|
299
|
+
isolation;
|
|
300
|
+
#capabilities = new Map();
|
|
301
|
+
#catalog = new FeatureCatalog();
|
|
302
|
+
#rootsByFeature = new Map();
|
|
303
|
+
#featureIdsByPackageRoot = new Map();
|
|
304
|
+
#projectionDisposers = [];
|
|
305
|
+
#host;
|
|
306
|
+
#plugins;
|
|
307
|
+
constructor(graph, modules, configResolver, generation, environment, installResources, environmentLayers = {}, isolation) {
|
|
308
|
+
this.graph = graph;
|
|
309
|
+
this.modules = modules;
|
|
310
|
+
this.configResolver = configResolver;
|
|
311
|
+
this.generation = generation;
|
|
312
|
+
this.environment = environment;
|
|
313
|
+
this.installResources = installResources;
|
|
314
|
+
this.environmentLayers = environmentLayers;
|
|
315
|
+
this.isolation = isolation;
|
|
316
|
+
this.#host = new NodeDiscoveryHost(modules);
|
|
317
|
+
this.#plugins = new PluginScopeAssembler(modules, configResolver, environment, installResources, environmentLayers, undefined, isolation);
|
|
318
|
+
}
|
|
319
|
+
async prepare() {
|
|
320
|
+
try {
|
|
321
|
+
// Prepare is deliberately ordered: providers define discovery, setup
|
|
322
|
+
// creates owner scopes, then definitions can be projected against both.
|
|
323
|
+
await this.#loadProviders(this.graph.root);
|
|
324
|
+
await this.#plugins.setupTree(this.graph.root);
|
|
325
|
+
await this.#discover();
|
|
326
|
+
const projected = await new FeatureProjector(this.#catalog.values())
|
|
327
|
+
.project(this.generation, this.#projectionState());
|
|
328
|
+
this.#projectionDisposers.push(...projected.disposers);
|
|
329
|
+
const state = projected.state;
|
|
330
|
+
const snapshot = createSnapshotView(this.generation, state);
|
|
331
|
+
const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, this.#featureIdsByPackageRoot);
|
|
332
|
+
const assets = GenerationAssets.create(this.#plugins.createdScopeDisposers(), this.#projectionDisposers);
|
|
333
|
+
return {
|
|
334
|
+
generation: {
|
|
335
|
+
snapshot: state,
|
|
336
|
+
dispose: () => assets.dispose(),
|
|
337
|
+
handoff: composeGenerationHandoffs(this.#plugins.generationHandoff(), projected.handoff),
|
|
338
|
+
},
|
|
339
|
+
ownership,
|
|
340
|
+
model: {
|
|
341
|
+
graph: this.graph,
|
|
342
|
+
providers: new Map(this.#catalog.values().map((provider) => [provider.id, provider])),
|
|
343
|
+
rootsByFeature: new Map([...this.#rootsByFeature].map(([feature, roots]) => [
|
|
344
|
+
feature,
|
|
345
|
+
Object.freeze([...roots]),
|
|
346
|
+
])),
|
|
347
|
+
featureIdsByPackageRoot: new Map(this.#featureIdsByPackageRoot),
|
|
348
|
+
scopes: new Map(this.#plugins.scopes),
|
|
349
|
+
assets,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
await disposePreparedParts(this.#plugins.createdScopeDisposers().map(([, dispose]) => dispose), this.#projectionDisposers, error);
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async #loadProviders(node) {
|
|
359
|
+
for (const requirement of node.features) {
|
|
360
|
+
const manifest = requirement.package.packageJson.zhin;
|
|
361
|
+
const module = await this.modules.load(resolve(requirement.package.root, manifest.entry));
|
|
362
|
+
const provider = module.default;
|
|
363
|
+
if (!provider || provider.protocol !== 1) {
|
|
364
|
+
throw new TypeError(`${requirement.package.name} does not default-export a Feature provider`);
|
|
365
|
+
}
|
|
366
|
+
this.#catalog.add(provider);
|
|
367
|
+
const packageRoot = resolve(requirement.package.root);
|
|
368
|
+
const existingFeature = this.#featureIdsByPackageRoot.get(packageRoot);
|
|
369
|
+
if (existingFeature && existingFeature !== provider.id) {
|
|
370
|
+
throw new Error(`Feature package ${requirement.package.name} changed identity within one generation`);
|
|
371
|
+
}
|
|
372
|
+
this.#featureIdsByPackageRoot.set(packageRoot, provider.id);
|
|
373
|
+
const roots = this.#rootsByFeature.get(provider.id) ?? [];
|
|
374
|
+
roots.push({ owner: node.id, packageRoot: node.package.root });
|
|
375
|
+
this.#rootsByFeature.set(provider.id, roots);
|
|
376
|
+
}
|
|
377
|
+
for (const child of node.children)
|
|
378
|
+
await this.#loadProviders(child);
|
|
379
|
+
}
|
|
380
|
+
async #discover() {
|
|
381
|
+
const discovery = new FeatureDiscovery(this.#host);
|
|
382
|
+
for (const provider of this.#catalog.values()) {
|
|
383
|
+
const roots = this.#rootsByFeature.get(provider.id) ?? [];
|
|
384
|
+
const slots = await discovery.discover(provider, roots);
|
|
385
|
+
for (const slot of slots)
|
|
386
|
+
this.#capabilities.set(slot.id, slot);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
#projectionState() {
|
|
390
|
+
return {
|
|
391
|
+
root: rootPluginId(),
|
|
392
|
+
tree: this.#plugins.tree,
|
|
393
|
+
config: this.#plugins.config,
|
|
394
|
+
resources: this.#plugins.resources,
|
|
395
|
+
capabilities: this.#capabilities,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function emptyState() {
|
|
400
|
+
return {
|
|
401
|
+
root: rootPluginId(),
|
|
402
|
+
tree: new Map(),
|
|
403
|
+
config: new Map(),
|
|
404
|
+
resources: new Map(),
|
|
405
|
+
capabilities: new Map(),
|
|
406
|
+
projections: new Map(),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function requirePrepared(prepared) {
|
|
410
|
+
if (!prepared)
|
|
411
|
+
throw new Error('RootController committed without a prepared generation');
|
|
412
|
+
return prepared;
|
|
413
|
+
}
|
|
414
|
+
function isProcessPlan(value) {
|
|
415
|
+
return 'kind' in value && value.kind === 'process';
|
|
416
|
+
}
|
|
417
|
+
async function disposePreparedParts(scopeDisposers, projectionDisposers, prepareError) {
|
|
418
|
+
const rollback = new DisposeStack();
|
|
419
|
+
for (const dispose of scopeDisposers)
|
|
420
|
+
rollback.add(dispose);
|
|
421
|
+
for (const dispose of projectionDisposers)
|
|
422
|
+
rollback.add(dispose);
|
|
423
|
+
try {
|
|
424
|
+
await rollback.dispose();
|
|
425
|
+
}
|
|
426
|
+
catch (disposeError) {
|
|
427
|
+
if (prepareError !== undefined) {
|
|
428
|
+
throw new AggregateError([prepareError, disposeError], 'Generation prepare and rollback both failed', { cause: disposeError });
|
|
429
|
+
}
|
|
430
|
+
throw disposeError;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FeatureId, PluginId, PreparedGeneration, Scope } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { CapabilityRoot, FeatureProvider } from '@zhin.js/feature-kit';
|
|
3
|
+
import type { GenerationAssets } from './generation-assets.js';
|
|
4
|
+
import type { ProjectGraph } from './project-graph.js';
|
|
5
|
+
import type { SourceOwnershipIndex } from './source-ownership.js';
|
|
6
|
+
export interface RuntimeGenerationModel {
|
|
7
|
+
readonly graph: ProjectGraph;
|
|
8
|
+
readonly providers: ReadonlyMap<FeatureId, FeatureProvider>;
|
|
9
|
+
readonly rootsByFeature: ReadonlyMap<FeatureId, readonly CapabilityRoot[]>;
|
|
10
|
+
readonly featureIdsByPackageRoot: ReadonlyMap<string, FeatureId>;
|
|
11
|
+
readonly scopes: ReadonlyMap<PluginId, Scope>;
|
|
12
|
+
readonly assets: GenerationAssets;
|
|
13
|
+
}
|
|
14
|
+
export interface PreparedRuntimeGeneration {
|
|
15
|
+
readonly generation: PreparedGeneration;
|
|
16
|
+
readonly ownership: SourceOwnershipIndex;
|
|
17
|
+
readonly model: RuntimeGenerationModel;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CapabilityId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { ModuleRuntime } from './module-runtime.js';
|
|
3
|
+
import type { PreparedRuntimeGeneration, RuntimeGenerationModel } from './runtime-generation.js';
|
|
4
|
+
export declare class SlotGenerationPreparer {
|
|
5
|
+
private readonly modules;
|
|
6
|
+
private readonly model;
|
|
7
|
+
constructor(modules: ModuleRuntime, model: RuntimeGenerationModel);
|
|
8
|
+
prepare(current: RuntimeSnapshot, selected: readonly CapabilityId[]): Promise<PreparedRuntimeGeneration>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { DisposeStack, createSnapshotView, } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { FeatureDiscovery } from '@zhin.js/feature-kit';
|
|
3
|
+
import { FeatureProjector, composeGenerationHandoffs } from './feature-projector.js';
|
|
4
|
+
import { NodeDiscoveryHost } from './node-discovery-host.js';
|
|
5
|
+
import { SourceOwnershipIndex } from './source-ownership.js';
|
|
6
|
+
export class SlotGenerationPreparer {
|
|
7
|
+
modules;
|
|
8
|
+
model;
|
|
9
|
+
constructor(modules, model) {
|
|
10
|
+
this.modules = modules;
|
|
11
|
+
this.model = model;
|
|
12
|
+
}
|
|
13
|
+
async prepare(current, selected) {
|
|
14
|
+
const selectedByFeature = groupByFeature(current, selected);
|
|
15
|
+
const capabilities = new Map(current.capabilities);
|
|
16
|
+
const discovery = new FeatureDiscovery(new NodeDiscoveryHost(this.modules));
|
|
17
|
+
for (const [feature, ids] of selectedByFeature) {
|
|
18
|
+
const provider = this.model.providers.get(feature);
|
|
19
|
+
if (!provider)
|
|
20
|
+
throw new Error(`Missing Feature provider for ${feature}`);
|
|
21
|
+
for (const id of ids)
|
|
22
|
+
capabilities.delete(id);
|
|
23
|
+
const replacements = await discovery.discover(provider, this.model.rootsByFeature.get(feature) ?? [], { capabilities: ids });
|
|
24
|
+
for (const slot of replacements)
|
|
25
|
+
capabilities.set(slot.id, slot);
|
|
26
|
+
}
|
|
27
|
+
const projected = await new FeatureProjector(this.model.providers.values()).project(current.generation + 1, {
|
|
28
|
+
root: current.root,
|
|
29
|
+
tree: current.tree,
|
|
30
|
+
config: current.config,
|
|
31
|
+
resources: current.resources,
|
|
32
|
+
capabilities,
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
const snapshot = createSnapshotView(current.generation + 1, projected.state);
|
|
36
|
+
const ownership = SourceOwnershipIndex.fromGeneration(this.model.graph, snapshot, this.model.featureIdsByPackageRoot);
|
|
37
|
+
const assets = this.model.assets.fork(projected.disposers);
|
|
38
|
+
return {
|
|
39
|
+
generation: {
|
|
40
|
+
snapshot: projected.state,
|
|
41
|
+
dispose: () => assets.dispose(),
|
|
42
|
+
handoff: composeGenerationHandoffs(projected.handoff),
|
|
43
|
+
},
|
|
44
|
+
ownership,
|
|
45
|
+
model: { ...this.model, assets },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
await disposeProjections(projected.disposers, error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function groupByFeature(current, selected) {
|
|
55
|
+
const result = new Map();
|
|
56
|
+
for (const id of selected) {
|
|
57
|
+
const slot = current.capabilities.get(id);
|
|
58
|
+
if (!slot)
|
|
59
|
+
throw new Error(`Cannot reload missing Capability Slot: ${id}`);
|
|
60
|
+
const ids = result.get(slot.feature) ?? new Set();
|
|
61
|
+
ids.add(id);
|
|
62
|
+
result.set(slot.feature, ids);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
async function disposeProjections(disposers, prepareError) {
|
|
67
|
+
const rollback = new DisposeStack();
|
|
68
|
+
for (const dispose of disposers)
|
|
69
|
+
rollback.add(dispose);
|
|
70
|
+
try {
|
|
71
|
+
await rollback.dispose();
|
|
72
|
+
}
|
|
73
|
+
catch (disposeError) {
|
|
74
|
+
throw new AggregateError([prepareError, disposeError], 'Slot generation prepare and rollback both failed', { cause: disposeError });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CapabilityId, FeatureId, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { ProjectGraph } from './project-graph.js';
|
|
3
|
+
export type SourceRole = 'plugin' | 'schema' | 'manifest' | 'feature' | 'capability';
|
|
4
|
+
export interface SourceOwnershipRecord {
|
|
5
|
+
readonly source: string;
|
|
6
|
+
readonly role: SourceRole;
|
|
7
|
+
readonly owner: PluginId;
|
|
8
|
+
readonly capability?: CapabilityId;
|
|
9
|
+
readonly feature?: FeatureId;
|
|
10
|
+
}
|
|
11
|
+
export declare class SourceOwnershipIndex {
|
|
12
|
+
#private;
|
|
13
|
+
static empty(): SourceOwnershipIndex;
|
|
14
|
+
static fromGeneration(graph: ProjectGraph, snapshot: RuntimeSnapshot, featureIdsByPackageRoot: ReadonlyMap<string, FeatureId>): SourceOwnershipIndex;
|
|
15
|
+
add(record: SourceOwnershipRecord): void;
|
|
16
|
+
addPackageRoot(packageRoot: string, owner: PluginId): void;
|
|
17
|
+
recordsFor(source: string): readonly SourceOwnershipRecord[];
|
|
18
|
+
ownersForPath(source: string): readonly PluginId[];
|
|
19
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
2
|
+
export class SourceOwnershipIndex {
|
|
3
|
+
#records = new Map();
|
|
4
|
+
#packages = [];
|
|
5
|
+
static empty() {
|
|
6
|
+
return new SourceOwnershipIndex();
|
|
7
|
+
}
|
|
8
|
+
static fromGeneration(graph, snapshot, featureIdsByPackageRoot) {
|
|
9
|
+
const index = new SourceOwnershipIndex();
|
|
10
|
+
visitPlugin(graph.root, (node) => {
|
|
11
|
+
index.addPackageRoot(node.package.root, node.id);
|
|
12
|
+
index.add({
|
|
13
|
+
source: resolve(node.package.root, node.package.packageJson.zhin.entry),
|
|
14
|
+
role: 'plugin',
|
|
15
|
+
owner: node.id,
|
|
16
|
+
});
|
|
17
|
+
index.add({
|
|
18
|
+
source: resolve(node.package.root, 'schema.json'),
|
|
19
|
+
role: 'schema',
|
|
20
|
+
owner: node.id,
|
|
21
|
+
});
|
|
22
|
+
index.add({
|
|
23
|
+
source: resolve(node.package.root, 'package.json'),
|
|
24
|
+
role: 'manifest',
|
|
25
|
+
owner: node.id,
|
|
26
|
+
});
|
|
27
|
+
for (const requirement of node.features) {
|
|
28
|
+
const packageRoot = resolve(requirement.package.root);
|
|
29
|
+
index.addPackageRoot(packageRoot, node.id);
|
|
30
|
+
index.add({
|
|
31
|
+
source: resolve(packageRoot, 'package.json'),
|
|
32
|
+
role: 'manifest',
|
|
33
|
+
owner: node.id,
|
|
34
|
+
feature: featureIdsByPackageRoot.get(packageRoot),
|
|
35
|
+
});
|
|
36
|
+
index.add({
|
|
37
|
+
source: resolve(packageRoot, requirement.package.packageJson.zhin.entry),
|
|
38
|
+
role: 'feature',
|
|
39
|
+
owner: node.id,
|
|
40
|
+
feature: featureIdsByPackageRoot.get(packageRoot),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
for (const slot of snapshot.capabilities.values()) {
|
|
45
|
+
index.add({
|
|
46
|
+
source: slot.source,
|
|
47
|
+
role: 'capability',
|
|
48
|
+
owner: slot.owner,
|
|
49
|
+
capability: slot.id,
|
|
50
|
+
feature: slot.feature,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return index;
|
|
54
|
+
}
|
|
55
|
+
add(record) {
|
|
56
|
+
const source = normalizeSource(record.source);
|
|
57
|
+
const records = this.#records.get(source) ?? [];
|
|
58
|
+
if (!records.some((item) => sameRecord(item, record))) {
|
|
59
|
+
records.push(Object.freeze({ ...record, source }));
|
|
60
|
+
this.#records.set(source, records);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
addPackageRoot(packageRoot, owner) {
|
|
64
|
+
const normalized = normalizeSource(packageRoot);
|
|
65
|
+
if (!this.#packages.some((item) => item.packageRoot === normalized && item.owner === owner)) {
|
|
66
|
+
this.#packages.push(Object.freeze({ packageRoot: normalized, owner }));
|
|
67
|
+
this.#packages.sort((left, right) => right.packageRoot.length - left.packageRoot.length);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
recordsFor(source) {
|
|
71
|
+
return Object.freeze([...(this.#records.get(normalizeSource(source)) ?? [])]);
|
|
72
|
+
}
|
|
73
|
+
ownersForPath(source) {
|
|
74
|
+
const normalized = normalizeSource(source);
|
|
75
|
+
let longest = -1;
|
|
76
|
+
const owners = new Set();
|
|
77
|
+
for (const item of this.#packages) {
|
|
78
|
+
if (!contains(item.packageRoot, normalized))
|
|
79
|
+
continue;
|
|
80
|
+
if (item.packageRoot.length < longest)
|
|
81
|
+
break;
|
|
82
|
+
longest = item.packageRoot.length;
|
|
83
|
+
owners.add(item.owner);
|
|
84
|
+
}
|
|
85
|
+
return Object.freeze([...owners]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function visitPlugin(node, visit) {
|
|
89
|
+
visit(node);
|
|
90
|
+
for (const child of node.children)
|
|
91
|
+
visitPlugin(child, visit);
|
|
92
|
+
}
|
|
93
|
+
function normalizeSource(source) {
|
|
94
|
+
return resolve(source);
|
|
95
|
+
}
|
|
96
|
+
function contains(packageRoot, source) {
|
|
97
|
+
const path = relative(packageRoot, source);
|
|
98
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
99
|
+
}
|
|
100
|
+
function sameRecord(left, right) {
|
|
101
|
+
return (left.role === right.role &&
|
|
102
|
+
left.owner === right.owner &&
|
|
103
|
+
left.capability === right.capability &&
|
|
104
|
+
left.feature === right.feature);
|
|
105
|
+
}
|