@zhin.js/runtime 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config-composer.js +8 -0
- package/lib/config-patch-planner.js +42 -6
- package/lib/environment-store.js +7 -2
- package/lib/feature-projector.d.ts +4 -4
- package/lib/feature-projector.js +10 -9
- package/lib/generation-assets.d.ts +4 -4
- package/lib/generation-assets.js +29 -7
- package/lib/hmr-coordinator.d.ts +2 -0
- package/lib/hmr-coordinator.js +5 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/manifest.d.ts +6 -0
- package/lib/manifest.js +9 -1
- package/lib/native-development-runtime.js +26 -2
- package/lib/package-resolver.d.ts +4 -1
- package/lib/package-resolver.js +31 -12
- package/lib/platform-features.d.ts +25 -0
- package/lib/platform-features.js +38 -0
- package/lib/plugin-scope-assembler.d.ts +8 -2
- package/lib/plugin-scope-assembler.js +40 -5
- package/lib/primary-config.d.ts +16 -0
- package/lib/primary-config.js +20 -0
- package/lib/project-graph.d.ts +4 -3
- package/lib/project-graph.js +92 -19
- package/lib/root-runtime.js +97 -40
- package/lib/setup-capabilities.d.ts +5 -0
- package/lib/setup-capabilities.js +44 -0
- package/lib/slot-generation-preparer.js +12 -4
- package/lib/source-ownership.js +2 -0
- package/lib/subtree-generation-preparer.d.ts +3 -1
- package/lib/subtree-generation-preparer.js +13 -6
- package/lib/topology-generation-preparer.d.ts +3 -1
- package/lib/topology-generation-preparer.js +13 -6
- package/package.json +13 -13
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
-
import { GenerationHandoffStack, Scope, rootPluginId, } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { GenerationHandoffStack, Scope, capabilityId, featureId, rootPluginId, } from '@zhin.js/plugin-runtime';
|
|
3
3
|
import { runtimeEnvironmentToken } from './environment.js';
|
|
4
4
|
import { EnvStoreFactory, envStoreToken, } from './environment-store.js';
|
|
5
|
+
import { createPrimaryConfig, primaryConfigToken, } from './primary-config.js';
|
|
5
6
|
/** Assembles Plugin setup into mutable shadow maps without publishing them. */
|
|
6
7
|
export class PluginScopeAssembler {
|
|
7
8
|
modules;
|
|
8
9
|
configResolver;
|
|
9
10
|
environment;
|
|
11
|
+
primaryConfigDocument;
|
|
10
12
|
installResources;
|
|
11
13
|
isolation;
|
|
12
14
|
scopes;
|
|
13
15
|
tree;
|
|
14
16
|
config;
|
|
15
17
|
resources;
|
|
18
|
+
#setupCapabilities = new Map();
|
|
16
19
|
#created = [];
|
|
17
20
|
#handoffs = new GenerationHandoffStack();
|
|
18
21
|
#envStores;
|
|
19
|
-
|
|
22
|
+
#setupFeatureAliases = new Map();
|
|
23
|
+
constructor(modules, configResolver, environment, primaryConfigDocument, installResources, environmentLayers = {}, seed, isolation) {
|
|
20
24
|
this.modules = modules;
|
|
21
25
|
this.configResolver = configResolver;
|
|
22
26
|
this.environment = environment;
|
|
27
|
+
this.primaryConfigDocument = primaryConfigDocument;
|
|
23
28
|
this.installResources = installResources;
|
|
24
29
|
this.isolation = isolation;
|
|
25
30
|
this.#envStores = new EnvStoreFactory(environment, environmentLayers);
|
|
@@ -40,6 +45,9 @@ export class PluginScopeAssembler {
|
|
|
40
45
|
this.resources.delete(owner);
|
|
41
46
|
}
|
|
42
47
|
}
|
|
48
|
+
installSetupFeatureAliases(aliases) {
|
|
49
|
+
this.#setupFeatureAliases = new Map(aliases);
|
|
50
|
+
}
|
|
43
51
|
async setupTree(node) {
|
|
44
52
|
const manifest = node.package.packageJson.zhin;
|
|
45
53
|
const parentScope = node.parent ? this.scopes.get(node.parent) : undefined;
|
|
@@ -49,13 +57,17 @@ export class PluginScopeAssembler {
|
|
|
49
57
|
this.scopes.set(node.id, scope);
|
|
50
58
|
this.#created.push(node.id);
|
|
51
59
|
// Every owner shadows the inherited EnvStore with its exact overlay view.
|
|
52
|
-
|
|
60
|
+
const environment = this.#envStores.create(node.id);
|
|
61
|
+
scope.provide(envStoreToken, environment);
|
|
53
62
|
if (!node.parent) {
|
|
54
63
|
scope.provide(runtimeEnvironmentToken, this.environment);
|
|
64
|
+
const config = createPrimaryConfig(this.primaryConfigDocument, environment);
|
|
65
|
+
scope.provide(primaryConfigToken, config);
|
|
55
66
|
await this.installResources?.({
|
|
56
67
|
resources: scope,
|
|
57
68
|
lifecycle: scope.disposers,
|
|
58
69
|
handoff: this.#handoffs,
|
|
70
|
+
config,
|
|
59
71
|
});
|
|
60
72
|
}
|
|
61
73
|
const config = Object.freeze(this.configResolver(node) ?? {});
|
|
@@ -109,13 +121,33 @@ export class PluginScopeAssembler {
|
|
|
109
121
|
throw new Error(`Missing resource ${token.id} for Plugin ${node.id}`);
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
|
-
const
|
|
124
|
+
const register = (feature, localName, capabilityDefinition) => {
|
|
125
|
+
const featureName = featureId(String(feature));
|
|
126
|
+
const id = capabilityId(node.id, featureName, localName);
|
|
127
|
+
if (this.#setupCapabilities.has(id)) {
|
|
128
|
+
throw new Error(`Duplicate setup Capability: ${id}`);
|
|
129
|
+
}
|
|
130
|
+
this.#setupCapabilities.set(id, Object.freeze({
|
|
131
|
+
id,
|
|
132
|
+
owner: node.id,
|
|
133
|
+
feature: featureName,
|
|
134
|
+
localName,
|
|
135
|
+
source: resolve(node.package.root, manifest.entry),
|
|
136
|
+
definition: capabilityDefinition,
|
|
137
|
+
}));
|
|
138
|
+
};
|
|
139
|
+
const setupContext = {
|
|
113
140
|
plugin,
|
|
114
141
|
config: view,
|
|
115
142
|
resources: scope,
|
|
116
143
|
lifecycle: scope.disposers,
|
|
117
144
|
handoff: this.#handoffs,
|
|
118
|
-
|
|
145
|
+
addFeature: register,
|
|
146
|
+
};
|
|
147
|
+
for (const [method, feature] of this.#setupFeatureAliases) {
|
|
148
|
+
setupContext[method] = (name, value) => register(feature, name, value);
|
|
149
|
+
}
|
|
150
|
+
const returned = await definition.setup?.(Object.freeze(setupContext));
|
|
119
151
|
if (returned)
|
|
120
152
|
scope.disposers.add(returned);
|
|
121
153
|
metadata = definition.metadata;
|
|
@@ -161,6 +193,9 @@ export class PluginScopeAssembler {
|
|
|
161
193
|
generationHandoff() {
|
|
162
194
|
return this.#handoffs.seal();
|
|
163
195
|
}
|
|
196
|
+
setupCapabilities() {
|
|
197
|
+
return Object.freeze([...this.#setupCapabilities.values()]);
|
|
198
|
+
}
|
|
164
199
|
}
|
|
165
200
|
function isWithin(plugin, root) {
|
|
166
201
|
return plugin === root || plugin.startsWith(`${root}/`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EnvStore } from './environment-store.js';
|
|
2
|
+
import type { RuntimeConfigDocument } from './config-composer.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generation-owned, validated Root configuration.
|
|
5
|
+
*
|
|
6
|
+
* `document` deliberately retains environment placeholders for safe display and
|
|
7
|
+
* persistence. Runtime consumers use `expanded`, which is derived exclusively
|
|
8
|
+
* through the Root EnvStore instead of reading process.env themselves.
|
|
9
|
+
*/
|
|
10
|
+
export interface PrimaryConfig {
|
|
11
|
+
readonly document: RuntimeConfigDocument;
|
|
12
|
+
readonly expanded: RuntimeConfigDocument;
|
|
13
|
+
get<T = unknown>(key: string): T | undefined;
|
|
14
|
+
}
|
|
15
|
+
export declare const primaryConfigToken: import("@zhin.js/plugin-runtime").Token<PrimaryConfig>;
|
|
16
|
+
export declare function createPrimaryConfig(document: RuntimeConfigDocument, environment: EnvStore): PrimaryConfig;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createToken } from '@zhin.js/plugin-runtime';
|
|
2
|
+
export const primaryConfigToken = createToken('zhin.primary-config', 'Validated Root configuration for the current generation');
|
|
3
|
+
export function createPrimaryConfig(document, environment) {
|
|
4
|
+
const safeDocument = freezeConfig(structuredClone(document));
|
|
5
|
+
const expanded = freezeConfig(environment.expandMissingAsEmpty(safeDocument));
|
|
6
|
+
return Object.freeze({
|
|
7
|
+
document: safeDocument,
|
|
8
|
+
expanded,
|
|
9
|
+
get(key) {
|
|
10
|
+
return expanded[key];
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function freezeConfig(value) {
|
|
15
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value))
|
|
16
|
+
return value;
|
|
17
|
+
for (const child of Object.values(value))
|
|
18
|
+
freezeConfig(child);
|
|
19
|
+
return Object.freeze(value);
|
|
20
|
+
}
|
package/lib/project-graph.d.ts
CHANGED
|
@@ -18,13 +18,14 @@ export interface ProjectGraph {
|
|
|
18
18
|
readonly packages: ReadonlyMap<string, ResolvedPackage>;
|
|
19
19
|
readonly buildOrder: readonly ResolvedPackage[];
|
|
20
20
|
}
|
|
21
|
+
export interface ProjectGraphServiceOptions {
|
|
22
|
+
readonly engineVersion?: string;
|
|
23
|
+
}
|
|
21
24
|
export declare class ProjectGraphError extends Error {
|
|
22
25
|
constructor(message: string);
|
|
23
26
|
}
|
|
24
27
|
export declare class ProjectGraphService {
|
|
25
28
|
#private;
|
|
26
|
-
|
|
27
|
-
private readonly engineVersion;
|
|
28
|
-
constructor(resolver: PackageResolver, engineVersion?: string);
|
|
29
|
+
constructor(resolver: PackageResolver, engineVersionOrOptions?: string | ProjectGraphServiceOptions);
|
|
29
30
|
inspect(projectRoot: string): Promise<ProjectGraph>;
|
|
30
31
|
}
|
package/lib/project-graph.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { childPluginId, rootPluginId } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import { assertFeatureApi, assertPackageEngine, runtimeEngineVersion, } from './compatibility.js';
|
|
3
3
|
import { PackageResolutionError, } from './package-resolver.js';
|
|
4
|
+
import { PLATFORM_FEATURE_CARRIER, PLATFORM_FEATURE_FACADE, declaredPackageDependency, mergeChildPluginReferences, mergeFeatureReferences, } from './platform-features.js';
|
|
4
5
|
export class ProjectGraphError extends Error {
|
|
5
6
|
constructor(message) {
|
|
6
7
|
super(message);
|
|
@@ -8,18 +9,20 @@ export class ProjectGraphError extends Error {
|
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
export class ProjectGraphService {
|
|
11
|
-
resolver;
|
|
12
|
-
engineVersion;
|
|
13
|
-
constructor(resolver,
|
|
14
|
-
this
|
|
15
|
-
this
|
|
12
|
+
#resolver;
|
|
13
|
+
#engineVersion;
|
|
14
|
+
constructor(resolver, engineVersionOrOptions = runtimeEngineVersion) {
|
|
15
|
+
this.#resolver = resolver;
|
|
16
|
+
this.#engineVersion = typeof engineVersionOrOptions === 'string'
|
|
17
|
+
? engineVersionOrOptions
|
|
18
|
+
: (engineVersionOrOptions.engineVersion ?? runtimeEngineVersion);
|
|
16
19
|
}
|
|
17
20
|
async inspect(projectRoot) {
|
|
18
|
-
const rootPackage = await this
|
|
21
|
+
const rootPackage = await this.#resolver.root(projectRoot);
|
|
19
22
|
assertPackageType(rootPackage, 'plugin');
|
|
20
23
|
const packages = new Map();
|
|
21
|
-
const root = await this.#visitPlugin(rootPackage, rootPluginId(), 'root', undefined, [], packages);
|
|
22
|
-
for (const pkg of this
|
|
24
|
+
const root = await this.#visitPlugin(rootPackage, rootPluginId(), 'root', undefined, [], packages, true);
|
|
25
|
+
for (const pkg of this.#resolver.workspacePackages())
|
|
23
26
|
addPackage(packages, pkg);
|
|
24
27
|
return Object.freeze({
|
|
25
28
|
root,
|
|
@@ -27,39 +30,62 @@ export class ProjectGraphService {
|
|
|
27
30
|
buildOrder: topologicalBuildOrder(packages),
|
|
28
31
|
});
|
|
29
32
|
}
|
|
30
|
-
async #visitPlugin(pkg, id, instanceKey, parent, ancestors, packages) {
|
|
33
|
+
async #visitPlugin(pkg, id, instanceKey, parent, ancestors, packages, isRoot) {
|
|
31
34
|
if (ancestors.includes(pkg.root)) {
|
|
32
35
|
throw new ProjectGraphError(`Plugin cycle detected: ${[...ancestors, pkg.root].join(' -> ')}`);
|
|
33
36
|
}
|
|
34
37
|
const manifest = assertPackageType(pkg, 'plugin');
|
|
35
|
-
assertPackageEngine(pkg, this
|
|
38
|
+
assertPackageEngine(pkg, this.#engineVersion);
|
|
36
39
|
addPackage(packages, pkg);
|
|
40
|
+
let featureCarrier;
|
|
41
|
+
let pluginFacade;
|
|
42
|
+
let featureRefs = manifest.features;
|
|
43
|
+
let pluginRefs = manifest.plugins;
|
|
44
|
+
if (isRoot && manifest.platformFeatures !== false) {
|
|
45
|
+
const platform = await tryResolvePlatform(this.#resolver, pkg);
|
|
46
|
+
featureCarrier = platform.featureCarrier;
|
|
47
|
+
pluginFacade = platform.pluginFacade;
|
|
48
|
+
if (featureCarrier) {
|
|
49
|
+
addPackage(packages, featureCarrier);
|
|
50
|
+
const carrierManifest = featureCarrier.packageJson.zhin;
|
|
51
|
+
const inherited = carrierManifest.type === 'plugin' ? carrierManifest.features : [];
|
|
52
|
+
featureRefs = mergeFeatureReferences(manifest.features, inherited);
|
|
53
|
+
}
|
|
54
|
+
if (pluginFacade) {
|
|
55
|
+
addPackage(packages, pluginFacade);
|
|
56
|
+
const facadeManifest = pluginFacade.packageJson.zhin;
|
|
57
|
+
const inheritedPlugins = facadeManifest.type === 'plugin' ? facadeManifest.plugins : [];
|
|
58
|
+
pluginRefs = mergeChildPluginReferences(manifest.plugins, inheritedPlugins);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
37
61
|
const featurePackages = new Set();
|
|
38
|
-
const features = await Promise.all(
|
|
62
|
+
const features = await Promise.all(featureRefs.map(async (reference) => {
|
|
39
63
|
if (featurePackages.has(reference.package)) {
|
|
40
64
|
throw new ProjectGraphError(`Duplicate Feature requirement ${reference.package} in ${pkg.name}`);
|
|
41
65
|
}
|
|
42
66
|
featurePackages.add(reference.package);
|
|
43
|
-
const resolved = await
|
|
67
|
+
const resolved = await this.#resolveFeatureReference(pkg, reference, featureCarrier);
|
|
44
68
|
if (!resolved)
|
|
45
69
|
return undefined;
|
|
46
70
|
assertPackageType(resolved, 'feature');
|
|
47
|
-
assertPackageEngine(resolved, this
|
|
71
|
+
assertPackageEngine(resolved, this.#engineVersion);
|
|
48
72
|
assertFeatureApi(pkg, reference, resolved);
|
|
49
73
|
addPackage(packages, resolved);
|
|
50
74
|
return Object.freeze({ reference, package: resolved });
|
|
51
75
|
}));
|
|
76
|
+
const ownPluginKeys = new Set(manifest.plugins.map((item) => item.instanceKey));
|
|
52
77
|
const instanceKeys = new Set();
|
|
53
|
-
const children = await Promise.all(
|
|
78
|
+
const children = await Promise.all(pluginRefs.map(async (reference) => {
|
|
54
79
|
if (instanceKeys.has(reference.instanceKey)) {
|
|
55
80
|
throw new ProjectGraphError(`Duplicate child instanceKey ${reference.instanceKey} in ${pkg.name}`);
|
|
56
81
|
}
|
|
57
82
|
instanceKeys.add(reference.instanceKey);
|
|
58
|
-
const
|
|
83
|
+
const resolveFrom = ownPluginKeys.has(reference.instanceKey) ? pkg : (pluginFacade ?? pkg);
|
|
84
|
+
const resolved = await resolveReference(this.#resolver, resolveFrom, reference);
|
|
59
85
|
if (!resolved)
|
|
60
86
|
return undefined;
|
|
61
87
|
assertPackageType(resolved, 'plugin');
|
|
62
|
-
return this.#visitPlugin(resolved, childPluginId(id, reference.instanceKey), reference.instanceKey, id, [...ancestors, pkg.root], packages);
|
|
88
|
+
return this.#visitPlugin(resolved, childPluginId(id, reference.instanceKey), reference.instanceKey, id, [...ancestors, pkg.root], packages, false);
|
|
63
89
|
}));
|
|
64
90
|
return Object.freeze({
|
|
65
91
|
id,
|
|
@@ -70,15 +96,62 @@ export class ProjectGraphService {
|
|
|
70
96
|
children: Object.freeze(children.filter(isDefined)),
|
|
71
97
|
});
|
|
72
98
|
}
|
|
99
|
+
async #resolveFeatureReference(pkg, reference, featureCarrier) {
|
|
100
|
+
try {
|
|
101
|
+
return await this.#resolver.resolve(reference.package, pkg);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// 非解析类错误(损坏的 package.json 等)不回退、不容错,直接上抛。
|
|
105
|
+
if (!(error instanceof PackageResolutionError))
|
|
106
|
+
throw error;
|
|
107
|
+
// carrier 回退:继承自 @zhin.js/core 的 Feature 引用声明在 carrier 上,
|
|
108
|
+
// 从引用包解析失败(未声明 / 未安装 / workspace 链接缺失)时改从 carrier 解析。
|
|
109
|
+
if (featureCarrier)
|
|
110
|
+
return resolveReference(this.#resolver, featureCarrier, reference);
|
|
111
|
+
// 无 carrier 时按统一规则处理:optional 引用容错,其余上抛。
|
|
112
|
+
if (reference.optional)
|
|
113
|
+
return undefined;
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function tryResolvePlatform(resolver, from) {
|
|
119
|
+
let featureCarrier;
|
|
120
|
+
let pluginFacade;
|
|
121
|
+
const directCore = declaredPackageDependency(from.packageJson.dependencies, from.packageJson.optionalDependencies, PLATFORM_FEATURE_CARRIER);
|
|
122
|
+
if (directCore) {
|
|
123
|
+
try {
|
|
124
|
+
featureCarrier = await resolver.resolve(PLATFORM_FEATURE_CARRIER, from);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (!(error instanceof PackageResolutionError))
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const facadeDecl = declaredPackageDependency(from.packageJson.dependencies, from.packageJson.optionalDependencies, PLATFORM_FEATURE_FACADE);
|
|
132
|
+
if (facadeDecl) {
|
|
133
|
+
try {
|
|
134
|
+
pluginFacade = await resolver.resolve(PLATFORM_FEATURE_FACADE, from);
|
|
135
|
+
if (!featureCarrier) {
|
|
136
|
+
featureCarrier = await resolver.resolve(PLATFORM_FEATURE_CARRIER, pluginFacade);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!(error instanceof PackageResolutionError))
|
|
141
|
+
throw error;
|
|
142
|
+
pluginFacade = undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { featureCarrier, pluginFacade };
|
|
73
146
|
}
|
|
74
147
|
async function resolveReference(resolver, from, reference) {
|
|
75
148
|
try {
|
|
76
149
|
return await resolver.resolve(reference.package, from);
|
|
77
150
|
}
|
|
78
151
|
catch (error) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
152
|
+
// optional 引用对所有 PackageResolutionError 统一容错(未声明 / 未安装 /
|
|
153
|
+
// workspace 链接缺失);非解析类错误(损坏的 package.json 等)继续上抛。
|
|
154
|
+
if (reference.optional && error instanceof PackageResolutionError)
|
|
82
155
|
return undefined;
|
|
83
156
|
throw error;
|
|
84
157
|
}
|
package/lib/root-runtime.js
CHANGED
|
@@ -12,11 +12,12 @@ import { GenerationAssets } from './generation-assets.js';
|
|
|
12
12
|
import { NodeDiscoveryHost } from './node-discovery-host.js';
|
|
13
13
|
import { NodePackageResolver } from './package-resolver.js';
|
|
14
14
|
import { PluginScopeAssembler, } from './plugin-scope-assembler.js';
|
|
15
|
-
import { ProjectGraphService } from './project-graph.js';
|
|
15
|
+
import { ProjectGraphService, } from './project-graph.js';
|
|
16
16
|
import { HmrCoordinator } from './hmr-coordinator.js';
|
|
17
17
|
import { RootProcessRestartExecutor, } from './process-restart.js';
|
|
18
18
|
import { SlotGenerationPreparer } from './slot-generation-preparer.js';
|
|
19
19
|
import { SourceOwnershipIndex } from './source-ownership.js';
|
|
20
|
+
import { addCapabilitySlot, featureSetupAliases, mergeSetupCapabilities, } from './setup-capabilities.js';
|
|
20
21
|
import { SubtreeGenerationPreparer, SubtreeTopologyChangedError, } from './subtree-generation-preparer.js';
|
|
21
22
|
import { TopologyGenerationPreparer } from './topology-generation-preparer.js';
|
|
22
23
|
import { RestartBoundaryPlanner } from './restart-boundary.js';
|
|
@@ -118,7 +119,7 @@ export class RootRuntime {
|
|
|
118
119
|
restart = new RestartBoundaryPlanner().plan(this.#model.graph, inspected.graph, plan.changed);
|
|
119
120
|
if (restart)
|
|
120
121
|
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
|
+
prepared = await new TopologyGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current);
|
|
122
123
|
}
|
|
123
124
|
else if (plan.subtrees.length === 0 && plan.slots.length > 0 && this.#model) {
|
|
124
125
|
prepared = await new SlotGenerationPreparer(this.#modules, this.#model)
|
|
@@ -168,12 +169,36 @@ export class RootRuntime {
|
|
|
168
169
|
async #inspectProject() {
|
|
169
170
|
const resolver = await NodePackageResolver.create(this.#projectRoot);
|
|
170
171
|
const graph = await new ProjectGraphService(resolver).inspect(this.#projectRoot);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
await this.#refreshConfigDocument();
|
|
173
|
+
if (this.#configResolver) {
|
|
174
|
+
return {
|
|
175
|
+
graph,
|
|
176
|
+
configResolver: this.#configResolver,
|
|
177
|
+
primaryConfigDocument: Object.freeze({}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const composed = await new ConfigComposer().compose(graph, this.#configDocument);
|
|
181
|
+
return {
|
|
182
|
+
graph,
|
|
183
|
+
configResolver: this.#configViewResolver(composed.views),
|
|
184
|
+
primaryConfigDocument: composed.document,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The config file itself is watched, so an external edit triggers a full
|
|
189
|
+
* reload. Re-read through the port before composing: without this the reload
|
|
190
|
+
* would rebuild the whole generation from the stale in-memory document read
|
|
191
|
+
* at start, the edit would silently not apply, and the next patchConfig
|
|
192
|
+
* would hit a revision conflict. On drift the file is authoritative.
|
|
193
|
+
*/
|
|
194
|
+
async #refreshConfigDocument() {
|
|
195
|
+
if (!this.#configPort)
|
|
196
|
+
return;
|
|
197
|
+
const snapshot = await this.#configPort.read();
|
|
198
|
+
if (snapshot.revision === this.#configSnapshot?.revision)
|
|
199
|
+
return;
|
|
200
|
+
this.#configSnapshot = snapshot;
|
|
201
|
+
this.#configDocument = structuredClone(snapshot.document);
|
|
177
202
|
}
|
|
178
203
|
#configViewResolver(views) {
|
|
179
204
|
const env = createEnvStore(rootPluginId(), this.#environment, this.#environmentLayers);
|
|
@@ -188,7 +213,10 @@ export class RootRuntime {
|
|
|
188
213
|
if (!this.#configDocument) {
|
|
189
214
|
throw new Error('Config patches require a document-backed RootRuntime config');
|
|
190
215
|
}
|
|
191
|
-
|
|
216
|
+
// Adopt any external edit before planning so the port's revision check
|
|
217
|
+
// cannot conflict and the patch applies on top of the on-disk document.
|
|
218
|
+
await this.#refreshConfigDocument();
|
|
219
|
+
const currentDocument = requireConfigDocument(this.#configDocument);
|
|
192
220
|
let plan;
|
|
193
221
|
let prepared;
|
|
194
222
|
let documentTransaction;
|
|
@@ -200,33 +228,51 @@ export class RootRuntime {
|
|
|
200
228
|
plan = planned;
|
|
201
229
|
if (!planned.documentChanged)
|
|
202
230
|
return undefined;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
231
|
+
try {
|
|
232
|
+
if (this.#configPort) {
|
|
233
|
+
const currentSnapshot = requireConfigDocumentSnapshot(this.#configSnapshot);
|
|
234
|
+
// Port preparation must remain inert; validation and shadow setup can
|
|
235
|
+
// still reject this candidate without touching the backing document.
|
|
236
|
+
documentTransaction = await this.#configPort.prepare(currentSnapshot, patches);
|
|
237
|
+
if (!isDeepStrictEqual(documentTransaction.document, planned.candidate)) {
|
|
238
|
+
throw new ConfigDocumentDivergenceError();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Host-level sections (http/database/ai/...) never land in a Plugin
|
|
242
|
+
// view, so their patches plan zero roots. With a Root Resource
|
|
243
|
+
// installer the whole generation must still be rebuilt; only an
|
|
244
|
+
// installer-less runtime may take the commit-only shortcut.
|
|
245
|
+
if (!this.#installResources && planned.roots.length === 0) {
|
|
246
|
+
if (documentTransaction)
|
|
247
|
+
committedDocument = await documentTransaction.commit();
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const inspected = {
|
|
251
|
+
graph,
|
|
252
|
+
configResolver: this.#configViewResolver(planned.views),
|
|
253
|
+
primaryConfigDocument: planned.document,
|
|
254
|
+
};
|
|
255
|
+
// Root resources may consume any Primary Config section. Reinstall them
|
|
256
|
+
// when present so a committed patch cannot leave Host services on the
|
|
257
|
+
// previous generation's document.
|
|
258
|
+
if (!this.#installResources && this.#model && !planned.roots.includes(rootPluginId())) {
|
|
259
|
+
prepared = await this.#prepareSubtrees(current, inspected, planned.roots);
|
|
210
260
|
}
|
|
261
|
+
else {
|
|
262
|
+
prepared = await this.#prepareInspected(current, inspected);
|
|
263
|
+
}
|
|
264
|
+
return documentTransaction
|
|
265
|
+
? withConfigDocumentHandoff(prepared.generation, documentTransaction, (committed) => { committedDocument = committed; })
|
|
266
|
+
: prepared.generation;
|
|
211
267
|
}
|
|
212
|
-
|
|
268
|
+
catch (error) {
|
|
269
|
+
// A prepared document transaction is inert until handoff; roll it back
|
|
270
|
+
// before any shadow-phase failure escapes so the port never leaks a
|
|
271
|
+
// pending write.
|
|
213
272
|
if (documentTransaction)
|
|
214
|
-
|
|
215
|
-
|
|
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);
|
|
273
|
+
await documentTransaction.rollback().catch(() => undefined);
|
|
274
|
+
throw error;
|
|
226
275
|
}
|
|
227
|
-
return documentTransaction
|
|
228
|
-
? withConfigDocumentHandoff(prepared.generation, documentTransaction, (committed) => { committedDocument = committed; })
|
|
229
|
-
: prepared.generation;
|
|
230
276
|
});
|
|
231
277
|
const completed = requireConfigPatchPlan(plan);
|
|
232
278
|
if (prepared)
|
|
@@ -237,14 +283,14 @@ export class RootRuntime {
|
|
|
237
283
|
return snapshot;
|
|
238
284
|
}
|
|
239
285
|
#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);
|
|
286
|
+
const assembler = new GenerationAssembler(inspected.graph, this.#modules, inspected.configResolver, inspected.primaryConfigDocument, current.generation + 1, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation);
|
|
241
287
|
return assembler.prepare();
|
|
242
288
|
}
|
|
243
289
|
async #prepareSubtrees(current, inspected, roots) {
|
|
244
290
|
if (!this.#model)
|
|
245
291
|
return this.#prepareInspected(current, inspected);
|
|
246
292
|
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);
|
|
293
|
+
return await new SubtreeGenerationPreparer(this.#modules, this.#model, inspected.graph, inspected.configResolver, inspected.primaryConfigDocument, this.#environment, this.#installResources, this.#environmentLayers, this.#isolation).prepare(current, roots);
|
|
248
294
|
}
|
|
249
295
|
catch (error) {
|
|
250
296
|
if (!(error instanceof SubtreeTopologyChangedError))
|
|
@@ -278,6 +324,11 @@ function requireConfigDocumentSnapshot(snapshot) {
|
|
|
278
324
|
throw new Error('ConfigDocumentPort has not been read');
|
|
279
325
|
return snapshot;
|
|
280
326
|
}
|
|
327
|
+
function requireConfigDocument(document) {
|
|
328
|
+
if (!document)
|
|
329
|
+
throw new Error('Config patches require a document-backed RootRuntime config');
|
|
330
|
+
return document;
|
|
331
|
+
}
|
|
281
332
|
function cloneConfigPatches(patches) {
|
|
282
333
|
return Object.freeze(patches.map((patch) => Object.freeze(patch.op === 'set'
|
|
283
334
|
? { ...patch, path: Object.freeze([...patch.path]), value: structuredClone(patch.value) }
|
|
@@ -292,6 +343,7 @@ class GenerationAssembler {
|
|
|
292
343
|
graph;
|
|
293
344
|
modules;
|
|
294
345
|
configResolver;
|
|
346
|
+
primaryConfigDocument;
|
|
295
347
|
generation;
|
|
296
348
|
environment;
|
|
297
349
|
installResources;
|
|
@@ -301,31 +353,35 @@ class GenerationAssembler {
|
|
|
301
353
|
#catalog = new FeatureCatalog();
|
|
302
354
|
#rootsByFeature = new Map();
|
|
303
355
|
#featureIdsByPackageRoot = new Map();
|
|
304
|
-
#projectionDisposers =
|
|
356
|
+
#projectionDisposers = new Map();
|
|
305
357
|
#host;
|
|
306
358
|
#plugins;
|
|
307
|
-
constructor(graph, modules, configResolver, generation, environment, installResources, environmentLayers = {}, isolation) {
|
|
359
|
+
constructor(graph, modules, configResolver, primaryConfigDocument, generation, environment, installResources, environmentLayers = {}, isolation) {
|
|
308
360
|
this.graph = graph;
|
|
309
361
|
this.modules = modules;
|
|
310
362
|
this.configResolver = configResolver;
|
|
363
|
+
this.primaryConfigDocument = primaryConfigDocument;
|
|
311
364
|
this.generation = generation;
|
|
312
365
|
this.environment = environment;
|
|
313
366
|
this.installResources = installResources;
|
|
314
367
|
this.environmentLayers = environmentLayers;
|
|
315
368
|
this.isolation = isolation;
|
|
316
369
|
this.#host = new NodeDiscoveryHost(modules);
|
|
317
|
-
this.#plugins = new PluginScopeAssembler(modules, configResolver, environment, installResources, environmentLayers, undefined, isolation);
|
|
370
|
+
this.#plugins = new PluginScopeAssembler(modules, configResolver, environment, primaryConfigDocument, installResources, environmentLayers, undefined, isolation);
|
|
318
371
|
}
|
|
319
372
|
async prepare() {
|
|
320
373
|
try {
|
|
321
374
|
// Prepare is deliberately ordered: providers define discovery, setup
|
|
322
375
|
// creates owner scopes, then definitions can be projected against both.
|
|
323
376
|
await this.#loadProviders(this.graph.root);
|
|
377
|
+
this.#plugins.installSetupFeatureAliases(featureSetupAliases(this.#catalog.values()));
|
|
324
378
|
await this.#plugins.setupTree(this.graph.root);
|
|
325
379
|
await this.#discover();
|
|
326
380
|
const projected = await new FeatureProjector(this.#catalog.values())
|
|
327
381
|
.project(this.generation, this.#projectionState());
|
|
328
|
-
|
|
382
|
+
for (const [feature, dispose] of projected.disposers) {
|
|
383
|
+
this.#projectionDisposers.set(feature, dispose);
|
|
384
|
+
}
|
|
329
385
|
const state = projected.state;
|
|
330
386
|
const snapshot = createSnapshotView(this.generation, state);
|
|
331
387
|
const ownership = SourceOwnershipIndex.fromGeneration(this.graph, snapshot, this.#featureIdsByPackageRoot);
|
|
@@ -351,7 +407,7 @@ class GenerationAssembler {
|
|
|
351
407
|
};
|
|
352
408
|
}
|
|
353
409
|
catch (error) {
|
|
354
|
-
await disposePreparedParts(this.#plugins.createdScopeDisposers().map(([, dispose]) => dispose), this.#projectionDisposers, error);
|
|
410
|
+
await disposePreparedParts(this.#plugins.createdScopeDisposers().map(([, dispose]) => dispose), [...this.#projectionDisposers.values()], error);
|
|
355
411
|
throw error;
|
|
356
412
|
}
|
|
357
413
|
}
|
|
@@ -378,12 +434,13 @@ class GenerationAssembler {
|
|
|
378
434
|
await this.#loadProviders(child);
|
|
379
435
|
}
|
|
380
436
|
async #discover() {
|
|
437
|
+
mergeSetupCapabilities(this.#capabilities, this.#plugins.setupCapabilities(), new Map(this.#catalog.values().map((provider) => [provider.id, provider])), this.#rootsByFeature);
|
|
381
438
|
const discovery = new FeatureDiscovery(this.#host);
|
|
382
439
|
for (const provider of this.#catalog.values()) {
|
|
383
440
|
const roots = this.#rootsByFeature.get(provider.id) ?? [];
|
|
384
441
|
const slots = await discovery.discover(provider, roots);
|
|
385
442
|
for (const slot of slots)
|
|
386
|
-
this.#capabilities
|
|
443
|
+
addCapabilitySlot(this.#capabilities, slot);
|
|
387
444
|
}
|
|
388
445
|
}
|
|
389
446
|
#projectionState() {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CapabilityId, type CapabilitySlot, type FeatureId, type SetupCapabilityRegistration } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { CapabilityRoot, FeatureProvider } from '@zhin.js/feature-kit';
|
|
3
|
+
export declare function mergeSetupCapabilities(capabilities: Map<CapabilityId, CapabilitySlot>, registrations: readonly Readonly<SetupCapabilityRegistration>[], providers: ReadonlyMap<FeatureId, FeatureProvider>, rootsByFeature: ReadonlyMap<FeatureId, readonly CapabilityRoot[]>): void;
|
|
4
|
+
export declare function addCapabilitySlot(capabilities: Map<CapabilityId, CapabilitySlot>, slot: Readonly<CapabilitySlot>): void;
|
|
5
|
+
export declare function featureSetupAliases(providers: Iterable<FeatureProvider>): ReadonlyMap<string, FeatureId>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createCapabilitySlot, } from '@zhin.js/plugin-runtime';
|
|
2
|
+
export function mergeSetupCapabilities(capabilities, registrations, providers, rootsByFeature) {
|
|
3
|
+
for (const registration of registrations) {
|
|
4
|
+
const provider = providers.get(registration.feature);
|
|
5
|
+
const mounted = rootsByFeature.get(registration.feature)?.some((root) => root.owner === registration.owner);
|
|
6
|
+
if (!provider || !mounted) {
|
|
7
|
+
throw new Error(`Feature ${registration.feature} is not mounted for Plugin ${registration.owner}`);
|
|
8
|
+
}
|
|
9
|
+
const definition = provider.authoring.validate(registration.definition, {
|
|
10
|
+
owner: registration.owner,
|
|
11
|
+
feature: registration.feature,
|
|
12
|
+
localName: registration.localName,
|
|
13
|
+
source: registration.source,
|
|
14
|
+
});
|
|
15
|
+
addCapabilitySlot(capabilities, createCapabilitySlot({
|
|
16
|
+
owner: registration.owner,
|
|
17
|
+
feature: registration.feature,
|
|
18
|
+
localName: registration.localName,
|
|
19
|
+
source: registration.source,
|
|
20
|
+
definition,
|
|
21
|
+
origin: 'setup',
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function addCapabilitySlot(capabilities, slot) {
|
|
26
|
+
if (capabilities.has(slot.id)) {
|
|
27
|
+
throw new Error(`Duplicate Capability Slot: ${slot.id}`);
|
|
28
|
+
}
|
|
29
|
+
capabilities.set(slot.id, slot);
|
|
30
|
+
}
|
|
31
|
+
export function featureSetupAliases(providers) {
|
|
32
|
+
const aliases = new Map();
|
|
33
|
+
for (const provider of providers) {
|
|
34
|
+
const method = provider.authoring.setupMethod;
|
|
35
|
+
if (!method)
|
|
36
|
+
continue;
|
|
37
|
+
const existing = aliases.get(method);
|
|
38
|
+
if (existing && existing !== provider.id) {
|
|
39
|
+
throw new Error(`Duplicate Feature setup method ${method}: ${existing}, ${provider.id}`);
|
|
40
|
+
}
|
|
41
|
+
aliases.set(method, provider.id);
|
|
42
|
+
}
|
|
43
|
+
return aliases;
|
|
44
|
+
}
|