@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,23 @@
|
|
|
1
|
+
import { type PackageJson } from './manifest.js';
|
|
2
|
+
export interface ResolvedPackage {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly root: string;
|
|
5
|
+
readonly packageJson: PackageJson;
|
|
6
|
+
readonly source: 'workspace' | 'node_modules';
|
|
7
|
+
}
|
|
8
|
+
export interface PackageResolver {
|
|
9
|
+
root(root: string): Promise<ResolvedPackage>;
|
|
10
|
+
resolve(request: string, from: ResolvedPackage): Promise<ResolvedPackage>;
|
|
11
|
+
workspacePackages(): readonly ResolvedPackage[];
|
|
12
|
+
}
|
|
13
|
+
export declare class PackageResolutionError extends Error {
|
|
14
|
+
readonly request?: string | undefined;
|
|
15
|
+
constructor(message: string, request?: string | undefined);
|
|
16
|
+
}
|
|
17
|
+
export declare class NodePackageResolver implements PackageResolver {
|
|
18
|
+
#private;
|
|
19
|
+
static create(projectRoot: string): Promise<NodePackageResolver>;
|
|
20
|
+
root(root: string): Promise<ResolvedPackage>;
|
|
21
|
+
workspacePackages(): readonly ResolvedPackage[];
|
|
22
|
+
resolve(request: string, from: ResolvedPackage): Promise<ResolvedPackage>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { access, readFile, readdir, realpath } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { parsePackageJson } from './manifest.js';
|
|
4
|
+
export class PackageResolutionError extends Error {
|
|
5
|
+
request;
|
|
6
|
+
constructor(message, request) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.request = request;
|
|
9
|
+
this.name = 'PackageResolutionError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class NodePackageResolver {
|
|
13
|
+
#workspaceByName = new Map();
|
|
14
|
+
#cache = new Map();
|
|
15
|
+
static async create(projectRoot) {
|
|
16
|
+
const resolver = new NodePackageResolver();
|
|
17
|
+
const root = await resolver.#readPackage(projectRoot, 'workspace');
|
|
18
|
+
resolver.#workspaceByName.set(root.name, root);
|
|
19
|
+
for (const directory of ['packages', 'plugins']) {
|
|
20
|
+
const parent = join(projectRoot, directory);
|
|
21
|
+
for (const entry of await safeReadDirectories(parent)) {
|
|
22
|
+
const packageRoot = join(parent, entry);
|
|
23
|
+
if (await exists(join(packageRoot, 'pnpm-workspace.yaml'))) {
|
|
24
|
+
throw new PackageResolutionError(`Nested workspace is not allowed: ${packageRoot}`);
|
|
25
|
+
}
|
|
26
|
+
if (directory === 'plugins' && await exists(join(packageRoot, 'plugins'))) {
|
|
27
|
+
throw new PackageResolutionError(`Nested local Plugin directory is not allowed: ${join(packageRoot, 'plugins')}`);
|
|
28
|
+
}
|
|
29
|
+
if (!await exists(join(packageRoot, 'package.json')))
|
|
30
|
+
continue;
|
|
31
|
+
const pkg = await resolver.#readPackage(packageRoot, 'workspace');
|
|
32
|
+
if (resolver.#workspaceByName.has(pkg.name)) {
|
|
33
|
+
throw new PackageResolutionError(`Duplicate workspace package: ${pkg.name}`);
|
|
34
|
+
}
|
|
35
|
+
resolver.#workspaceByName.set(pkg.name, pkg);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return resolver;
|
|
39
|
+
}
|
|
40
|
+
async root(root) {
|
|
41
|
+
return this.#readPackage(root, 'workspace');
|
|
42
|
+
}
|
|
43
|
+
workspacePackages() {
|
|
44
|
+
return [...this.#workspaceByName.values()];
|
|
45
|
+
}
|
|
46
|
+
async resolve(request, from) {
|
|
47
|
+
const specification = declaredDependency(request, from.packageJson);
|
|
48
|
+
const workspace = this.#workspaceByName.get(request);
|
|
49
|
+
if (specification.startsWith('workspace:')) {
|
|
50
|
+
if (workspace)
|
|
51
|
+
return workspace;
|
|
52
|
+
// Examples live outside the monorepo packages/plugins scan roots; pnpm still
|
|
53
|
+
// links workspace:* into node_modules, so fall through before failing.
|
|
54
|
+
}
|
|
55
|
+
else if (workspace) {
|
|
56
|
+
return workspace;
|
|
57
|
+
}
|
|
58
|
+
let current = from.root;
|
|
59
|
+
while (true) {
|
|
60
|
+
const packageRoot = join(current, 'node_modules', ...request.split('/'));
|
|
61
|
+
if (await exists(join(packageRoot, 'package.json'))) {
|
|
62
|
+
return this.#readPackage(packageRoot, 'node_modules');
|
|
63
|
+
}
|
|
64
|
+
const parent = dirname(current);
|
|
65
|
+
if (parent === current)
|
|
66
|
+
break;
|
|
67
|
+
current = parent;
|
|
68
|
+
}
|
|
69
|
+
throw new PackageResolutionError(specification.startsWith('workspace:')
|
|
70
|
+
? `Workspace dependency ${request} declared by ${from.name} is missing`
|
|
71
|
+
: `Cannot resolve ${request} from ${from.name}`, request);
|
|
72
|
+
}
|
|
73
|
+
async #readPackage(packageRoot, source) {
|
|
74
|
+
const normalized = await realpath(resolve(packageRoot));
|
|
75
|
+
const cached = this.#cache.get(normalized);
|
|
76
|
+
if (cached)
|
|
77
|
+
return cached;
|
|
78
|
+
const file = join(normalized, 'package.json');
|
|
79
|
+
const content = await readFile(file, 'utf8');
|
|
80
|
+
const packageJson = parsePackageJson(JSON.parse(content), file);
|
|
81
|
+
const result = Object.freeze({
|
|
82
|
+
name: packageJson.name,
|
|
83
|
+
root: normalized,
|
|
84
|
+
packageJson,
|
|
85
|
+
source,
|
|
86
|
+
});
|
|
87
|
+
this.#cache.set(normalized, result);
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function declaredDependency(request, pkg) {
|
|
92
|
+
const specification = (pkg.dependencies?.[request]
|
|
93
|
+
?? pkg.optionalDependencies?.[request]);
|
|
94
|
+
if (!specification) {
|
|
95
|
+
throw new PackageResolutionError(`${pkg.name} references ${request} in zhin manifest but does not declare it as a package dependency`, request);
|
|
96
|
+
}
|
|
97
|
+
return specification;
|
|
98
|
+
}
|
|
99
|
+
async function safeReadDirectories(parent) {
|
|
100
|
+
try {
|
|
101
|
+
const entries = await readdir(parent, { withFileTypes: true });
|
|
102
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (isNotFound(error))
|
|
106
|
+
return [];
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function exists(path) {
|
|
111
|
+
try {
|
|
112
|
+
await access(path);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function isNotFound(error) {
|
|
120
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
121
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DisposeStack, Scope, type Dispose, type GenerationHandoff, type GenerationHandoffRegistry, type PluginId, type PluginNodeSnapshot, type TokenId } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { type RuntimeEnvironment } from './environment.js';
|
|
3
|
+
import { type EnvironmentLayers } from './environment-store.js';
|
|
4
|
+
import type { ModuleRuntime } from './module-runtime.js';
|
|
5
|
+
import type { PluginGraphNode } from './project-graph.js';
|
|
6
|
+
import type { IsolatedPluginRuntimePort } from './isolation.js';
|
|
7
|
+
export type PluginConfigResolver = (node: PluginGraphNode) => unknown;
|
|
8
|
+
export interface RootResourceContext {
|
|
9
|
+
readonly resources: Scope;
|
|
10
|
+
readonly lifecycle: DisposeStack;
|
|
11
|
+
readonly handoff: GenerationHandoffRegistry;
|
|
12
|
+
}
|
|
13
|
+
export type RootResourceInstaller = (context: RootResourceContext) => void | Promise<void>;
|
|
14
|
+
export interface PluginAssemblySeed {
|
|
15
|
+
readonly scopes: ReadonlyMap<PluginId, Scope>;
|
|
16
|
+
readonly tree: ReadonlyMap<PluginId, PluginNodeSnapshot>;
|
|
17
|
+
readonly config: ReadonlyMap<PluginId, unknown>;
|
|
18
|
+
readonly resources: ReadonlyMap<PluginId, ReadonlyMap<TokenId, unknown>>;
|
|
19
|
+
}
|
|
20
|
+
/** Assembles Plugin setup into mutable shadow maps without publishing them. */
|
|
21
|
+
export declare class PluginScopeAssembler {
|
|
22
|
+
#private;
|
|
23
|
+
private readonly modules;
|
|
24
|
+
private readonly configResolver;
|
|
25
|
+
private readonly environment;
|
|
26
|
+
private readonly installResources?;
|
|
27
|
+
private readonly isolation?;
|
|
28
|
+
readonly scopes: Map<PluginId, Scope>;
|
|
29
|
+
readonly tree: Map<PluginId, PluginNodeSnapshot>;
|
|
30
|
+
readonly config: Map<PluginId, unknown>;
|
|
31
|
+
readonly resources: Map<PluginId, ReadonlyMap<TokenId, unknown>>;
|
|
32
|
+
constructor(modules: ModuleRuntime, configResolver: PluginConfigResolver, environment: RuntimeEnvironment, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, seed?: PluginAssemblySeed, isolation?: IsolatedPluginRuntimePort | undefined);
|
|
33
|
+
removeSubtrees(roots: readonly PluginId[]): void;
|
|
34
|
+
setupTree(node: PluginGraphNode): Promise<void>;
|
|
35
|
+
synchronizeTree(node: PluginGraphNode): void;
|
|
36
|
+
createdScopeDisposers(): readonly (readonly [PluginId, Dispose])[];
|
|
37
|
+
generationHandoff(): GenerationHandoff | undefined;
|
|
38
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { GenerationHandoffStack, Scope, rootPluginId, } from '@zhin.js/plugin-runtime';
|
|
3
|
+
import { runtimeEnvironmentToken } from './environment.js';
|
|
4
|
+
import { EnvStoreFactory, envStoreToken, } from './environment-store.js';
|
|
5
|
+
/** Assembles Plugin setup into mutable shadow maps without publishing them. */
|
|
6
|
+
export class PluginScopeAssembler {
|
|
7
|
+
modules;
|
|
8
|
+
configResolver;
|
|
9
|
+
environment;
|
|
10
|
+
installResources;
|
|
11
|
+
isolation;
|
|
12
|
+
scopes;
|
|
13
|
+
tree;
|
|
14
|
+
config;
|
|
15
|
+
resources;
|
|
16
|
+
#created = [];
|
|
17
|
+
#handoffs = new GenerationHandoffStack();
|
|
18
|
+
#envStores;
|
|
19
|
+
constructor(modules, configResolver, environment, installResources, environmentLayers = {}, seed, isolation) {
|
|
20
|
+
this.modules = modules;
|
|
21
|
+
this.configResolver = configResolver;
|
|
22
|
+
this.environment = environment;
|
|
23
|
+
this.installResources = installResources;
|
|
24
|
+
this.isolation = isolation;
|
|
25
|
+
this.#envStores = new EnvStoreFactory(environment, environmentLayers);
|
|
26
|
+
this.scopes = new Map(seed?.scopes);
|
|
27
|
+
this.tree = new Map(seed?.tree);
|
|
28
|
+
this.config = new Map(seed?.config);
|
|
29
|
+
this.resources = new Map(seed?.resources);
|
|
30
|
+
}
|
|
31
|
+
removeSubtrees(roots) {
|
|
32
|
+
// Seeded Scopes belong to a committed generation. Removing a map entry
|
|
33
|
+
// only prepares the shadow view; GenerationAssets owns eventual disposal.
|
|
34
|
+
for (const owner of [...this.scopes.keys()]) {
|
|
35
|
+
if (!roots.some((root) => isWithin(owner, root)))
|
|
36
|
+
continue;
|
|
37
|
+
this.scopes.delete(owner);
|
|
38
|
+
this.tree.delete(owner);
|
|
39
|
+
this.config.delete(owner);
|
|
40
|
+
this.resources.delete(owner);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async setupTree(node) {
|
|
44
|
+
const manifest = node.package.packageJson.zhin;
|
|
45
|
+
const parentScope = node.parent ? this.scopes.get(node.parent) : undefined;
|
|
46
|
+
if (node.parent && !parentScope)
|
|
47
|
+
throw new Error(`Missing parent scope for ${node.id}`);
|
|
48
|
+
const scope = new Scope(node.id, parentScope);
|
|
49
|
+
this.scopes.set(node.id, scope);
|
|
50
|
+
this.#created.push(node.id);
|
|
51
|
+
// Every owner shadows the inherited EnvStore with its exact overlay view.
|
|
52
|
+
scope.provide(envStoreToken, this.#envStores.create(node.id));
|
|
53
|
+
if (!node.parent) {
|
|
54
|
+
scope.provide(runtimeEnvironmentToken, this.environment);
|
|
55
|
+
await this.installResources?.({
|
|
56
|
+
resources: scope,
|
|
57
|
+
lifecycle: scope.disposers,
|
|
58
|
+
handoff: this.#handoffs,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const config = Object.freeze(this.configResolver(node) ?? {});
|
|
62
|
+
const view = { get: () => config };
|
|
63
|
+
const plugin = Object.freeze({
|
|
64
|
+
id: node.id,
|
|
65
|
+
instanceKey: node.instanceKey,
|
|
66
|
+
parent: node.parent,
|
|
67
|
+
root: rootPluginId(),
|
|
68
|
+
role: node.parent ? 'child' : 'root',
|
|
69
|
+
});
|
|
70
|
+
let metadata;
|
|
71
|
+
if (manifest.runtime === 'isolated') {
|
|
72
|
+
if (!node.parent)
|
|
73
|
+
throw new Error('Root Plugin cannot use runtime: isolated');
|
|
74
|
+
if (node.features.length > 0) {
|
|
75
|
+
throw new Error(`Isolated Plugin ${node.id} cannot mount Host Feature providers`);
|
|
76
|
+
}
|
|
77
|
+
if (!this.isolation) {
|
|
78
|
+
throw new Error(`Isolated Plugin runtime adapter is required: ${node.package.name}`);
|
|
79
|
+
}
|
|
80
|
+
const prepared = await this.isolation.prepare({
|
|
81
|
+
owner: node.id,
|
|
82
|
+
parent: node.parent,
|
|
83
|
+
packageName: node.package.name,
|
|
84
|
+
entry: resolve(node.package.root, manifest.entry),
|
|
85
|
+
config,
|
|
86
|
+
environment: this.environment,
|
|
87
|
+
});
|
|
88
|
+
// Ownership transfers to the shadow Scope immediately. Every later
|
|
89
|
+
// validation or binding failure is then covered by normal rollback.
|
|
90
|
+
scope.disposers.add(prepared.dispose);
|
|
91
|
+
if (!prepared.descriptor.name) {
|
|
92
|
+
throw new TypeError(`Isolated Plugin ${node.package.name} returned an invalid descriptor`);
|
|
93
|
+
}
|
|
94
|
+
for (const binding of prepared.resources ?? []) {
|
|
95
|
+
scope.provide(binding.token, binding.value);
|
|
96
|
+
}
|
|
97
|
+
if (prepared.handoff)
|
|
98
|
+
this.#handoffs.add(prepared.handoff);
|
|
99
|
+
metadata = prepared.descriptor.metadata;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const module = await this.modules.load(resolve(node.package.root, manifest.entry));
|
|
103
|
+
const definition = module.default;
|
|
104
|
+
if (!definition || typeof definition.name !== 'string') {
|
|
105
|
+
throw new TypeError(`${node.package.name} does not default-export a Plugin definition`);
|
|
106
|
+
}
|
|
107
|
+
for (const token of definition.requires ?? []) {
|
|
108
|
+
if (!scope.has(token)) {
|
|
109
|
+
throw new Error(`Missing resource ${token.id} for Plugin ${node.id}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const returned = await definition.setup?.({
|
|
113
|
+
plugin,
|
|
114
|
+
config: view,
|
|
115
|
+
resources: scope,
|
|
116
|
+
lifecycle: scope.disposers,
|
|
117
|
+
handoff: this.#handoffs,
|
|
118
|
+
});
|
|
119
|
+
if (returned)
|
|
120
|
+
scope.disposers.add(returned);
|
|
121
|
+
metadata = definition.metadata;
|
|
122
|
+
}
|
|
123
|
+
scope.seal();
|
|
124
|
+
this.tree.set(node.id, Object.freeze({
|
|
125
|
+
id: node.id,
|
|
126
|
+
instanceKey: node.instanceKey,
|
|
127
|
+
packageName: node.package.name,
|
|
128
|
+
packageRoot: node.package.root,
|
|
129
|
+
parent: node.parent,
|
|
130
|
+
children: Object.freeze(node.children.map((child) => child.id)),
|
|
131
|
+
metadata,
|
|
132
|
+
}));
|
|
133
|
+
this.config.set(node.id, config);
|
|
134
|
+
this.resources.set(node.id, scope.snapshot());
|
|
135
|
+
for (const child of node.children)
|
|
136
|
+
await this.setupTree(child);
|
|
137
|
+
}
|
|
138
|
+
synchronizeTree(node) {
|
|
139
|
+
const current = this.tree.get(node.id);
|
|
140
|
+
if (!current)
|
|
141
|
+
throw new Error(`Missing Plugin tree node: ${node.id}`);
|
|
142
|
+
this.tree.set(node.id, Object.freeze({
|
|
143
|
+
...current,
|
|
144
|
+
instanceKey: node.instanceKey,
|
|
145
|
+
packageName: node.package.name,
|
|
146
|
+
packageRoot: node.package.root,
|
|
147
|
+
parent: node.parent,
|
|
148
|
+
children: Object.freeze(node.children.map((child) => child.id)),
|
|
149
|
+
}));
|
|
150
|
+
for (const child of node.children)
|
|
151
|
+
this.synchronizeTree(child);
|
|
152
|
+
}
|
|
153
|
+
createdScopeDisposers() {
|
|
154
|
+
return this.#created.map((owner) => {
|
|
155
|
+
const scope = this.scopes.get(owner);
|
|
156
|
+
if (!scope)
|
|
157
|
+
throw new Error(`Missing created Scope: ${owner}`);
|
|
158
|
+
return [owner, () => scope.disposers.dispose()];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
generationHandoff() {
|
|
162
|
+
return this.#handoffs.seal();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isWithin(plugin, root) {
|
|
166
|
+
return plugin === root || plugin.startsWith(`${root}/`);
|
|
167
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProcessInvalidationPlan } from './invalidation-planner.js';
|
|
2
|
+
export interface ProcessRestartAdapter {
|
|
3
|
+
restart(plan: ProcessInvalidationPlan): void | Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface StoppableRoot {
|
|
6
|
+
stop(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/** Drains one Root lifecycle before handing process replacement to the Host. */
|
|
9
|
+
export declare class RootProcessRestartExecutor {
|
|
10
|
+
#private;
|
|
11
|
+
private readonly root;
|
|
12
|
+
private readonly adapter;
|
|
13
|
+
constructor(root: StoppableRoot, adapter: ProcessRestartAdapter);
|
|
14
|
+
execute(plan: ProcessInvalidationPlan): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Drains one Root lifecycle before handing process replacement to the Host. */
|
|
2
|
+
export class RootProcessRestartExecutor {
|
|
3
|
+
root;
|
|
4
|
+
adapter;
|
|
5
|
+
#execution;
|
|
6
|
+
constructor(root, adapter) {
|
|
7
|
+
this.root = root;
|
|
8
|
+
this.adapter = adapter;
|
|
9
|
+
}
|
|
10
|
+
execute(plan) {
|
|
11
|
+
if (!this.#execution) {
|
|
12
|
+
const request = clonePlan(plan);
|
|
13
|
+
// Keep the completed promise: one process incarnation may request its
|
|
14
|
+
// replacement exactly once, even if several watcher paths converge.
|
|
15
|
+
this.#execution = Promise.resolve().then(async () => {
|
|
16
|
+
await this.root.stop();
|
|
17
|
+
await this.adapter.restart(request);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return this.#execution;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function clonePlan(plan) {
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
kind: 'process',
|
|
26
|
+
changed: Object.freeze([...plan.changed]),
|
|
27
|
+
reasons: Object.freeze([...plan.reasons]),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type PluginId } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { PackageReference } from './manifest.js';
|
|
3
|
+
import { type PackageResolver, type ResolvedPackage } from './package-resolver.js';
|
|
4
|
+
export interface FeatureRequirementNode {
|
|
5
|
+
readonly reference: PackageReference;
|
|
6
|
+
readonly package: ResolvedPackage;
|
|
7
|
+
}
|
|
8
|
+
export interface PluginGraphNode {
|
|
9
|
+
readonly id: PluginId;
|
|
10
|
+
readonly instanceKey: string;
|
|
11
|
+
readonly package: ResolvedPackage;
|
|
12
|
+
readonly parent?: PluginId;
|
|
13
|
+
readonly features: readonly FeatureRequirementNode[];
|
|
14
|
+
readonly children: readonly PluginGraphNode[];
|
|
15
|
+
}
|
|
16
|
+
export interface ProjectGraph {
|
|
17
|
+
readonly root: PluginGraphNode;
|
|
18
|
+
readonly packages: ReadonlyMap<string, ResolvedPackage>;
|
|
19
|
+
readonly buildOrder: readonly ResolvedPackage[];
|
|
20
|
+
}
|
|
21
|
+
export declare class ProjectGraphError extends Error {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
export declare class ProjectGraphService {
|
|
25
|
+
#private;
|
|
26
|
+
private readonly resolver;
|
|
27
|
+
private readonly engineVersion;
|
|
28
|
+
constructor(resolver: PackageResolver, engineVersion?: string);
|
|
29
|
+
inspect(projectRoot: string): Promise<ProjectGraph>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { childPluginId, rootPluginId } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { assertFeatureApi, assertPackageEngine, runtimeEngineVersion, } from './compatibility.js';
|
|
3
|
+
import { PackageResolutionError, } from './package-resolver.js';
|
|
4
|
+
export class ProjectGraphError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ProjectGraphError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class ProjectGraphService {
|
|
11
|
+
resolver;
|
|
12
|
+
engineVersion;
|
|
13
|
+
constructor(resolver, engineVersion = runtimeEngineVersion) {
|
|
14
|
+
this.resolver = resolver;
|
|
15
|
+
this.engineVersion = engineVersion;
|
|
16
|
+
}
|
|
17
|
+
async inspect(projectRoot) {
|
|
18
|
+
const rootPackage = await this.resolver.root(projectRoot);
|
|
19
|
+
assertPackageType(rootPackage, 'plugin');
|
|
20
|
+
const packages = new Map();
|
|
21
|
+
const root = await this.#visitPlugin(rootPackage, rootPluginId(), 'root', undefined, [], packages);
|
|
22
|
+
for (const pkg of this.resolver.workspacePackages())
|
|
23
|
+
addPackage(packages, pkg);
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
root,
|
|
26
|
+
packages,
|
|
27
|
+
buildOrder: topologicalBuildOrder(packages),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async #visitPlugin(pkg, id, instanceKey, parent, ancestors, packages) {
|
|
31
|
+
if (ancestors.includes(pkg.root)) {
|
|
32
|
+
throw new ProjectGraphError(`Plugin cycle detected: ${[...ancestors, pkg.root].join(' -> ')}`);
|
|
33
|
+
}
|
|
34
|
+
const manifest = assertPackageType(pkg, 'plugin');
|
|
35
|
+
assertPackageEngine(pkg, this.engineVersion);
|
|
36
|
+
addPackage(packages, pkg);
|
|
37
|
+
const featurePackages = new Set();
|
|
38
|
+
const features = await Promise.all(manifest.features.map(async (reference) => {
|
|
39
|
+
if (featurePackages.has(reference.package)) {
|
|
40
|
+
throw new ProjectGraphError(`Duplicate Feature requirement ${reference.package} in ${pkg.name}`);
|
|
41
|
+
}
|
|
42
|
+
featurePackages.add(reference.package);
|
|
43
|
+
const resolved = await resolveReference(this.resolver, pkg, reference);
|
|
44
|
+
if (!resolved)
|
|
45
|
+
return undefined;
|
|
46
|
+
assertPackageType(resolved, 'feature');
|
|
47
|
+
assertPackageEngine(resolved, this.engineVersion);
|
|
48
|
+
assertFeatureApi(pkg, reference, resolved);
|
|
49
|
+
addPackage(packages, resolved);
|
|
50
|
+
return Object.freeze({ reference, package: resolved });
|
|
51
|
+
}));
|
|
52
|
+
const instanceKeys = new Set();
|
|
53
|
+
const children = await Promise.all(manifest.plugins.map(async (reference) => {
|
|
54
|
+
if (instanceKeys.has(reference.instanceKey)) {
|
|
55
|
+
throw new ProjectGraphError(`Duplicate child instanceKey ${reference.instanceKey} in ${pkg.name}`);
|
|
56
|
+
}
|
|
57
|
+
instanceKeys.add(reference.instanceKey);
|
|
58
|
+
const resolved = await resolveReference(this.resolver, pkg, reference);
|
|
59
|
+
if (!resolved)
|
|
60
|
+
return undefined;
|
|
61
|
+
assertPackageType(resolved, 'plugin');
|
|
62
|
+
return this.#visitPlugin(resolved, childPluginId(id, reference.instanceKey), reference.instanceKey, id, [...ancestors, pkg.root], packages);
|
|
63
|
+
}));
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
id,
|
|
66
|
+
instanceKey,
|
|
67
|
+
package: pkg,
|
|
68
|
+
parent,
|
|
69
|
+
features: Object.freeze(features.filter(isDefined)),
|
|
70
|
+
children: Object.freeze(children.filter(isDefined)),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function resolveReference(resolver, from, reference) {
|
|
75
|
+
try {
|
|
76
|
+
return await resolver.resolve(reference.package, from);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (reference.optional
|
|
80
|
+
&& error instanceof PackageResolutionError
|
|
81
|
+
&& error.message.startsWith('Cannot resolve'))
|
|
82
|
+
return undefined;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function assertPackageType(pkg, type) {
|
|
87
|
+
if (pkg.packageJson.zhin.type !== type) {
|
|
88
|
+
throw new ProjectGraphError(`${pkg.name} must be a Zhin ${type} package`);
|
|
89
|
+
}
|
|
90
|
+
return pkg.packageJson.zhin;
|
|
91
|
+
}
|
|
92
|
+
function topologicalBuildOrder(packages) {
|
|
93
|
+
const result = [];
|
|
94
|
+
const visited = new Set();
|
|
95
|
+
const visiting = new Set();
|
|
96
|
+
const visitPackage = (pkg) => {
|
|
97
|
+
if (visited.has(pkg.root))
|
|
98
|
+
return;
|
|
99
|
+
if (visiting.has(pkg.root)) {
|
|
100
|
+
throw new ProjectGraphError(`Package dependency cycle detected at ${pkg.name}`);
|
|
101
|
+
}
|
|
102
|
+
visiting.add(pkg.root);
|
|
103
|
+
const dependencies = {
|
|
104
|
+
...pkg.packageJson.dependencies,
|
|
105
|
+
...pkg.packageJson.optionalDependencies,
|
|
106
|
+
};
|
|
107
|
+
for (const name of Object.keys(dependencies)) {
|
|
108
|
+
const dependency = packages.get(name);
|
|
109
|
+
if (dependency)
|
|
110
|
+
visitPackage(dependency);
|
|
111
|
+
}
|
|
112
|
+
visiting.delete(pkg.root);
|
|
113
|
+
visited.add(pkg.root);
|
|
114
|
+
result.push(pkg);
|
|
115
|
+
};
|
|
116
|
+
for (const pkg of packages.values())
|
|
117
|
+
visitPackage(pkg);
|
|
118
|
+
return Object.freeze(result);
|
|
119
|
+
}
|
|
120
|
+
function isDefined(value) {
|
|
121
|
+
return value !== undefined;
|
|
122
|
+
}
|
|
123
|
+
function addPackage(packages, pkg) {
|
|
124
|
+
const previous = packages.get(pkg.name);
|
|
125
|
+
if (previous && previous.root !== pkg.root) {
|
|
126
|
+
throw new ProjectGraphError(`Multiple package locations for ${pkg.name} are not supported in one generation`);
|
|
127
|
+
}
|
|
128
|
+
packages.set(pkg.name, pkg);
|
|
129
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ProcessInvalidationPlan } from './invalidation-planner.js';
|
|
2
|
+
import type { ProjectGraph } from './project-graph.js';
|
|
3
|
+
/** Decides which manifest changes cannot remain inside a generation transaction. */
|
|
4
|
+
export declare class RestartBoundaryPlanner {
|
|
5
|
+
plan(previous: ProjectGraph, next: ProjectGraph, changed: readonly string[]): ProcessInvalidationPlan | undefined;
|
|
6
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { graphNodes } from './topology-transaction.js';
|
|
4
|
+
/** Decides which manifest changes cannot remain inside a generation transaction. */
|
|
5
|
+
export class RestartBoundaryPlanner {
|
|
6
|
+
plan(previous, next, changed) {
|
|
7
|
+
const reasons = new Set();
|
|
8
|
+
const previousPackages = mountedPackages(previous);
|
|
9
|
+
const nextPackages = mountedPackages(next);
|
|
10
|
+
for (const [root, pkg] of nextPackages) {
|
|
11
|
+
const old = previousPackages.get(root);
|
|
12
|
+
if (!old)
|
|
13
|
+
continue;
|
|
14
|
+
if (runtimeAbiChanged(old, pkg)) {
|
|
15
|
+
reasons.add(`package runtime ABI changed: ${pkg.name}`);
|
|
16
|
+
}
|
|
17
|
+
const oldManifest = old.packageJson.zhin;
|
|
18
|
+
const nextManifest = pkg.packageJson.zhin;
|
|
19
|
+
if (oldManifest.engine !== nextManifest.engine) {
|
|
20
|
+
reasons.add(`runtime engine contract changed: ${pkg.name}`);
|
|
21
|
+
}
|
|
22
|
+
if (oldManifest.type === 'feature'
|
|
23
|
+
&& nextManifest.type === 'feature'
|
|
24
|
+
&& oldManifest.featureApi !== nextManifest.featureApi)
|
|
25
|
+
reasons.add(`Feature API contract changed: ${pkg.name}`);
|
|
26
|
+
if (oldManifest.type === 'plugin'
|
|
27
|
+
&& nextManifest.type === 'plugin'
|
|
28
|
+
&& oldManifest.runtime !== nextManifest.runtime)
|
|
29
|
+
reasons.add(`Plugin execution runtime changed: ${pkg.name}`);
|
|
30
|
+
}
|
|
31
|
+
const oldRoot = previous.root.package;
|
|
32
|
+
const nextRoot = next.root.package;
|
|
33
|
+
const oldRootManifest = oldRoot.packageJson.zhin;
|
|
34
|
+
const nextRootManifest = nextRoot.packageJson.zhin;
|
|
35
|
+
if (oldRoot.name !== nextRoot.name
|
|
36
|
+
|| oldRoot.root !== nextRoot.root
|
|
37
|
+
|| oldRootManifest.entry !== nextRootManifest.entry)
|
|
38
|
+
reasons.add('Root Plugin runtime contract changed');
|
|
39
|
+
if (reasons.size === 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
kind: 'process',
|
|
43
|
+
changed: Object.freeze([...changed].map((source) => resolve(source))),
|
|
44
|
+
reasons: Object.freeze([...reasons]),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function mountedPackages(graph) {
|
|
49
|
+
const packages = new Map();
|
|
50
|
+
for (const node of graphNodes(graph).values()) {
|
|
51
|
+
packages.set(resolve(node.package.root), node.package);
|
|
52
|
+
for (const feature of node.features) {
|
|
53
|
+
packages.set(resolve(feature.package.root), feature.package);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return packages;
|
|
57
|
+
}
|
|
58
|
+
function runtimeAbiChanged(previous, next) {
|
|
59
|
+
return !isDeepStrictEqual(runtimeAbi(previous), runtimeAbi(next));
|
|
60
|
+
}
|
|
61
|
+
function runtimeAbi(pkg) {
|
|
62
|
+
return {
|
|
63
|
+
type: pkg.packageJson.type,
|
|
64
|
+
main: pkg.packageJson.main,
|
|
65
|
+
exports: pkg.packageJson.exports,
|
|
66
|
+
imports: pkg.packageJson.imports,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { RootController, type ControlErrorHandler, type PluginId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { type RuntimeConfigDocument } from './config-composer.js';
|
|
3
|
+
import { type ConfigDocumentPort } from './config-document.js';
|
|
4
|
+
import { type ConfigPatch } from './config-patch-planner.js';
|
|
5
|
+
import { type RuntimeEnvironment } from './environment.js';
|
|
6
|
+
import { type EnvironmentLayers } from './environment-store.js';
|
|
7
|
+
import type { IsolatedPluginRuntimePort } from './isolation.js';
|
|
8
|
+
import type { ModuleRuntime } from './module-runtime.js';
|
|
9
|
+
import { type PluginConfigResolver, type RootResourceInstaller } from './plugin-scope-assembler.js';
|
|
10
|
+
import { HmrCoordinator, type HmrCoordinatorOptions } from './hmr-coordinator.js';
|
|
11
|
+
import { RootProcessRestartExecutor, type ProcessRestartAdapter } from './process-restart.js';
|
|
12
|
+
import { SourceOwnershipIndex } from './source-ownership.js';
|
|
13
|
+
export type { PluginConfigResolver, RootResourceContext, RootResourceInstaller, } from './plugin-scope-assembler.js';
|
|
14
|
+
export interface RootRuntimeOptions {
|
|
15
|
+
readonly projectRoot: string;
|
|
16
|
+
readonly modules: ModuleRuntime;
|
|
17
|
+
readonly environment: RuntimeEnvironment;
|
|
18
|
+
readonly environmentVariables?: EnvironmentLayers;
|
|
19
|
+
readonly config?: PluginConfigResolver | RuntimeConfigDocument | ConfigDocumentPort;
|
|
20
|
+
readonly installResources?: RootResourceInstaller;
|
|
21
|
+
readonly isolation?: IsolatedPluginRuntimePort;
|
|
22
|
+
readonly onControlError?: ControlErrorHandler;
|
|
23
|
+
}
|
|
24
|
+
export type RootHmrOptions = Omit<HmrCoordinatorOptions, 'modules' | 'ownership' | 'runtime'>;
|
|
25
|
+
export declare class RootRuntime {
|
|
26
|
+
#private;
|
|
27
|
+
readonly controller: RootController;
|
|
28
|
+
constructor(options: RootRuntimeOptions);
|
|
29
|
+
get snapshot(): RuntimeSnapshot;
|
|
30
|
+
get sourceOwnership(): SourceOwnershipIndex;
|
|
31
|
+
start(): Promise<RuntimeSnapshot>;
|
|
32
|
+
reload(target?: PluginId | string): Promise<RuntimeSnapshot>;
|
|
33
|
+
patchConfig(patches: readonly ConfigPatch[]): Promise<RuntimeSnapshot>;
|
|
34
|
+
createHmrCoordinator(options: RootHmrOptions): HmrCoordinator;
|
|
35
|
+
createProcessRestartExecutor(adapter: ProcessRestartAdapter): RootProcessRestartExecutor;
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
}
|