@zhin.js/runtime 1.1.8 → 1.1.10
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/README.md +20 -4
- package/lib/application-plugin-declaration.d.ts +17 -0
- package/lib/application-plugin-declaration.js +69 -0
- package/lib/config-composer.d.ts +2408 -167
- package/lib/config-composer.js +61 -20
- package/lib/config-patch-planner.d.ts +3 -2
- package/lib/config-patch-planner.js +7 -5
- package/lib/editor-temp-file.d.ts +12 -0
- package/lib/editor-temp-file.js +44 -0
- package/lib/hmr-coordinator.js +4 -0
- package/lib/host-config-schema.json +3843 -140
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/manifest.d.ts +14 -0
- package/lib/manifest.js +64 -4
- package/lib/native-development-runtime.js +5 -0
- package/lib/plugin-config-schema.d.ts +10 -0
- package/lib/plugin-config-schema.js +25 -2
- package/lib/plugin-scope-assembler.d.ts +3 -3
- package/lib/plugin-scope-assembler.js +18 -8
- package/lib/primary-config.d.ts +6 -2
- package/lib/primary-config.js +9 -1
- package/lib/project-graph.d.ts +2 -2
- package/lib/project-graph.js +18 -17
- package/lib/root-runtime.d.ts +2 -0
- package/lib/root-runtime.js +138 -44
- package/lib/runtime-input-planner.d.ts +3 -1
- package/lib/runtime-input-planner.js +7 -4
- package/lib/slot-generation-preparer.d.ts +3 -1
- package/lib/slot-generation-preparer.js +7 -2
- package/lib/topology-generation-preparer.js +25 -3
- package/lib/topology-transaction.js +3 -1
- package/package.json +15 -15
package/lib/root-runtime.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { normalizeSourcePath } from './source-path.js';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
-
import { DisposeStack, GenerationHandoffStack, RootController, ConfigDocumentDivergenceError, createSnapshotView, rootPluginId, } from '@zhin.js/plugin-runtime';
|
|
4
|
+
import { DisposeStack, GenerationHandoffStack, RootController, applyConfigPatches, ConfigDocumentDivergenceError, createSnapshotView, rootPluginId, } from '@zhin.js/plugin-runtime';
|
|
5
5
|
import { FeatureCatalog, FeatureDiscovery, } from '@zhin.js/feature-kit';
|
|
6
|
-
import { ConfigComposer } from './config-composer.js';
|
|
6
|
+
import { ConfigComposer, HOST_CONFIG_KEYS } from './config-composer.js';
|
|
7
|
+
import { createPrimaryConfig, primaryConfigToken } from './primary-config.js';
|
|
7
8
|
import { immutableConfigSchema } from './plugin-config-schema.js';
|
|
8
9
|
import { ConfigPatchPlanner } from './config-patch-planner.js';
|
|
9
10
|
import { defineRuntimeEnvironment } from './environment.js';
|
|
@@ -25,6 +26,7 @@ import { SubtreeGenerationPreparer, SubtreeTopologyChangedError, } from './subtr
|
|
|
25
26
|
import { TopologyGenerationPreparer } from './topology-generation-preparer.js';
|
|
26
27
|
import { RestartBoundaryPlanner } from './restart-boundary.js';
|
|
27
28
|
import { RuntimeInputPlanner } from './runtime-input-planner.js';
|
|
29
|
+
import { TopologyTransactionPlanner } from './topology-transaction.js';
|
|
28
30
|
export class RootRuntime {
|
|
29
31
|
#projectRoot;
|
|
30
32
|
#modules;
|
|
@@ -38,7 +40,7 @@ export class RootRuntime {
|
|
|
38
40
|
#configDocument;
|
|
39
41
|
#installResources;
|
|
40
42
|
#isolation;
|
|
41
|
-
#
|
|
43
|
+
#controlTail = Promise.resolve();
|
|
42
44
|
#stopResult;
|
|
43
45
|
#initialConfigSchemas = new Map();
|
|
44
46
|
#resolveConfigurationReady;
|
|
@@ -56,8 +58,13 @@ export class RootRuntime {
|
|
|
56
58
|
this.#environment = defineRuntimeEnvironment(options.environment);
|
|
57
59
|
this.#environmentLayers = defineEnvironmentLayers(options.environmentVariables);
|
|
58
60
|
this.#environmentSource = options.environmentSource;
|
|
59
|
-
if (typeof options.config
|
|
61
|
+
if (options.application !== undefined && typeof options.config !== 'function') {
|
|
62
|
+
throw new TypeError('application is only used with a custom config resolver; pass the v2 document as config otherwise');
|
|
63
|
+
}
|
|
64
|
+
if (typeof options.config === 'function') {
|
|
60
65
|
this.#configResolver = options.config;
|
|
66
|
+
this.#configDocument = structuredClone(options.application ?? {});
|
|
67
|
+
}
|
|
61
68
|
else if (isConfigDocumentPort(options.config))
|
|
62
69
|
this.#configPort = options.config;
|
|
63
70
|
else
|
|
@@ -94,7 +101,10 @@ export class RootRuntime {
|
|
|
94
101
|
get sourceOwnership() {
|
|
95
102
|
return this.#controller.committed.state.ownership;
|
|
96
103
|
}
|
|
97
|
-
|
|
104
|
+
start() {
|
|
105
|
+
return this.#enqueueControl(() => this.#start());
|
|
106
|
+
}
|
|
107
|
+
async #start() {
|
|
98
108
|
try {
|
|
99
109
|
if (this.#environmentSource) {
|
|
100
110
|
this.#environmentLayers = defineEnvironmentLayers(await this.#environmentSource.read());
|
|
@@ -115,15 +125,24 @@ export class RootRuntime {
|
|
|
115
125
|
}
|
|
116
126
|
}
|
|
117
127
|
async reload(target = rootPluginId()) {
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
return this.#enqueueControl(async () => {
|
|
129
|
+
const previousInputs = this.#runtimeInputState();
|
|
130
|
+
return this.#controller.reload(target, async (current, signal) => {
|
|
131
|
+
return (await this.#prepare(current, signal)).generation;
|
|
132
|
+
}).catch(error => {
|
|
133
|
+
this.#restoreRuntimeInputs(previousInputs);
|
|
134
|
+
throw error;
|
|
135
|
+
});
|
|
120
136
|
});
|
|
121
|
-
return snapshot;
|
|
122
137
|
}
|
|
123
138
|
patchConfig(patches) {
|
|
124
139
|
const operations = cloneConfigPatches(patches);
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
return this.#enqueueControl(() => this.#applyConfigPatches(operations));
|
|
141
|
+
}
|
|
142
|
+
/** Serializes source reads and input adoption around the Controller's generation transaction. */
|
|
143
|
+
#enqueueControl(operation) {
|
|
144
|
+
const result = this.#controlTail.then(operation, operation);
|
|
145
|
+
this.#controlTail = result.catch(() => undefined);
|
|
127
146
|
return result;
|
|
128
147
|
}
|
|
129
148
|
createHmrCoordinator(options) {
|
|
@@ -159,10 +178,13 @@ export class RootRuntime {
|
|
|
159
178
|
return result;
|
|
160
179
|
}
|
|
161
180
|
async #reloadPlan(plan) {
|
|
181
|
+
return this.#enqueueControl(() => this.#reloadPlanInQueue(plan));
|
|
182
|
+
}
|
|
183
|
+
async #reloadPlanInQueue(plan) {
|
|
162
184
|
const inputs = await this.#planRuntimeInputs(plan.changed);
|
|
163
185
|
if (inputs && 'kind' in inputs)
|
|
164
186
|
return inputs;
|
|
165
|
-
const previousInputs =
|
|
187
|
+
const previousInputs = this.#runtimeInputState();
|
|
166
188
|
const effectiveInputPlan = inputs
|
|
167
189
|
? Object.freeze({
|
|
168
190
|
...plan,
|
|
@@ -179,10 +201,12 @@ export class RootRuntime {
|
|
|
179
201
|
&& effective.manifestSources.length === 0
|
|
180
202
|
&& effective.subtrees.length === 0
|
|
181
203
|
&& effective.slots.length === 0
|
|
182
|
-
&& resolved.capabilities.size === 0
|
|
204
|
+
&& resolved.capabilities.size === 0
|
|
205
|
+
&& !inputs.topologyChanged
|
|
206
|
+
&& !inputs.primaryConfigChanged)
|
|
183
207
|
return undefined;
|
|
184
|
-
if (this.#model && effective.manifestSources.length > 0) {
|
|
185
|
-
const inspected = await this.#inspectProject(inputs?.document, inputs?.environmentLayers);
|
|
208
|
+
if (this.#model && (effective.manifestSources.length > 0 || inputs?.topologyChanged)) {
|
|
209
|
+
const inspected = inputs?.inspected ?? await this.#inspectProject(inputs?.document, inputs?.environmentLayers);
|
|
186
210
|
restart = new RestartBoundaryPlanner().plan(this.#model.graph, inspected.graph, effective.changed);
|
|
187
211
|
if (restart)
|
|
188
212
|
return undefined;
|
|
@@ -191,13 +215,18 @@ export class RootRuntime {
|
|
|
191
215
|
: await new TopologyGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, inspected.environmentLayers, this.#isolation).prepare(current, signal, { ...effective, capabilities: resolved.capabilities });
|
|
192
216
|
}
|
|
193
217
|
else if (effective.subtrees.length === 0 && resolved.capabilities.size > 0 && this.#model) {
|
|
194
|
-
prepared = await new SlotGenerationPreparer(this.#modules, this.#model
|
|
218
|
+
prepared = await new SlotGenerationPreparer(this.#modules, this.#model, inputs?.primaryConfigChanged
|
|
219
|
+
? createPrimaryConfig(inputs.inspected.primaryConfigDocument, createEnvStore(rootPluginId(), this.#environment, inputs.environmentLayers), inputs.inspected.configResolver.compositionMode)
|
|
220
|
+
: undefined)
|
|
195
221
|
.prepare(current, resolved.capabilities, signal);
|
|
196
222
|
}
|
|
197
223
|
else if (this.#model && this.#canPrepareSubtrees(effective)) {
|
|
198
|
-
const inspected = await this.#inspectProject(inputs?.document, inputs?.environmentLayers);
|
|
224
|
+
const inspected = inputs?.inspected ?? await this.#inspectProject(inputs?.document, inputs?.environmentLayers);
|
|
199
225
|
prepared = await this.#prepareSubtrees(current, inspected, effective.subtrees, signal);
|
|
200
226
|
}
|
|
227
|
+
else if (this.#model && inputs?.primaryConfigChanged && effective.subtrees.length === 0 && resolved.capabilities.size === 0) {
|
|
228
|
+
prepared = await this.#prepareSubtrees(current, inputs.inspected, [], signal);
|
|
229
|
+
}
|
|
201
230
|
else {
|
|
202
231
|
prepared = await this.#prepare(current, signal, inputs?.document, inputs?.environmentLayers);
|
|
203
232
|
}
|
|
@@ -206,6 +235,9 @@ export class RootRuntime {
|
|
|
206
235
|
return inputs && previousInputs
|
|
207
236
|
? withRuntimeInputHandoff(prepared.generation, () => this.#adoptRuntimeInputs(inputs), () => this.#restoreRuntimeInputs(previousInputs))
|
|
208
237
|
: prepared.generation;
|
|
238
|
+
}).catch(error => {
|
|
239
|
+
this.#restoreRuntimeInputs(previousInputs);
|
|
240
|
+
throw error;
|
|
209
241
|
});
|
|
210
242
|
if (restart)
|
|
211
243
|
return restart;
|
|
@@ -227,8 +259,10 @@ export class RootRuntime {
|
|
|
227
259
|
const environmentLayers = environmentChanged && this.#environmentSource
|
|
228
260
|
? defineEnvironmentLayers(await this.#environmentSource.read())
|
|
229
261
|
: this.#environmentLayers;
|
|
230
|
-
const
|
|
262
|
+
const inspected = await this.#inspectProject(document, environmentLayers, configSnapshot?.pluginOrder ?? this.#configSnapshot?.pluginOrder);
|
|
263
|
+
const inputPlan = await new RuntimeInputPlanner(new ConfigComposer(this.#disabledPluginInstanceKeys, this.#modules), this.#committedConfigComposer()).plan({
|
|
231
264
|
graph: this.#model.graph,
|
|
265
|
+
nextGraph: inspected.graph,
|
|
232
266
|
environment: this.#environment,
|
|
233
267
|
previousDocument: requireConfigDocument(this.#configDocument),
|
|
234
268
|
nextDocument: document,
|
|
@@ -248,6 +282,9 @@ export class RootRuntime {
|
|
|
248
282
|
environmentLayers,
|
|
249
283
|
inputSources,
|
|
250
284
|
roots: inputPlan.roots,
|
|
285
|
+
inspected,
|
|
286
|
+
topologyChanged: new TopologyTransactionPlanner().plan(this.#model.graph, inspected.graph).changed,
|
|
287
|
+
primaryConfigChanged: this.#primaryConfigChanged(inspected.primaryConfigDocument),
|
|
251
288
|
});
|
|
252
289
|
}
|
|
253
290
|
#adoptRuntimeInputs(candidate) {
|
|
@@ -303,14 +340,14 @@ export class RootRuntime {
|
|
|
303
340
|
signal.throwIfAborted();
|
|
304
341
|
return this.#prepareInspected(current, inspected, signal);
|
|
305
342
|
}
|
|
306
|
-
async #inspectProject(document, environmentLayers = this.#environmentLayers) {
|
|
307
|
-
const resolver = await NodePackageResolver.create(this.#projectRoot);
|
|
308
|
-
const graph = await new ProjectGraphService(resolver, {
|
|
309
|
-
disabledPluginInstanceKeys: this.#disabledPluginInstanceKeys,
|
|
310
|
-
}).inspect(this.#projectRoot);
|
|
343
|
+
async #inspectProject(document, environmentLayers = this.#environmentLayers, pluginOrder) {
|
|
311
344
|
if (document === undefined)
|
|
312
345
|
await this.#refreshConfigDocument();
|
|
313
346
|
const activeDocument = document ?? this.#configDocument;
|
|
347
|
+
const resolver = await NodePackageResolver.create(this.#projectRoot);
|
|
348
|
+
const graph = await new ProjectGraphService(resolver, {
|
|
349
|
+
disabledPluginInstanceKeys: this.#disabledPluginInstanceKeys,
|
|
350
|
+
}).inspect(this.#projectRoot, activeDocument, pluginOrder ?? this.#configSnapshot?.pluginOrder);
|
|
314
351
|
if (this.#configResolver) {
|
|
315
352
|
const customResolver = this.#configResolver;
|
|
316
353
|
const schemas = new Map(graphNodes(graph.root).flatMap(node => {
|
|
@@ -319,6 +356,7 @@ export class RootRuntime {
|
|
|
319
356
|
}));
|
|
320
357
|
const configResolver = node => customResolver(node);
|
|
321
358
|
configResolver.schema = node => schemas.get(node.id);
|
|
359
|
+
configResolver.compositionMode = 'custom';
|
|
322
360
|
if (this.snapshot.generation === 0) {
|
|
323
361
|
this.#initialConfigSchemas = new Map(graphNodes(graph.root).flatMap(node => {
|
|
324
362
|
const schema = schemas.get(node.id);
|
|
@@ -329,7 +367,7 @@ export class RootRuntime {
|
|
|
329
367
|
return {
|
|
330
368
|
graph,
|
|
331
369
|
configResolver,
|
|
332
|
-
primaryConfigDocument: Object.freeze({}),
|
|
370
|
+
primaryConfigDocument: Object.freeze(structuredClone(activeDocument ?? {})),
|
|
333
371
|
environmentLayers,
|
|
334
372
|
};
|
|
335
373
|
}
|
|
@@ -374,14 +412,26 @@ export class RootRuntime {
|
|
|
374
412
|
.expandMissingAsEmpty(view);
|
|
375
413
|
};
|
|
376
414
|
resolver.schema = (node) => schemas?.get(node.id);
|
|
415
|
+
resolver.compositionMode = 'document';
|
|
377
416
|
return resolver;
|
|
378
417
|
}
|
|
418
|
+
#committedConfigComposer() {
|
|
419
|
+
return new ConfigComposer([], undefined, new Map([...this.snapshot.tree.values()].flatMap(node => node.configSchema ? [[node.id, node.configSchema]] : [])));
|
|
420
|
+
}
|
|
421
|
+
#committedPrimaryConfig() {
|
|
422
|
+
return this.snapshot.resources.get(rootPluginId())?.get(primaryConfigToken.id);
|
|
423
|
+
}
|
|
424
|
+
#primaryConfigChanged(document) {
|
|
425
|
+
return !isDeepStrictEqual(this.#committedPrimaryConfig()?.document, document);
|
|
426
|
+
}
|
|
379
427
|
async #applyConfigPatches(patches) {
|
|
380
|
-
if (!this.#configDocument) {
|
|
428
|
+
if (this.#configResolver || !this.#configDocument) {
|
|
381
429
|
throw new Error('Config patches require a document-backed RootRuntime config');
|
|
382
430
|
}
|
|
383
431
|
// Adopt any external edit before planning so the port's revision check
|
|
384
432
|
// cannot conflict and the patch applies on top of the on-disk document.
|
|
433
|
+
const previousInputs = this.#runtimeInputState();
|
|
434
|
+
const previousDocument = this.#configDocument;
|
|
385
435
|
await this.#refreshConfigDocument();
|
|
386
436
|
const currentDocument = requireConfigDocument(this.#configDocument);
|
|
387
437
|
let plan;
|
|
@@ -390,31 +440,34 @@ export class RootRuntime {
|
|
|
390
440
|
const snapshot = await this.#controller.reload(rootPluginId(), async (current, signal) => {
|
|
391
441
|
let prepared;
|
|
392
442
|
signal.throwIfAborted();
|
|
393
|
-
const resolver = await NodePackageResolver.create(this.#projectRoot);
|
|
394
|
-
const graph = await new ProjectGraphService(resolver, {
|
|
395
|
-
disabledPluginInstanceKeys: this.#disabledPluginInstanceKeys,
|
|
396
|
-
}).inspect(this.#projectRoot);
|
|
397
|
-
const planned = await new ConfigPatchPlanner(new ConfigComposer(this.#disabledPluginInstanceKeys, this.#modules)).plan(graph, currentDocument, patches);
|
|
398
|
-
plan = planned;
|
|
399
|
-
if (!planned.documentChanged)
|
|
400
|
-
return undefined;
|
|
401
443
|
try {
|
|
402
|
-
|
|
444
|
+
const candidate = applyConfigPatches(currentDocument, patches);
|
|
445
|
+
const editsDeclarations = patches.some(patch => patch.path.length === 0 || patch.path[0] === 'plugins');
|
|
446
|
+
if (this.#configPort && (!isDeepStrictEqual(currentDocument, candidate) || editsDeclarations)) {
|
|
403
447
|
const currentSnapshot = requireConfigDocumentSnapshot(this.#configSnapshot);
|
|
404
448
|
// Port preparation must remain inert; validation and shadow setup can
|
|
405
449
|
// still reject this candidate without touching the backing document.
|
|
406
450
|
documentTransaction = await this.#configPort.prepare(currentSnapshot, patches);
|
|
407
|
-
if (!isDeepStrictEqual(documentTransaction.document,
|
|
451
|
+
if (!isDeepStrictEqual(documentTransaction.document, candidate)) {
|
|
408
452
|
throw new ConfigDocumentDivergenceError();
|
|
409
453
|
}
|
|
410
454
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
455
|
+
const resolver = await NodePackageResolver.create(this.#projectRoot);
|
|
456
|
+
const graphService = new ProjectGraphService(resolver, {
|
|
457
|
+
disabledPluginInstanceKeys: this.#disabledPluginInstanceKeys,
|
|
458
|
+
});
|
|
459
|
+
const previousGraph = this.#model?.graph ?? await graphService.inspect(this.#projectRoot, previousDocument);
|
|
460
|
+
const pluginOrder = documentTransaction?.pluginOrder
|
|
461
|
+
?? patchedPluginOrder(this.#configSnapshot?.pluginOrder, candidate, patches);
|
|
462
|
+
const graph = await graphService.inspect(this.#projectRoot, candidate, pluginOrder);
|
|
463
|
+
const planned = await new ConfigPatchPlanner(new ConfigComposer(this.#disabledPluginInstanceKeys, this.#modules), this.#committedConfigComposer()).plan(previousGraph, currentDocument, patches, graph, previousDocument);
|
|
464
|
+
plan = planned;
|
|
465
|
+
const topologyChanged = this.#model
|
|
466
|
+
? new TopologyTransactionPlanner().plan(this.#model.graph, graph).changed : false;
|
|
467
|
+
const orderChanged = !isDeepStrictEqual(this.#configSnapshot?.pluginOrder, pluginOrder);
|
|
468
|
+
if (!planned.documentChanged && !topologyChanged && !orderChanged && isDeepStrictEqual(previousDocument, candidate)) {
|
|
416
469
|
if (documentTransaction)
|
|
417
|
-
|
|
470
|
+
await documentTransaction.rollback();
|
|
418
471
|
return undefined;
|
|
419
472
|
}
|
|
420
473
|
const inspected = {
|
|
@@ -423,15 +476,30 @@ export class RootRuntime {
|
|
|
423
476
|
primaryConfigDocument: planned.document,
|
|
424
477
|
environmentLayers: this.#environmentLayers,
|
|
425
478
|
};
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
479
|
+
if (!this.#primaryConfigChanged(planned.document) && planned.roots.length === 0 && !topologyChanged) {
|
|
480
|
+
if (documentTransaction)
|
|
481
|
+
committedDocument = await documentTransaction.commit();
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
const previousPrimary = this.#committedPrimaryConfig()?.document;
|
|
485
|
+
const hostChanged = HOST_CONFIG_KEYS.some(key => !isDeepStrictEqual(previousPrimary?.[key], planned.document[key]));
|
|
486
|
+
const retainRoot = !this.#installResources || !hostChanged;
|
|
487
|
+
// Host sections require resource reinstallation. Other edits publish a
|
|
488
|
+
// current PrimaryConfig projection while retaining Root lifetimes.
|
|
489
|
+
if (this.#model && retainRoot && topologyChanged && !planned.roots.includes(rootPluginId())) {
|
|
490
|
+
prepared = await new TopologyGenerationPreparer(this.#modules, this.#model, graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current, signal);
|
|
491
|
+
}
|
|
492
|
+
else if (this.#model && retainRoot && !planned.roots.includes(rootPluginId())) {
|
|
430
493
|
prepared = await this.#prepareSubtrees(current, inspected, planned.roots, signal);
|
|
431
494
|
}
|
|
432
495
|
else {
|
|
433
496
|
prepared = await this.#prepareInspected(current, inspected, signal);
|
|
434
497
|
}
|
|
498
|
+
if (!prepared) {
|
|
499
|
+
if (documentTransaction)
|
|
500
|
+
committedDocument = await documentTransaction.commit();
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
435
503
|
return documentTransaction
|
|
436
504
|
? withConfigDocumentHandoff(prepared.generation, documentTransaction, (committed) => { committedDocument = committed; })
|
|
437
505
|
: prepared.generation;
|
|
@@ -444,6 +512,9 @@ export class RootRuntime {
|
|
|
444
512
|
await documentTransaction.rollback().catch(() => undefined);
|
|
445
513
|
throw error;
|
|
446
514
|
}
|
|
515
|
+
}).catch(error => {
|
|
516
|
+
this.#restoreRuntimeInputs(previousInputs);
|
|
517
|
+
throw error;
|
|
447
518
|
});
|
|
448
519
|
const completed = requireConfigPatchPlan(plan);
|
|
449
520
|
this.#configDocument = completed.candidate;
|
|
@@ -472,6 +543,29 @@ function collapseInvalidationSubtrees(values) {
|
|
|
472
543
|
const sorted = [...new Set(values)].sort((left, right) => left.length - right.length);
|
|
473
544
|
return Object.freeze(sorted.filter((candidate, index) => !sorted.slice(0, index).some((root) => isWithinInvalidationRoot(candidate, root))));
|
|
474
545
|
}
|
|
546
|
+
/** Preserve known source ordering when a structural port omits optional write metadata. */
|
|
547
|
+
function patchedPluginOrder(previous, candidate, patches) {
|
|
548
|
+
if (!previous)
|
|
549
|
+
return undefined;
|
|
550
|
+
let order = [...previous];
|
|
551
|
+
for (const patch of patches) {
|
|
552
|
+
if (patch.path.length === 0 || (patch.path[0] === 'plugins' && patch.path.length === 1)) {
|
|
553
|
+
const value = patch.path.length === 0 && patch.op === 'set'
|
|
554
|
+
? patch.value?.plugins : patch.op === 'set' ? patch.value : {};
|
|
555
|
+
order = value && typeof value === 'object' ? Object.keys(value) : [];
|
|
556
|
+
}
|
|
557
|
+
else if (patch.path[0] === 'plugins' && patch.path[1]) {
|
|
558
|
+
const key = patch.path[1];
|
|
559
|
+
if (patch.op === 'remove')
|
|
560
|
+
order = order.filter(alias => alias !== key);
|
|
561
|
+
else if (!order.includes(key))
|
|
562
|
+
order.push(key);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const plugins = candidate.plugins;
|
|
566
|
+
const keys = plugins && typeof plugins === 'object' ? Object.keys(plugins) : [];
|
|
567
|
+
return Object.freeze([...order.filter(key => keys.includes(key)), ...keys.filter(key => !order.includes(key))]);
|
|
568
|
+
}
|
|
475
569
|
function isWithinInvalidationRoot(plugin, root) {
|
|
476
570
|
return plugin === root || plugin.startsWith(`${root}/`);
|
|
477
571
|
}
|
|
@@ -12,9 +12,11 @@ export interface RuntimeInputPlan {
|
|
|
12
12
|
/** Classifies config/environment changes by their actual expanded runtime projection. */
|
|
13
13
|
export declare class RuntimeInputPlanner {
|
|
14
14
|
private readonly composer;
|
|
15
|
-
|
|
15
|
+
private readonly previousComposer;
|
|
16
|
+
constructor(composer?: ConfigComposer, previousComposer?: ConfigComposer);
|
|
16
17
|
plan(options: {
|
|
17
18
|
readonly graph: ProjectGraph;
|
|
19
|
+
readonly nextGraph?: ProjectGraph;
|
|
18
20
|
readonly environment: RuntimeEnvironment;
|
|
19
21
|
readonly previousDocument: RuntimeConfigDocument;
|
|
20
22
|
readonly nextDocument: RuntimeConfigDocument;
|
|
@@ -5,15 +5,18 @@ import { createEnvStore, } from './environment-store.js';
|
|
|
5
5
|
/** Classifies config/environment changes by their actual expanded runtime projection. */
|
|
6
6
|
export class RuntimeInputPlanner {
|
|
7
7
|
composer;
|
|
8
|
-
|
|
8
|
+
previousComposer;
|
|
9
|
+
constructor(composer = new ConfigComposer(), previousComposer = composer) {
|
|
9
10
|
this.composer = composer;
|
|
11
|
+
this.previousComposer = previousComposer;
|
|
10
12
|
}
|
|
11
13
|
async plan(options) {
|
|
12
|
-
const previous = await this.
|
|
13
|
-
const
|
|
14
|
+
const previous = await this.previousComposer.compose(options.graph, options.previousDocument);
|
|
15
|
+
const nextGraph = options.nextGraph ?? options.graph;
|
|
16
|
+
const next = await this.composer.compose(nextGraph, options.nextDocument);
|
|
14
17
|
const environmentChanged = !isDeepStrictEqual(options.previousEnvironment, options.nextEnvironment);
|
|
15
18
|
const hostKeys = HOST_CONFIG_KEYS.filter((key) => !isDeepStrictEqual(expandHostValue(previous.document[key], options.environment, options.previousEnvironment), expandHostValue(next.document[key], options.environment, options.nextEnvironment)));
|
|
16
|
-
const changed = indexGraph(
|
|
19
|
+
const changed = indexGraph(nextGraph)
|
|
17
20
|
.filter((node) => !isDeepStrictEqual(expandPluginView(node.id, previous.views.get(node.id), options.environment, options.previousEnvironment), expandPluginView(node.id, next.views.get(node.id), options.environment, options.nextEnvironment)))
|
|
18
21
|
.map((node) => node.id);
|
|
19
22
|
// EnvStore is itself a public Plugin resource. A changed global layer can
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type CapabilityId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import type { ModuleRuntime } from './module-runtime.js';
|
|
3
3
|
import { type PreparedRuntimeGeneration, type RuntimeGenerationModel } from './runtime-generation.js';
|
|
4
|
+
import { type PrimaryConfig } from './primary-config.js';
|
|
4
5
|
import { type CapabilityDelta } from './convention-capability-delta.js';
|
|
5
6
|
export declare class SlotGenerationPreparer {
|
|
6
7
|
private readonly modules;
|
|
7
8
|
private readonly model;
|
|
8
|
-
|
|
9
|
+
private readonly primaryConfig?;
|
|
10
|
+
constructor(modules: ModuleRuntime, model: RuntimeGenerationModel, primaryConfig?: PrimaryConfig | undefined);
|
|
9
11
|
prepare(current: RuntimeSnapshot, selected: readonly CapabilityId[] | CapabilityDelta, signal: AbortSignal): Promise<PreparedRuntimeGeneration>;
|
|
10
12
|
}
|
|
@@ -4,13 +4,16 @@ import { FeatureProjector, composeGenerationHandoffs } from './feature-projector
|
|
|
4
4
|
import { NodeDiscoveryHost } from './node-discovery-host.js';
|
|
5
5
|
import { prepareRuntimeGeneration, } from './runtime-generation.js';
|
|
6
6
|
import { SourceOwnershipIndex } from './source-ownership.js';
|
|
7
|
+
import { projectPrimaryConfigResources } from './primary-config.js';
|
|
7
8
|
import { capabilityDeltaFromSlots, } from './convention-capability-delta.js';
|
|
8
9
|
export class SlotGenerationPreparer {
|
|
9
10
|
modules;
|
|
10
11
|
model;
|
|
11
|
-
|
|
12
|
+
primaryConfig;
|
|
13
|
+
constructor(modules, model, primaryConfig) {
|
|
12
14
|
this.modules = modules;
|
|
13
15
|
this.model = model;
|
|
16
|
+
this.primaryConfig = primaryConfig;
|
|
14
17
|
}
|
|
15
18
|
async prepare(current, selected, signal) {
|
|
16
19
|
signal.throwIfAborted();
|
|
@@ -38,7 +41,9 @@ export class SlotGenerationPreparer {
|
|
|
38
41
|
root: current.root,
|
|
39
42
|
tree: current.tree,
|
|
40
43
|
config: current.config,
|
|
41
|
-
resources:
|
|
44
|
+
resources: this.primaryConfig
|
|
45
|
+
? projectPrimaryConfigResources(current.resources, this.primaryConfig)
|
|
46
|
+
: current.resources,
|
|
42
47
|
capabilities,
|
|
43
48
|
}, signal, current.projections);
|
|
44
49
|
try {
|
|
@@ -78,13 +78,19 @@ export class TopologyGenerationPreparer {
|
|
|
78
78
|
plugins.synchronizeTree(this.graph.root);
|
|
79
79
|
const capabilities = await this.#prepareCapabilities(current, plan, setupRoots, featureTopology, selected);
|
|
80
80
|
mergeSetupCapabilities(capabilities, plugins.setupCapabilities(), featureTopology.providers, featureTopology.rootsByFeature);
|
|
81
|
-
const
|
|
81
|
+
const providers = [...featureTopology.providers.values()].filter(provider => provider.runtime.retain !== 'when-owner-unchanged'
|
|
82
|
+
|| this.model.providers.get(provider.id) !== provider
|
|
83
|
+
|| !current.projections.has(provider.id)
|
|
84
|
+
|| !sameFeatureOwners(this.model.rootsByFeature.get(provider.id) ?? [], featureTopology.rootsByFeature.get(provider.id) ?? [], this.model.scopes, plugins.scopes)
|
|
85
|
+
|| !sameFeatureSlots(provider.id, current.capabilities, capabilities));
|
|
86
|
+
const retained = new Map([...current.projections].filter(([id]) => featureTopology.providers.has(id)));
|
|
87
|
+
const projected = await new FeatureProjector(providers).project(current.generation + 1, {
|
|
82
88
|
root: current.root,
|
|
83
89
|
tree: plugins.tree,
|
|
84
90
|
config: plugins.config,
|
|
85
91
|
resources: plugins.resources,
|
|
86
92
|
capabilities,
|
|
87
|
-
}, signal);
|
|
93
|
+
}, signal, retained);
|
|
88
94
|
for (const [feature, dispose] of projected.disposers) {
|
|
89
95
|
projectionDisposers.set(feature, dispose);
|
|
90
96
|
}
|
|
@@ -92,7 +98,10 @@ export class TopologyGenerationPreparer {
|
|
|
92
98
|
plugins.bindCandidateSnapshot(snapshot);
|
|
93
99
|
const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, featureTopology.featureIdsByPackageRoot);
|
|
94
100
|
const replacements = new Map(plugins.createdScopeDisposers());
|
|
95
|
-
const assets = this.model.assets.replaceScopes(graphOrder(this.graph), replacements, new Set([
|
|
101
|
+
const assets = this.model.assets.replaceScopes(graphOrder(this.graph), replacements, new Set([
|
|
102
|
+
...providers.map(provider => provider.id),
|
|
103
|
+
...[...this.model.providers.keys()].filter(id => !featureTopology.providers.has(id)),
|
|
104
|
+
]), projectionDisposers);
|
|
96
105
|
return prepareRuntimeGeneration({
|
|
97
106
|
snapshot: projected.state,
|
|
98
107
|
dispose: () => assets.dispose(),
|
|
@@ -276,6 +285,19 @@ function selectedSlotsByFeature(current, selected, refreshed) {
|
|
|
276
285
|
function sameValues(left, right) {
|
|
277
286
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
278
287
|
}
|
|
288
|
+
function sameFeatureSlots(feature, previous, next) {
|
|
289
|
+
const before = [...previous.values()].filter(slot => slot.feature === feature);
|
|
290
|
+
const after = [...next.values()].filter(slot => slot.feature === feature);
|
|
291
|
+
return before.length === after.length && before.every(slot => next.get(slot.id) === slot);
|
|
292
|
+
}
|
|
293
|
+
function sameFeatureOwners(previous, next, previousScopes, nextScopes) {
|
|
294
|
+
return previous.length === next.length && previous.every((root, index) => {
|
|
295
|
+
const candidate = next[index];
|
|
296
|
+
return candidate?.owner === root.owner
|
|
297
|
+
&& candidate.packageRoot === root.packageRoot
|
|
298
|
+
&& previousScopes.get(root.owner) === nextScopes.get(root.owner);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
279
301
|
async function rollback(scopeDisposers, projectionDisposers, prepareError) {
|
|
280
302
|
const disposers = new DisposeStack();
|
|
281
303
|
for (const dispose of scopeDisposers)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
3
|
/** Compiles two resolved graphs into the smallest safe topology transaction. */
|
|
3
4
|
export class TopologyTransactionPlanner {
|
|
4
5
|
plan(previous, next) {
|
|
@@ -90,7 +91,8 @@ function scopeContractChanged(previous, next) {
|
|
|
90
91
|
|| previous.package.name !== next.package.name
|
|
91
92
|
|| previousManifest.entry !== nextManifest.entry
|
|
92
93
|
|| previousManifest.runtime !== nextManifest.runtime
|
|
93
|
-
|| previousManifest.engine !== nextManifest.engine
|
|
94
|
+
|| previousManifest.engine !== nextManifest.engine
|
|
95
|
+
|| !isDeepStrictEqual(previousManifest.metadata, nextManifest.metadata));
|
|
94
96
|
}
|
|
95
97
|
function childOrderChanged(previous, next) {
|
|
96
98
|
for (const [id, node] of next) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/runtime",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
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",
|
|
@@ -19,25 +19,25 @@
|
|
|
19
19
|
"es-module-lexer": "2.3.1",
|
|
20
20
|
"import-meta-resolve": "4.2.0",
|
|
21
21
|
"semver": "7.8.5",
|
|
22
|
-
"@zhin.js/feature-kit": "1.1.
|
|
23
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
22
|
+
"@zhin.js/feature-kit": "1.1.7",
|
|
23
|
+
"@zhin.js/plugin-runtime": "1.1.15"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^26.1.2",
|
|
27
27
|
"tsx": "^4.23.1",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
|
-
"@zhin.js/adapter": "1.1.
|
|
30
|
-
"@zhin.js/agent-feature": "1.1.
|
|
31
|
-
"@zhin.js/command": "1.1.
|
|
32
|
-
"@zhin.js/component": "1.1.
|
|
33
|
-
"@zhin.js/
|
|
34
|
-
"@zhin.js/
|
|
35
|
-
"@zhin.js/
|
|
36
|
-
"@zhin.js/middleware": "1.1.
|
|
37
|
-
"@zhin.js/
|
|
38
|
-
"@zhin.js/
|
|
39
|
-
"@zhin.js/skill": "1.1.
|
|
40
|
-
"@zhin.js/tool": "1.1.
|
|
29
|
+
"@zhin.js/adapter": "1.1.21",
|
|
30
|
+
"@zhin.js/agent-feature": "1.1.8",
|
|
31
|
+
"@zhin.js/command": "1.1.9",
|
|
32
|
+
"@zhin.js/component": "1.1.8",
|
|
33
|
+
"@zhin.js/layout": "1.1.8",
|
|
34
|
+
"@zhin.js/page": "1.1.8",
|
|
35
|
+
"@zhin.js/handler": "1.1.7",
|
|
36
|
+
"@zhin.js/middleware": "1.1.8",
|
|
37
|
+
"@zhin.js/prompt-section": "1.1.7",
|
|
38
|
+
"@zhin.js/mcp-feature": "1.1.8",
|
|
39
|
+
"@zhin.js/skill": "1.1.8",
|
|
40
|
+
"@zhin.js/tool": "1.1.8"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|