@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,24 @@
|
|
|
1
|
+
import { type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { RuntimeEnvironment } from './environment.js';
|
|
3
|
+
import type { EnvironmentLayers } from './environment-store.js';
|
|
4
|
+
import type { IsolatedPluginRuntimePort } from './isolation.js';
|
|
5
|
+
import type { ModuleRuntime } from './module-runtime.js';
|
|
6
|
+
import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
|
|
7
|
+
import type { ProjectGraph } from './project-graph.js';
|
|
8
|
+
import type { PreparedRuntimeGeneration, RuntimeGenerationModel } from './runtime-generation.js';
|
|
9
|
+
export declare class SubtreeTopologyChangedError extends Error {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
/** Replaces selected Plugin forests while retaining all other Scope lifetimes. */
|
|
13
|
+
export declare class SubtreeGenerationPreparer {
|
|
14
|
+
private readonly modules;
|
|
15
|
+
private readonly model;
|
|
16
|
+
private readonly graph;
|
|
17
|
+
private readonly configResolver;
|
|
18
|
+
private readonly environment;
|
|
19
|
+
private readonly installResources?;
|
|
20
|
+
private readonly environmentLayers;
|
|
21
|
+
private readonly isolation?;
|
|
22
|
+
constructor(modules: ModuleRuntime, model: RuntimeGenerationModel, graph: ProjectGraph, configResolver: PluginConfigResolver, environment: RuntimeEnvironment, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, isolation?: IsolatedPluginRuntimePort | undefined);
|
|
23
|
+
prepare(current: RuntimeSnapshot, roots: readonly PluginId[]): Promise<PreparedRuntimeGeneration>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { DisposeStack, createSnapshotView, rootPluginId, } 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 { PluginScopeAssembler, } from './plugin-scope-assembler.js';
|
|
6
|
+
import { SourceOwnershipIndex } from './source-ownership.js';
|
|
7
|
+
export class SubtreeTopologyChangedError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'SubtreeTopologyChangedError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Replaces selected Plugin forests while retaining all other Scope lifetimes. */
|
|
14
|
+
export class SubtreeGenerationPreparer {
|
|
15
|
+
modules;
|
|
16
|
+
model;
|
|
17
|
+
graph;
|
|
18
|
+
configResolver;
|
|
19
|
+
environment;
|
|
20
|
+
installResources;
|
|
21
|
+
environmentLayers;
|
|
22
|
+
isolation;
|
|
23
|
+
constructor(modules, model, graph, configResolver, environment, installResources, environmentLayers = {}, isolation) {
|
|
24
|
+
this.modules = modules;
|
|
25
|
+
this.model = model;
|
|
26
|
+
this.graph = graph;
|
|
27
|
+
this.configResolver = configResolver;
|
|
28
|
+
this.environment = environment;
|
|
29
|
+
this.installResources = installResources;
|
|
30
|
+
this.environmentLayers = environmentLayers;
|
|
31
|
+
this.isolation = isolation;
|
|
32
|
+
}
|
|
33
|
+
async prepare(current, roots) {
|
|
34
|
+
const nodes = indexGraph(this.graph);
|
|
35
|
+
assertCompatibleTopology(indexGraph(this.model.graph), nodes, roots);
|
|
36
|
+
const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.installResources, this.environmentLayers, {
|
|
37
|
+
scopes: this.model.scopes,
|
|
38
|
+
tree: current.tree,
|
|
39
|
+
config: current.config,
|
|
40
|
+
resources: current.resources,
|
|
41
|
+
}, this.isolation);
|
|
42
|
+
plugins.removeSubtrees(roots);
|
|
43
|
+
const projectionDisposers = [];
|
|
44
|
+
try {
|
|
45
|
+
for (const root of roots) {
|
|
46
|
+
const node = nodes.get(root);
|
|
47
|
+
if (!node)
|
|
48
|
+
throw new SubtreeTopologyChangedError(`Missing subtree root: ${root}`);
|
|
49
|
+
await plugins.setupTree(node);
|
|
50
|
+
}
|
|
51
|
+
const capabilities = new Map(current.capabilities);
|
|
52
|
+
for (const [id, slot] of capabilities) {
|
|
53
|
+
if (roots.some((root) => isWithin(slot.owner, root)))
|
|
54
|
+
capabilities.delete(id);
|
|
55
|
+
}
|
|
56
|
+
const discovery = new FeatureDiscovery(new NodeDiscoveryHost(this.modules));
|
|
57
|
+
for (const provider of this.model.providers.values()) {
|
|
58
|
+
const affectedRoots = (this.model.rootsByFeature.get(provider.id) ?? [])
|
|
59
|
+
.filter((root) => roots.some((owner) => isWithin(root.owner, owner)));
|
|
60
|
+
if (affectedRoots.length === 0)
|
|
61
|
+
continue;
|
|
62
|
+
const slots = await discovery.discover(provider, affectedRoots);
|
|
63
|
+
for (const slot of slots)
|
|
64
|
+
capabilities.set(slot.id, slot);
|
|
65
|
+
}
|
|
66
|
+
const projected = await new FeatureProjector(this.model.providers.values()).project(current.generation + 1, {
|
|
67
|
+
root: current.root,
|
|
68
|
+
tree: plugins.tree,
|
|
69
|
+
config: plugins.config,
|
|
70
|
+
resources: plugins.resources,
|
|
71
|
+
capabilities,
|
|
72
|
+
});
|
|
73
|
+
projectionDisposers.push(...projected.disposers);
|
|
74
|
+
const snapshot = createSnapshotView(current.generation + 1, projected.state);
|
|
75
|
+
const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, this.model.featureIdsByPackageRoot);
|
|
76
|
+
const replacements = new Map(plugins.createdScopeDisposers());
|
|
77
|
+
// Local prepare still commits a complete immutable generation. The
|
|
78
|
+
// replacement map controls lifetime ownership, not snapshot granularity.
|
|
79
|
+
const assets = this.model.assets.replaceScopes([...nodes.keys()], replacements, projectionDisposers);
|
|
80
|
+
return {
|
|
81
|
+
generation: {
|
|
82
|
+
snapshot: projected.state,
|
|
83
|
+
dispose: () => assets.dispose(),
|
|
84
|
+
handoff: composeGenerationHandoffs(plugins.generationHandoff(), projected.handoff),
|
|
85
|
+
},
|
|
86
|
+
ownership,
|
|
87
|
+
model: {
|
|
88
|
+
...this.model,
|
|
89
|
+
graph: this.graph,
|
|
90
|
+
scopes: new Map(plugins.scopes),
|
|
91
|
+
assets,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
await rollback(plugins.createdScopeDisposers().map(([, dispose]) => dispose), projectionDisposers, error);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function indexGraph(graph) {
|
|
102
|
+
const result = new Map();
|
|
103
|
+
const visit = (node) => {
|
|
104
|
+
result.set(node.id, node);
|
|
105
|
+
for (const child of node.children)
|
|
106
|
+
visit(child);
|
|
107
|
+
};
|
|
108
|
+
visit(graph.root);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
function assertCompatibleTopology(previousNodes, nextNodes, roots) {
|
|
112
|
+
if (roots.includes(rootPluginId())) {
|
|
113
|
+
throw new SubtreeTopologyChangedError('Root changes require a full generation');
|
|
114
|
+
}
|
|
115
|
+
if (previousNodes.size !== nextNodes.size) {
|
|
116
|
+
throw new SubtreeTopologyChangedError('Plugin tree size changed');
|
|
117
|
+
}
|
|
118
|
+
for (const [id, previous] of previousNodes) {
|
|
119
|
+
const next = nextNodes.get(id);
|
|
120
|
+
if (!next
|
|
121
|
+
|| next.parent !== previous.parent
|
|
122
|
+
|| next.instanceKey !== previous.instanceKey
|
|
123
|
+
|| next.package.root !== previous.package.root
|
|
124
|
+
|| next.package.name !== previous.package.name
|
|
125
|
+
|| !sameValues(next.children.map((child) => child.id), previous.children.map((child) => child.id))
|
|
126
|
+
|| !sameValues(featureRoots(next), featureRoots(previous))) {
|
|
127
|
+
throw new SubtreeTopologyChangedError(`Plugin topology changed at ${id}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function featureRoots(node) {
|
|
132
|
+
return node.features.map((feature) => feature.package.root);
|
|
133
|
+
}
|
|
134
|
+
function sameValues(left, right) {
|
|
135
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
136
|
+
}
|
|
137
|
+
function isWithin(plugin, root) {
|
|
138
|
+
return plugin === root || plugin.startsWith(`${root}/`);
|
|
139
|
+
}
|
|
140
|
+
async function rollback(scopeDisposers, projectionDisposers, prepareError) {
|
|
141
|
+
const disposers = new DisposeStack();
|
|
142
|
+
for (const dispose of scopeDisposers)
|
|
143
|
+
disposers.add(dispose);
|
|
144
|
+
for (const dispose of projectionDisposers)
|
|
145
|
+
disposers.add(dispose);
|
|
146
|
+
try {
|
|
147
|
+
await disposers.dispose();
|
|
148
|
+
}
|
|
149
|
+
catch (disposeError) {
|
|
150
|
+
throw new AggregateError([prepareError, disposeError], 'Subtree generation prepare and rollback both failed', { cause: disposeError });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { RuntimeEnvironment } from './environment.js';
|
|
3
|
+
import type { EnvironmentLayers } from './environment-store.js';
|
|
4
|
+
import type { IsolatedPluginRuntimePort } from './isolation.js';
|
|
5
|
+
import type { ModuleRuntime } from './module-runtime.js';
|
|
6
|
+
import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
|
|
7
|
+
import type { ProjectGraph } from './project-graph.js';
|
|
8
|
+
import type { PreparedRuntimeGeneration, RuntimeGenerationModel } from './runtime-generation.js';
|
|
9
|
+
/** Prepares manifest topology changes without rebuilding stable Plugin Scopes. */
|
|
10
|
+
export declare class TopologyGenerationPreparer {
|
|
11
|
+
#private;
|
|
12
|
+
private readonly modules;
|
|
13
|
+
private readonly model;
|
|
14
|
+
private readonly graph;
|
|
15
|
+
private readonly configResolver;
|
|
16
|
+
private readonly environment;
|
|
17
|
+
private readonly installResources?;
|
|
18
|
+
private readonly environmentLayers;
|
|
19
|
+
private readonly isolation?;
|
|
20
|
+
constructor(modules: ModuleRuntime, model: RuntimeGenerationModel, graph: ProjectGraph, configResolver: PluginConfigResolver, environment: RuntimeEnvironment, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, isolation?: IsolatedPluginRuntimePort | undefined);
|
|
21
|
+
prepare(current: RuntimeSnapshot): Promise<PreparedRuntimeGeneration | undefined>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { DisposeStack, createSnapshotView, } from '@zhin.js/plugin-runtime';
|
|
4
|
+
import { FeatureCatalog, FeatureDiscovery, } from '@zhin.js/feature-kit';
|
|
5
|
+
import { FeatureProjector, composeGenerationHandoffs } from './feature-projector.js';
|
|
6
|
+
import { NodeDiscoveryHost } from './node-discovery-host.js';
|
|
7
|
+
import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
|
|
8
|
+
import { SourceOwnershipIndex } from './source-ownership.js';
|
|
9
|
+
import { TopologyTransactionPlanner, collapseRoots, graphNodes, graphOrder, isWithin, } from './topology-transaction.js';
|
|
10
|
+
/** Prepares manifest topology changes without rebuilding stable Plugin Scopes. */
|
|
11
|
+
export class TopologyGenerationPreparer {
|
|
12
|
+
modules;
|
|
13
|
+
model;
|
|
14
|
+
graph;
|
|
15
|
+
configResolver;
|
|
16
|
+
environment;
|
|
17
|
+
installResources;
|
|
18
|
+
environmentLayers;
|
|
19
|
+
isolation;
|
|
20
|
+
constructor(modules, model, graph, configResolver, environment, installResources, environmentLayers = {}, isolation) {
|
|
21
|
+
this.modules = modules;
|
|
22
|
+
this.model = model;
|
|
23
|
+
this.graph = graph;
|
|
24
|
+
this.configResolver = configResolver;
|
|
25
|
+
this.environment = environment;
|
|
26
|
+
this.installResources = installResources;
|
|
27
|
+
this.environmentLayers = environmentLayers;
|
|
28
|
+
this.isolation = isolation;
|
|
29
|
+
}
|
|
30
|
+
async prepare(current) {
|
|
31
|
+
const planned = new TopologyTransactionPlanner().plan(this.model.graph, this.graph);
|
|
32
|
+
const nextNodes = graphNodes(this.graph);
|
|
33
|
+
const configReplacements = changedConfigRoots(current, nextNodes, this.configResolver);
|
|
34
|
+
const replacedPluginRoots = collapseRoots([
|
|
35
|
+
...planned.replacedPluginRoots,
|
|
36
|
+
...configReplacements,
|
|
37
|
+
]);
|
|
38
|
+
const plan = withReplacements(planned, replacedPluginRoots);
|
|
39
|
+
if (!plan.changed)
|
|
40
|
+
return undefined;
|
|
41
|
+
const featureTopology = await this.#loadFeatureTopology(plan);
|
|
42
|
+
const plugins = new PluginScopeAssembler(this.modules, this.configResolver, this.environment, this.installResources, this.environmentLayers, {
|
|
43
|
+
scopes: this.model.scopes,
|
|
44
|
+
tree: current.tree,
|
|
45
|
+
config: current.config,
|
|
46
|
+
resources: current.resources,
|
|
47
|
+
}, this.isolation);
|
|
48
|
+
const setupRoots = collapseRoots([
|
|
49
|
+
...plan.addedPluginRoots,
|
|
50
|
+
...plan.replacedPluginRoots,
|
|
51
|
+
]);
|
|
52
|
+
const removalRoots = collapseRoots([
|
|
53
|
+
...plan.removedPluginRoots,
|
|
54
|
+
...plan.replacedPluginRoots,
|
|
55
|
+
]);
|
|
56
|
+
plugins.removeSubtrees(removalRoots);
|
|
57
|
+
const projectionDisposers = [];
|
|
58
|
+
try {
|
|
59
|
+
for (const root of setupRoots) {
|
|
60
|
+
const node = nextNodes.get(root);
|
|
61
|
+
if (!node)
|
|
62
|
+
throw new Error(`Missing topology setup root: ${root}`);
|
|
63
|
+
await plugins.setupTree(node);
|
|
64
|
+
}
|
|
65
|
+
// Retained parents still need a new immutable children view after add,
|
|
66
|
+
// remove, move, or reorder operations.
|
|
67
|
+
plugins.synchronizeTree(this.graph.root);
|
|
68
|
+
const capabilities = await this.#prepareCapabilities(current, plan, setupRoots, featureTopology);
|
|
69
|
+
const projected = await new FeatureProjector(featureTopology.providers.values()).project(current.generation + 1, {
|
|
70
|
+
root: current.root,
|
|
71
|
+
tree: plugins.tree,
|
|
72
|
+
config: plugins.config,
|
|
73
|
+
resources: plugins.resources,
|
|
74
|
+
capabilities,
|
|
75
|
+
});
|
|
76
|
+
projectionDisposers.push(...projected.disposers);
|
|
77
|
+
const snapshot = createSnapshotView(current.generation + 1, projected.state);
|
|
78
|
+
const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, featureTopology.featureIdsByPackageRoot);
|
|
79
|
+
const replacements = new Map(plugins.createdScopeDisposers());
|
|
80
|
+
const assets = this.model.assets.replaceScopes(graphOrder(this.graph), replacements, projectionDisposers);
|
|
81
|
+
return {
|
|
82
|
+
generation: {
|
|
83
|
+
snapshot: projected.state,
|
|
84
|
+
dispose: () => assets.dispose(),
|
|
85
|
+
handoff: composeGenerationHandoffs(plugins.generationHandoff(), projected.handoff),
|
|
86
|
+
},
|
|
87
|
+
ownership,
|
|
88
|
+
model: {
|
|
89
|
+
graph: this.graph,
|
|
90
|
+
providers: featureTopology.providers,
|
|
91
|
+
rootsByFeature: featureTopology.rootsByFeature,
|
|
92
|
+
featureIdsByPackageRoot: featureTopology.featureIdsByPackageRoot,
|
|
93
|
+
scopes: new Map(plugins.scopes),
|
|
94
|
+
assets,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
await rollback(plugins.createdScopeDisposers().map(([, dispose]) => dispose), projectionDisposers, error);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async #loadFeatureTopology(plan) {
|
|
104
|
+
const catalog = new FeatureCatalog();
|
|
105
|
+
const providers = new Map();
|
|
106
|
+
const rootsByFeature = new Map();
|
|
107
|
+
const featureIdsByPackageRoot = new Map();
|
|
108
|
+
const loadedByPackageRoot = new Map();
|
|
109
|
+
for (const node of graphNodes(this.graph).values()) {
|
|
110
|
+
for (const requirement of node.features) {
|
|
111
|
+
const packageRoot = resolve(requirement.package.root);
|
|
112
|
+
let provider = loadedByPackageRoot.get(packageRoot);
|
|
113
|
+
if (!provider) {
|
|
114
|
+
const previousId = this.model.featureIdsByPackageRoot.get(packageRoot);
|
|
115
|
+
provider = !plan.reloadedFeaturePackages.has(packageRoot) && previousId
|
|
116
|
+
? this.model.providers.get(previousId)
|
|
117
|
+
: undefined;
|
|
118
|
+
if (!provider)
|
|
119
|
+
provider = await loadProvider(this.modules, requirement.package);
|
|
120
|
+
loadedByPackageRoot.set(packageRoot, provider);
|
|
121
|
+
featureIdsByPackageRoot.set(packageRoot, provider.id);
|
|
122
|
+
catalog.add(provider);
|
|
123
|
+
providers.set(provider.id, provider);
|
|
124
|
+
}
|
|
125
|
+
const roots = rootsByFeature.get(provider.id) ?? [];
|
|
126
|
+
roots.push({ owner: node.id, packageRoot: node.package.root });
|
|
127
|
+
rootsByFeature.set(provider.id, roots);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
providers: new Map(providers),
|
|
132
|
+
rootsByFeature: new Map([...rootsByFeature].map(([feature, roots]) => [feature, Object.freeze(roots)])),
|
|
133
|
+
featureIdsByPackageRoot: new Map(featureIdsByPackageRoot),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async #prepareCapabilities(current, plan, setupRoots, topology) {
|
|
137
|
+
const nextOwners = new Set(graphNodes(this.graph).keys());
|
|
138
|
+
const mounted = mountedFeatures(topology);
|
|
139
|
+
const refresh = new Set();
|
|
140
|
+
for (const [feature, roots] of topology.rootsByFeature) {
|
|
141
|
+
for (const root of roots) {
|
|
142
|
+
if (setupRoots.some((owner) => isWithin(root.owner, owner))) {
|
|
143
|
+
refresh.add(ownerFeatureKey(root.owner, feature));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const mount of plan.addedFeatures) {
|
|
148
|
+
const feature = topology.featureIdsByPackageRoot.get(mount.packageRoot);
|
|
149
|
+
if (feature)
|
|
150
|
+
refresh.add(ownerFeatureKey(mount.owner, feature));
|
|
151
|
+
}
|
|
152
|
+
for (const mount of featureMountsForReload(this.graph, plan.reloadedFeaturePackages)) {
|
|
153
|
+
const feature = topology.featureIdsByPackageRoot.get(mount.packageRoot);
|
|
154
|
+
if (feature)
|
|
155
|
+
refresh.add(ownerFeatureKey(mount.owner, feature));
|
|
156
|
+
}
|
|
157
|
+
for (const mount of plan.removedFeatures) {
|
|
158
|
+
const previousFeature = this.model.featureIdsByPackageRoot.get(mount.packageRoot);
|
|
159
|
+
if (previousFeature)
|
|
160
|
+
refresh.add(ownerFeatureKey(mount.owner, previousFeature));
|
|
161
|
+
}
|
|
162
|
+
const capabilities = new Map(current.capabilities);
|
|
163
|
+
for (const [id, slot] of capabilities) {
|
|
164
|
+
const key = ownerFeatureKey(slot.owner, slot.feature);
|
|
165
|
+
if (!nextOwners.has(slot.owner) || !mounted.has(key) || refresh.has(key)) {
|
|
166
|
+
capabilities.delete(id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const discovery = new FeatureDiscovery(new NodeDiscoveryHost(this.modules));
|
|
170
|
+
for (const [feature, roots] of topology.rootsByFeature) {
|
|
171
|
+
const selected = roots.filter((root) => refresh.has(ownerFeatureKey(root.owner, feature)));
|
|
172
|
+
if (selected.length === 0)
|
|
173
|
+
continue;
|
|
174
|
+
const provider = topology.providers.get(feature);
|
|
175
|
+
if (!provider)
|
|
176
|
+
throw new Error(`Missing Feature provider for ${feature}`);
|
|
177
|
+
for (const slot of await discovery.discover(provider, selected)) {
|
|
178
|
+
capabilities.set(slot.id, slot);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return capabilities;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function loadProvider(modules, pkg) {
|
|
185
|
+
const manifest = pkg.packageJson.zhin;
|
|
186
|
+
const module = await modules.load(resolve(pkg.root, manifest.entry));
|
|
187
|
+
const provider = module.default;
|
|
188
|
+
if (!provider || provider.protocol !== 1) {
|
|
189
|
+
throw new TypeError(`${pkg.name} does not default-export a Feature provider`);
|
|
190
|
+
}
|
|
191
|
+
return provider;
|
|
192
|
+
}
|
|
193
|
+
function changedConfigRoots(current, nextNodes, configResolver) {
|
|
194
|
+
const changed = [];
|
|
195
|
+
for (const [id, node] of nextNodes) {
|
|
196
|
+
if (!current.tree.has(id))
|
|
197
|
+
continue;
|
|
198
|
+
if (!isDeepStrictEqual(current.config.get(id), configResolver(node) ?? {}))
|
|
199
|
+
changed.push(id);
|
|
200
|
+
}
|
|
201
|
+
return collapseRoots(changed);
|
|
202
|
+
}
|
|
203
|
+
function withReplacements(plan, replacedPluginRoots) {
|
|
204
|
+
if (sameValues(plan.replacedPluginRoots, replacedPluginRoots))
|
|
205
|
+
return plan;
|
|
206
|
+
return Object.freeze({
|
|
207
|
+
...plan,
|
|
208
|
+
replacedPluginRoots,
|
|
209
|
+
changed: plan.changed || replacedPluginRoots.length > 0,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function mountedFeatures(topology) {
|
|
213
|
+
const mounted = new Set();
|
|
214
|
+
for (const [feature, roots] of topology.rootsByFeature) {
|
|
215
|
+
for (const root of roots)
|
|
216
|
+
mounted.add(ownerFeatureKey(root.owner, feature));
|
|
217
|
+
}
|
|
218
|
+
return mounted;
|
|
219
|
+
}
|
|
220
|
+
function featureMountsForReload(graph, packageRoots) {
|
|
221
|
+
const mounts = [];
|
|
222
|
+
for (const node of graphNodes(graph).values()) {
|
|
223
|
+
for (const requirement of node.features) {
|
|
224
|
+
const packageRoot = resolve(requirement.package.root);
|
|
225
|
+
if (packageRoots.has(packageRoot))
|
|
226
|
+
mounts.push({ owner: node.id, packageRoot });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return mounts;
|
|
230
|
+
}
|
|
231
|
+
function ownerFeatureKey(owner, feature) {
|
|
232
|
+
return `${owner}\0${feature}`;
|
|
233
|
+
}
|
|
234
|
+
function sameValues(left, right) {
|
|
235
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
236
|
+
}
|
|
237
|
+
async function rollback(scopeDisposers, projectionDisposers, prepareError) {
|
|
238
|
+
const disposers = new DisposeStack();
|
|
239
|
+
for (const dispose of scopeDisposers)
|
|
240
|
+
disposers.add(dispose);
|
|
241
|
+
for (const dispose of projectionDisposers)
|
|
242
|
+
disposers.add(dispose);
|
|
243
|
+
try {
|
|
244
|
+
await disposers.dispose();
|
|
245
|
+
}
|
|
246
|
+
catch (disposeError) {
|
|
247
|
+
throw new AggregateError([prepareError, disposeError], 'Topology generation prepare and rollback both failed', { cause: disposeError });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PluginId } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { PluginGraphNode, ProjectGraph } from './project-graph.js';
|
|
3
|
+
export interface FeatureMount {
|
|
4
|
+
readonly owner: PluginId;
|
|
5
|
+
readonly packageRoot: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TopologyTransactionPlan {
|
|
8
|
+
readonly addedPluginRoots: readonly PluginId[];
|
|
9
|
+
readonly removedPluginRoots: readonly PluginId[];
|
|
10
|
+
readonly replacedPluginRoots: readonly PluginId[];
|
|
11
|
+
readonly addedFeatures: readonly FeatureMount[];
|
|
12
|
+
readonly removedFeatures: readonly FeatureMount[];
|
|
13
|
+
readonly reloadedFeaturePackages: ReadonlySet<string>;
|
|
14
|
+
readonly treeChanged: boolean;
|
|
15
|
+
readonly featureOrderChanged: boolean;
|
|
16
|
+
readonly changed: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Compiles two resolved graphs into the smallest safe topology transaction. */
|
|
19
|
+
export declare class TopologyTransactionPlanner {
|
|
20
|
+
plan(previous: ProjectGraph, next: ProjectGraph): TopologyTransactionPlan;
|
|
21
|
+
}
|
|
22
|
+
export declare function graphNodes(graph: ProjectGraph): ReadonlyMap<PluginId, PluginGraphNode>;
|
|
23
|
+
export declare function graphOrder(graph: ProjectGraph): readonly PluginId[];
|
|
24
|
+
export declare function featureMounts(graph: ProjectGraph): readonly FeatureMount[];
|
|
25
|
+
export declare function isWithin(plugin: PluginId, root: PluginId): boolean;
|
|
26
|
+
export declare function collapseRoots(values: readonly PluginId[]): readonly PluginId[];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
/** Compiles two resolved graphs into the smallest safe topology transaction. */
|
|
3
|
+
export class TopologyTransactionPlanner {
|
|
4
|
+
plan(previous, next) {
|
|
5
|
+
const previousNodes = indexGraph(previous);
|
|
6
|
+
const nextNodes = indexGraph(next);
|
|
7
|
+
const added = [...nextNodes.keys()].filter((id) => !previousNodes.has(id));
|
|
8
|
+
const removed = [...previousNodes.keys()].filter((id) => !nextNodes.has(id));
|
|
9
|
+
const replaced = [...nextNodes].flatMap(([id, node]) => {
|
|
10
|
+
const old = previousNodes.get(id);
|
|
11
|
+
return old && scopeContractChanged(old, node) ? [id] : [];
|
|
12
|
+
});
|
|
13
|
+
const addedPluginRoots = collapseRoots(added);
|
|
14
|
+
const removedPluginRoots = collapseRoots(removed);
|
|
15
|
+
const replacedPluginRoots = collapseRoots(replaced);
|
|
16
|
+
const previousFeatures = featureMounts(previous);
|
|
17
|
+
const nextFeatures = featureMounts(next);
|
|
18
|
+
const addedFeatures = difference(nextFeatures, previousFeatures);
|
|
19
|
+
const removedFeatures = difference(previousFeatures, nextFeatures);
|
|
20
|
+
const reloadedFeaturePackages = changedFeaturePackages(previous, next);
|
|
21
|
+
const treeChanged = childOrderChanged(previousNodes, nextNodes);
|
|
22
|
+
const featureOrderChanged = !sameValues(previousFeatures.map(featureMountKey), nextFeatures.map(featureMountKey));
|
|
23
|
+
const changed = (addedPluginRoots.length > 0
|
|
24
|
+
|| removedPluginRoots.length > 0
|
|
25
|
+
|| replacedPluginRoots.length > 0
|
|
26
|
+
|| addedFeatures.length > 0
|
|
27
|
+
|| removedFeatures.length > 0
|
|
28
|
+
|| reloadedFeaturePackages.size > 0
|
|
29
|
+
|| treeChanged
|
|
30
|
+
|| featureOrderChanged);
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
addedPluginRoots,
|
|
33
|
+
removedPluginRoots,
|
|
34
|
+
replacedPluginRoots,
|
|
35
|
+
addedFeatures,
|
|
36
|
+
removedFeatures,
|
|
37
|
+
reloadedFeaturePackages,
|
|
38
|
+
treeChanged,
|
|
39
|
+
featureOrderChanged,
|
|
40
|
+
changed,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function graphNodes(graph) {
|
|
45
|
+
return indexGraph(graph);
|
|
46
|
+
}
|
|
47
|
+
export function graphOrder(graph) {
|
|
48
|
+
return Object.freeze([...indexGraph(graph).keys()]);
|
|
49
|
+
}
|
|
50
|
+
export function featureMounts(graph) {
|
|
51
|
+
const mounts = [];
|
|
52
|
+
for (const node of indexGraph(graph).values()) {
|
|
53
|
+
for (const feature of node.features) {
|
|
54
|
+
mounts.push(Object.freeze({
|
|
55
|
+
owner: node.id,
|
|
56
|
+
packageRoot: resolve(feature.package.root),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Object.freeze(mounts);
|
|
61
|
+
}
|
|
62
|
+
export function isWithin(plugin, root) {
|
|
63
|
+
return plugin === root || plugin.startsWith(`${root}/`);
|
|
64
|
+
}
|
|
65
|
+
export function collapseRoots(values) {
|
|
66
|
+
const sorted = [...new Set(values)].sort((left, right) => left.length - right.length);
|
|
67
|
+
const roots = [];
|
|
68
|
+
for (const candidate of sorted) {
|
|
69
|
+
if (!roots.some((root) => isWithin(candidate, root)))
|
|
70
|
+
roots.push(candidate);
|
|
71
|
+
}
|
|
72
|
+
return Object.freeze(roots);
|
|
73
|
+
}
|
|
74
|
+
function indexGraph(graph) {
|
|
75
|
+
const nodes = new Map();
|
|
76
|
+
const visit = (node) => {
|
|
77
|
+
nodes.set(node.id, node);
|
|
78
|
+
for (const child of node.children)
|
|
79
|
+
visit(child);
|
|
80
|
+
};
|
|
81
|
+
visit(graph.root);
|
|
82
|
+
return nodes;
|
|
83
|
+
}
|
|
84
|
+
function scopeContractChanged(previous, next) {
|
|
85
|
+
const previousManifest = previous.package.packageJson.zhin;
|
|
86
|
+
const nextManifest = next.package.packageJson.zhin;
|
|
87
|
+
return (previous.parent !== next.parent
|
|
88
|
+
|| previous.instanceKey !== next.instanceKey
|
|
89
|
+
|| previous.package.root !== next.package.root
|
|
90
|
+
|| previous.package.name !== next.package.name
|
|
91
|
+
|| previousManifest.entry !== nextManifest.entry
|
|
92
|
+
|| previousManifest.runtime !== nextManifest.runtime
|
|
93
|
+
|| previousManifest.engine !== nextManifest.engine);
|
|
94
|
+
}
|
|
95
|
+
function childOrderChanged(previous, next) {
|
|
96
|
+
for (const [id, node] of next) {
|
|
97
|
+
const old = previous.get(id);
|
|
98
|
+
if (!old)
|
|
99
|
+
continue;
|
|
100
|
+
const oldChildren = old.children.map((child) => child.id);
|
|
101
|
+
const nextChildren = node.children.map((child) => child.id);
|
|
102
|
+
if (!sameValues(oldChildren, nextChildren))
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
function changedFeaturePackages(previous, next) {
|
|
108
|
+
const previousPackages = featurePackages(previous);
|
|
109
|
+
const result = new Set();
|
|
110
|
+
for (const [root, pkg] of featurePackages(next)) {
|
|
111
|
+
const old = previousPackages.get(root);
|
|
112
|
+
if (!old)
|
|
113
|
+
continue;
|
|
114
|
+
const oldManifest = old.packageJson.zhin;
|
|
115
|
+
const nextManifest = pkg.packageJson.zhin;
|
|
116
|
+
if (old.name !== pkg.name
|
|
117
|
+
|| oldManifest.entry !== nextManifest.entry
|
|
118
|
+
|| oldManifest.engine !== nextManifest.engine
|
|
119
|
+
|| oldManifest.featureApi !== nextManifest.featureApi)
|
|
120
|
+
result.add(root);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function featurePackages(graph) {
|
|
125
|
+
const packages = new Map();
|
|
126
|
+
for (const node of indexGraph(graph).values()) {
|
|
127
|
+
for (const feature of node.features) {
|
|
128
|
+
packages.set(resolve(feature.package.root), feature.package);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return packages;
|
|
132
|
+
}
|
|
133
|
+
function difference(left, right) {
|
|
134
|
+
const rightKeys = new Set(right.map(featureMountKey));
|
|
135
|
+
return Object.freeze(left.filter((mount) => !rightKeys.has(featureMountKey(mount))));
|
|
136
|
+
}
|
|
137
|
+
function featureMountKey(mount) {
|
|
138
|
+
return `${mount.owner}\0${mount.packageRoot}`;
|
|
139
|
+
}
|
|
140
|
+
function sameValues(left, right) {
|
|
141
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
142
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESM resolve hook: when a relative/absolute `.js` specifier fails and a
|
|
3
|
+
* sibling `.ts` / `.tsx` exists, rewrite to that source file.
|
|
4
|
+
*
|
|
5
|
+
* Needed for Plugin Runtime on Node 22.6–22.17 with `--experimental-strip-types`:
|
|
6
|
+
* TypeScript authoring uses `.js` import suffixes, but strip-types does not
|
|
7
|
+
* remap missing `.js` → `.ts` the way `tsc`/tsx do.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolve(specifier: string, context: {
|
|
10
|
+
parentURL?: string;
|
|
11
|
+
}, nextResolve: (specifier: string, context: {
|
|
12
|
+
parentURL?: string;
|
|
13
|
+
}) => Promise<{
|
|
14
|
+
url: string;
|
|
15
|
+
shortCircuit?: boolean;
|
|
16
|
+
format?: string;
|
|
17
|
+
}>): Promise<{
|
|
18
|
+
url: string;
|
|
19
|
+
shortCircuit?: boolean;
|
|
20
|
+
format?: string;
|
|
21
|
+
}>;
|
|
22
|
+
/** Register once for the current process (safe to call from CLI / RootHost). */
|
|
23
|
+
export declare function ensureTypeScriptSpecifierRemap(): void;
|