@zhin.js/runtime 1.1.8 → 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -4
- package/lib/application-plugin-declaration.d.ts +17 -0
- package/lib/application-plugin-declaration.js +69 -0
- package/lib/config-composer.d.ts +2408 -167
- package/lib/config-composer.js +61 -20
- package/lib/config-patch-planner.d.ts +3 -2
- package/lib/config-patch-planner.js +7 -5
- package/lib/editor-temp-file.d.ts +12 -0
- package/lib/editor-temp-file.js +44 -0
- package/lib/hmr-coordinator.js +4 -0
- package/lib/host-config-schema.json +3843 -140
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/manifest.d.ts +14 -0
- package/lib/manifest.js +64 -4
- package/lib/native-development-runtime.js +5 -0
- package/lib/plugin-config-schema.d.ts +10 -0
- package/lib/plugin-config-schema.js +25 -2
- package/lib/plugin-scope-assembler.d.ts +3 -3
- package/lib/plugin-scope-assembler.js +18 -8
- package/lib/primary-config.d.ts +6 -2
- package/lib/primary-config.js +9 -1
- package/lib/project-graph.d.ts +2 -2
- package/lib/project-graph.js +18 -17
- package/lib/root-runtime.d.ts +2 -0
- package/lib/root-runtime.js +138 -44
- package/lib/runtime-input-planner.d.ts +3 -1
- package/lib/runtime-input-planner.js +7 -4
- package/lib/slot-generation-preparer.d.ts +3 -1
- package/lib/slot-generation-preparer.js +7 -2
- package/lib/topology-generation-preparer.js +25 -3
- package/lib/topology-transaction.js +3 -1
- package/package.json +15 -15
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/lib/manifest.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface ZhinPluginManifest {
|
|
|
12
12
|
readonly entry: string;
|
|
13
13
|
readonly engine?: string;
|
|
14
14
|
readonly runtime?: 'trusted' | 'isolated';
|
|
15
|
+
readonly metadata?: Readonly<{
|
|
16
|
+
displayName?: string;
|
|
17
|
+
icon?: string;
|
|
18
|
+
}>;
|
|
15
19
|
/**
|
|
16
20
|
* When true (default), Root plugins receive official Stable Features
|
|
17
21
|
* (`@zhin.js/adapter|command|component`) from the platform/CLI even if they
|
|
@@ -49,3 +53,13 @@ export declare class ManifestValidationError extends Error {
|
|
|
49
53
|
constructor(issues: readonly string[]);
|
|
50
54
|
}
|
|
51
55
|
export declare function parsePackageJson(value: unknown, source: string): PackageJson;
|
|
56
|
+
/** Conditions tried, in order, when `exports["./zhin"]` is a condition object. */
|
|
57
|
+
export declare const ZHIN_EXPORT_CONDITIONS: readonly ["zhin", "import", "default"];
|
|
58
|
+
/**
|
|
59
|
+
* The runtime entry declared by `exports["./zhin"]`, if any.
|
|
60
|
+
*
|
|
61
|
+
* A string target is used as is; a condition object is searched for `zhin`, then `import`,
|
|
62
|
+
* then `default` (nested condition objects are searched the same way); an array uses its
|
|
63
|
+
* first target that resolves. Other conditions such as `types` or `require` are ignored.
|
|
64
|
+
*/
|
|
65
|
+
export declare function zhinExportTarget(packageExports: unknown): string | undefined;
|
package/lib/manifest.js
CHANGED
|
@@ -12,7 +12,7 @@ export function parsePackageJson(value, source) {
|
|
|
12
12
|
const name = readString(record, 'name', source, issues);
|
|
13
13
|
if (name && !isPackageName(name))
|
|
14
14
|
issues.push(`${source}#name is not a valid package name`);
|
|
15
|
-
const zhin = parseZhinManifest(record.zhin, `${source}#zhin`, issues);
|
|
15
|
+
const zhin = parseZhinManifest(record.zhin, `${source}#zhin`, issues, record.exports, `${source}#exports`);
|
|
16
16
|
const version = optionalString(record.version, `${source}#version`, issues);
|
|
17
17
|
const moduleType = optionalModuleType(record.type, `${source}#type`, issues);
|
|
18
18
|
const main = optionalString(record.main, `${source}#main`, issues);
|
|
@@ -42,12 +42,54 @@ export function parsePackageJson(value, source) {
|
|
|
42
42
|
zhin,
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
/** Conditions tried, in order, when `exports["./zhin"]` is a condition object. */
|
|
46
|
+
export const ZHIN_EXPORT_CONDITIONS = Object.freeze(['zhin', 'import', 'default']);
|
|
47
|
+
/**
|
|
48
|
+
* The runtime entry declared by `exports["./zhin"]`, if any.
|
|
49
|
+
*
|
|
50
|
+
* A string target is used as is; a condition object is searched for `zhin`, then `import`,
|
|
51
|
+
* then `default` (nested condition objects are searched the same way); an array uses its
|
|
52
|
+
* first target that resolves. Other conditions such as `types` or `require` are ignored.
|
|
53
|
+
*/
|
|
54
|
+
export function zhinExportTarget(packageExports) {
|
|
55
|
+
if (!packageExports || typeof packageExports !== 'object' || Array.isArray(packageExports))
|
|
56
|
+
return undefined;
|
|
57
|
+
if (!Object.hasOwn(packageExports, './zhin'))
|
|
58
|
+
return undefined;
|
|
59
|
+
return selectExportTarget(packageExports['./zhin']);
|
|
60
|
+
}
|
|
61
|
+
function selectExportTarget(value) {
|
|
62
|
+
if (typeof value === 'string')
|
|
63
|
+
return value;
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
for (const item of value) {
|
|
66
|
+
const target = selectExportTarget(item);
|
|
67
|
+
if (target !== undefined)
|
|
68
|
+
return target;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
if (!value || typeof value !== 'object')
|
|
73
|
+
return undefined;
|
|
74
|
+
for (const condition of ZHIN_EXPORT_CONDITIONS) {
|
|
75
|
+
if (!Object.hasOwn(value, condition))
|
|
76
|
+
continue;
|
|
77
|
+
const target = selectExportTarget(value[condition]);
|
|
78
|
+
if (target !== undefined)
|
|
79
|
+
return target;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function parseZhinManifest(value, source, issues, packageExports, exportsSource = 'exports') {
|
|
46
84
|
const record = asRecord(value, source, issues);
|
|
47
85
|
if (record.protocol !== 1)
|
|
48
86
|
issues.push(`${source}.protocol must be 1`);
|
|
49
87
|
const type = readString(record, 'type', source, issues);
|
|
50
|
-
|
|
88
|
+
// `zhin.entry` wins when present; `exports["./zhin"]` is the standard-field alternative.
|
|
89
|
+
const exportedEntry = record.entry === undefined ? zhinExportTarget(packageExports) : undefined;
|
|
90
|
+
const entry = exportedEntry !== undefined
|
|
91
|
+
? readRelativeEntry(exportedEntry, `${exportsSource}["./zhin"]`, issues)
|
|
92
|
+
: readRelativeEntry(record.entry, `${source}.entry`, issues, `${exportsSource}["./zhin"]`);
|
|
51
93
|
const engine = optionalString(record.engine, `${source}.engine`, issues);
|
|
52
94
|
if (!entry || (type !== 'plugin' && type !== 'feature')) {
|
|
53
95
|
if (type && type !== 'plugin' && type !== 'feature') {
|
|
@@ -75,11 +117,25 @@ function parseZhinManifest(value, source, issues) {
|
|
|
75
117
|
entry,
|
|
76
118
|
engine,
|
|
77
119
|
runtime: runtime,
|
|
120
|
+
metadata: parsePluginMetadata(record.metadata, `${source}.metadata`, issues),
|
|
78
121
|
platformFeatures,
|
|
79
122
|
features: parseReferences(record.features, `${source}.features`, issues, false),
|
|
80
123
|
plugins: parseReferences(record.plugins, `${source}.plugins`, issues, true),
|
|
81
124
|
});
|
|
82
125
|
}
|
|
126
|
+
function parsePluginMetadata(value, source, issues) {
|
|
127
|
+
if (value === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
const record = asRecord(value, source, issues);
|
|
130
|
+
for (const key of Object.keys(record)) {
|
|
131
|
+
if (key !== 'displayName' && key !== 'icon')
|
|
132
|
+
issues.push(`${source}.${key} is not supported`);
|
|
133
|
+
}
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
...(record.displayName !== undefined ? { displayName: readString(record, 'displayName', source, issues) } : {}),
|
|
136
|
+
...(record.icon !== undefined ? { icon: readString(record, 'icon', source, issues) } : {}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
83
139
|
function parseReferences(value, source, issues, child) {
|
|
84
140
|
if (value === undefined)
|
|
85
141
|
return [];
|
|
@@ -118,7 +174,11 @@ function parseReferences(value, source, issues, child) {
|
|
|
118
174
|
}];
|
|
119
175
|
});
|
|
120
176
|
}
|
|
121
|
-
function readRelativeEntry(value, source, issues) {
|
|
177
|
+
function readRelativeEntry(value, source, issues, alternative) {
|
|
178
|
+
if (value === undefined && alternative) {
|
|
179
|
+
issues.push(`${source} or ${alternative} must declare a package-relative path starting with ./`);
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
122
182
|
if (typeof value !== 'string' || !value.startsWith('./')) {
|
|
123
183
|
issues.push(`${source} must be a package-relative path starting with ./`);
|
|
124
184
|
return undefined;
|
|
@@ -4,6 +4,7 @@ import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { ModuleDependencyIndex, } from './module-dependency-index.js';
|
|
6
6
|
import { withToolPackageInstallHint } from './missing-tool-package.js';
|
|
7
|
+
import { isEditorTempFile } from './editor-temp-file.js';
|
|
7
8
|
import { ensureTypeScriptSpecifierRemap } from './typescript-specifier-remap.js';
|
|
8
9
|
const ignoredDirectories = new Set([
|
|
9
10
|
'.git', '.zhin', 'coverage', 'data', 'dist', 'lib', 'node_modules',
|
|
@@ -353,8 +354,12 @@ function sourceSnapshot(roots) {
|
|
|
353
354
|
visit(root);
|
|
354
355
|
return result;
|
|
355
356
|
}
|
|
357
|
+
/** Shared by the native watcher and the polling fallback, so both skip the same files. */
|
|
356
358
|
function isWatchedSource(source) {
|
|
357
359
|
const name = source.slice(source.lastIndexOf(sep) + 1);
|
|
360
|
+
// Editor temp/backup files (e.g. `.!97910!index.ts`, `.env.swp`) never reload (#857).
|
|
361
|
+
if (isEditorTempFile(name))
|
|
362
|
+
return false;
|
|
358
363
|
return watchedExtensions.has(extname(source)) || name.startsWith('.env');
|
|
359
364
|
}
|
|
360
365
|
/** Mirrors sourceSnapshot: any path segment matching an ignored directory opts out. */
|
|
@@ -4,3 +4,13 @@ import type { ResolvedPackage } from './package-resolver.js';
|
|
|
4
4
|
export declare function readPluginConfigSchema(pkg: ResolvedPackage, modules?: Pick<ModuleRuntime, 'load'>): Promise<Readonly<Record<string, unknown>> | null>;
|
|
5
5
|
/** Detached declaration metadata for preflight and custom-resolver startup views. */
|
|
6
6
|
export declare function immutableConfigSchema(schema: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>>;
|
|
7
|
+
/**
|
|
8
|
+
* Thrown when a trusted plugin declares both `definePlugin({ config })` and a root `schema.json`.
|
|
9
|
+
* Neither source wins: startup stops so the two schemas cannot silently drift apart.
|
|
10
|
+
*/
|
|
11
|
+
export declare class PluginConfigSourceConflictError extends TypeError {
|
|
12
|
+
readonly packageName: string;
|
|
13
|
+
readonly entryFile: string;
|
|
14
|
+
readonly schemaFile: string;
|
|
15
|
+
constructor(packageName: string, entryFile: string, schemaFile: string);
|
|
16
|
+
}
|
|
@@ -14,12 +14,14 @@ export async function readPluginConfigSchema(pkg, modules) {
|
|
|
14
14
|
// Isolated plugin code never executes in the composition process.
|
|
15
15
|
const manifest = pkg.packageJson.zhin;
|
|
16
16
|
let declaration;
|
|
17
|
+
let entryFile;
|
|
17
18
|
if (modules && manifest.type === 'plugin' && manifest.runtime !== 'isolated') {
|
|
18
|
-
|
|
19
|
+
entryFile = resolve(pkg.root, manifest.entry);
|
|
20
|
+
const module = await modules.load(entryFile);
|
|
19
21
|
declaration = module.default?.config;
|
|
20
22
|
}
|
|
21
23
|
if (declaration && fileSchema !== undefined) {
|
|
22
|
-
throw new
|
|
24
|
+
throw new PluginConfigSourceConflictError(pkg.name, entryFile ?? manifest.entry, file);
|
|
23
25
|
}
|
|
24
26
|
const schema = declaration ? declaration.toJsonSchema() : fileSchema;
|
|
25
27
|
if (schema === undefined)
|
|
@@ -42,3 +44,24 @@ export function immutableConfigSchema(schema) {
|
|
|
42
44
|
freeze(copy);
|
|
43
45
|
return copy;
|
|
44
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Thrown when a trusted plugin declares both `definePlugin({ config })` and a root `schema.json`.
|
|
49
|
+
* Neither source wins: startup stops so the two schemas cannot silently drift apart.
|
|
50
|
+
*/
|
|
51
|
+
export class PluginConfigSourceConflictError extends TypeError {
|
|
52
|
+
packageName;
|
|
53
|
+
entryFile;
|
|
54
|
+
schemaFile;
|
|
55
|
+
constructor(packageName, entryFile, schemaFile) {
|
|
56
|
+
super(`${packageName} declares both plugin config and schema.json; choose one configuration schema. `
|
|
57
|
+
+ `Inline config: definePlugin({ config }) in ${entryFile}. JSON Schema: ${schemaFile}. `
|
|
58
|
+
+ 'Zhin does not pick one for you. To keep the typed inline Schema, delete schema.json; '
|
|
59
|
+
+ 'to keep schema.json (required for isolated plugins, readable without running the plugin), '
|
|
60
|
+
+ 'remove config from definePlugin. '
|
|
61
|
+
+ 'See https://zhin.js.org/authoring/define-plugin#choose-a-config-source');
|
|
62
|
+
this.packageName = packageName;
|
|
63
|
+
this.entryFile = entryFile;
|
|
64
|
+
this.schemaFile = schemaFile;
|
|
65
|
+
this.name = 'PluginConfigSourceConflictError';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { DisposeStack, Scope, type Dispose, type FeatureId, type GenerationHandoff, type GenerationAdmissionGate, type GenerationHandoffRegistry, type RuntimeSnapshot, type PluginId, type PluginNodeSnapshot, type SetupCapabilityRegistration, type TokenId } from '@zhin.js/plugin-runtime';
|
|
1
|
+
import { DisposeStack, Scope, type Dispose, type FeatureId, type GenerationHandoff, type GenerationAdmissionGate, type GenerationHandoffRegistry, type RuntimeSnapshot, type RuntimeConfigDocument, type PluginId, type PluginNodeSnapshot, type SetupCapabilityRegistration, type TokenId } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import { type RuntimeEnvironment } from './environment.js';
|
|
3
3
|
import { type EnvironmentLayers } from './environment-store.js';
|
|
4
4
|
import type { ModuleRuntime } from './module-runtime.js';
|
|
5
5
|
import type { PluginGraphNode } from './project-graph.js';
|
|
6
6
|
import type { IsolatedPluginRuntimePort } from './isolation.js';
|
|
7
7
|
import { type PrimaryConfig } from './primary-config.js';
|
|
8
|
-
import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
9
8
|
export interface PluginConfigResolver {
|
|
10
9
|
(node: PluginGraphNode): unknown;
|
|
11
10
|
schema?(node: PluginGraphNode): Readonly<Record<string, unknown>> | undefined;
|
|
11
|
+
/** Set by RootRuntime's actual composition branch, not by Plugin config values. */
|
|
12
|
+
compositionMode?: NonNullable<PrimaryConfig['compositionMode']>;
|
|
12
13
|
}
|
|
13
14
|
export interface RootResourceContext {
|
|
14
15
|
/** Exact shadow generation being assembled; never inferred from a latest snapshot. */
|
|
@@ -42,7 +43,6 @@ export declare class PluginScopeAssembler {
|
|
|
42
43
|
private readonly modules;
|
|
43
44
|
private readonly configResolver;
|
|
44
45
|
private readonly environment;
|
|
45
|
-
private readonly primaryConfigDocument;
|
|
46
46
|
private readonly generation;
|
|
47
47
|
private readonly installResources?;
|
|
48
48
|
private readonly isolation?;
|
|
@@ -2,13 +2,12 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { GenerationHandoffStack, Scope, capabilityId, createGenerationAdmissionGate, createToken, PluginDatabaseHost, PluginOutboundHost, PluginScheduleHost, databaseHostToken, databaseRootHostToken, outboundHostToken, outboundRootHostToken, featureId, rootPluginId, scheduleHostToken, scheduleRootHostToken, } 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
|
+
import { createPrimaryConfig, projectPrimaryConfigResources, primaryConfigToken, } from './primary-config.js';
|
|
6
6
|
/** Assembles Plugin setup into mutable shadow maps without publishing them. */
|
|
7
7
|
export class PluginScopeAssembler {
|
|
8
8
|
modules;
|
|
9
9
|
configResolver;
|
|
10
10
|
environment;
|
|
11
|
-
primaryConfigDocument;
|
|
12
11
|
generation;
|
|
13
12
|
installResources;
|
|
14
13
|
isolation;
|
|
@@ -22,20 +21,21 @@ export class PluginScopeAssembler {
|
|
|
22
21
|
#admission = createGenerationAdmissionGate();
|
|
23
22
|
#envStores;
|
|
24
23
|
#candidateSnapshot;
|
|
24
|
+
#primaryConfig;
|
|
25
25
|
#setupFeatureAliases = new Map();
|
|
26
26
|
constructor(modules, configResolver, environment, primaryConfigDocument, generation, installResources, environmentLayers = {}, seed, isolation) {
|
|
27
27
|
this.modules = modules;
|
|
28
28
|
this.configResolver = configResolver;
|
|
29
29
|
this.environment = environment;
|
|
30
|
-
this.primaryConfigDocument = primaryConfigDocument;
|
|
31
30
|
this.generation = generation;
|
|
32
31
|
this.installResources = installResources;
|
|
33
32
|
this.isolation = isolation;
|
|
34
33
|
this.#envStores = new EnvStoreFactory(environment, environmentLayers);
|
|
34
|
+
this.#primaryConfig = createPrimaryConfig(primaryConfigDocument, this.#envStores.create(rootPluginId()), configResolver.compositionMode);
|
|
35
35
|
this.scopes = new Map(seed?.scopes);
|
|
36
36
|
this.tree = new Map(seed?.tree);
|
|
37
37
|
this.config = new Map(seed?.config);
|
|
38
|
-
this.resources = new Map(
|
|
38
|
+
this.resources = projectPrimaryConfigResources(seed?.resources ?? new Map(), this.#primaryConfig);
|
|
39
39
|
}
|
|
40
40
|
removeSubtrees(roots) {
|
|
41
41
|
// Seeded Scopes belong to a committed generation. Removing a map entry
|
|
@@ -64,12 +64,14 @@ export class PluginScopeAssembler {
|
|
|
64
64
|
// Every owner shadows the inherited EnvStore with its exact overlay view.
|
|
65
65
|
const environment = this.#envStores.create(node.id);
|
|
66
66
|
scope.provide(envStoreToken, environment);
|
|
67
|
+
// New setup operations shadow a retained parent's construction-time value;
|
|
68
|
+
// operation contexts read the immutable projection from their own snapshot.
|
|
69
|
+
scope.provide(primaryConfigToken, this.#primaryConfig);
|
|
67
70
|
const register = (feature, localName, capabilityDefinition) => this.#registerSetupCapability(node, manifest, feature, localName, capabilityDefinition);
|
|
68
71
|
if (!node.parent) {
|
|
69
72
|
scope.provide(runtimeEnvironmentToken, this.environment);
|
|
70
73
|
scope.provide(rootGenerationAdmissionToken, this.#admission);
|
|
71
|
-
const config =
|
|
72
|
-
scope.provide(primaryConfigToken, config);
|
|
74
|
+
const config = this.#primaryConfig;
|
|
73
75
|
await this.installResources?.({
|
|
74
76
|
generation: this.generation,
|
|
75
77
|
readCandidateSnapshot: () => {
|
|
@@ -130,7 +132,7 @@ export class PluginScopeAssembler {
|
|
|
130
132
|
}
|
|
131
133
|
if (prepared.handoff)
|
|
132
134
|
this.#handoffs.add(prepared.handoff);
|
|
133
|
-
metadata = prepared.descriptor.metadata;
|
|
135
|
+
metadata = mergePluginMetadata(manifest, prepared.descriptor.metadata, node.id);
|
|
134
136
|
}
|
|
135
137
|
else {
|
|
136
138
|
const module = await this.modules.load(resolve(node.package.root, manifest.entry));
|
|
@@ -139,6 +141,7 @@ export class PluginScopeAssembler {
|
|
|
139
141
|
if (!definition || typeof definition.name !== 'string') {
|
|
140
142
|
throw new TypeError(`${node.package.name} does not default-export a Plugin definition`);
|
|
141
143
|
}
|
|
144
|
+
metadata = mergePluginMetadata(manifest, definition.metadata, node.id);
|
|
142
145
|
for (const token of definition.requires ?? []) {
|
|
143
146
|
if (!scope.has(token)) {
|
|
144
147
|
throw new Error(`Missing resource ${token.id} for Plugin ${node.id}`);
|
|
@@ -160,7 +163,6 @@ export class PluginScopeAssembler {
|
|
|
160
163
|
signal.throwIfAborted();
|
|
161
164
|
if (returned)
|
|
162
165
|
scope.disposers.add(returned);
|
|
163
|
-
metadata = definition.metadata;
|
|
164
166
|
}
|
|
165
167
|
scope.seal();
|
|
166
168
|
this.tree.set(node.id, Object.freeze({
|
|
@@ -257,3 +259,11 @@ const rootGenerationAdmissionToken = createToken('zhin.runtime.root-generation-a
|
|
|
257
259
|
function isWithin(plugin, root) {
|
|
258
260
|
return plugin === root || plugin.startsWith(`${root}/`);
|
|
259
261
|
}
|
|
262
|
+
function mergePluginMetadata(manifest, code, owner) {
|
|
263
|
+
for (const key of ['displayName', 'icon']) {
|
|
264
|
+
if (manifest.metadata?.[key] !== undefined && code?.[key] !== undefined) {
|
|
265
|
+
throw new TypeError(`Plugin ${owner} declares metadata.${key} in both manifest and code`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return manifest.metadata || code ? Object.freeze({ ...code, ...manifest.metadata }) : undefined;
|
|
269
|
+
}
|
package/lib/primary-config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { type PluginId, type RuntimeConfigDocument, type SnapshotState, type TokenId } from '@zhin.js/plugin-runtime';
|
|
1
2
|
import type { EnvStore } from './environment-store.js';
|
|
2
|
-
import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
3
3
|
/**
|
|
4
4
|
* Generation-owned, validated Root configuration.
|
|
5
5
|
*
|
|
@@ -8,9 +8,13 @@ import type { RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
|
8
8
|
* through the Root EnvStore instead of reading process.env themselves.
|
|
9
9
|
*/
|
|
10
10
|
export interface PrimaryConfig {
|
|
11
|
+
/** Actual Runtime composition branch; absent on older authorities, never inferred from values. */
|
|
12
|
+
readonly compositionMode?: 'document' | 'custom' | 'unknown';
|
|
11
13
|
readonly document: RuntimeConfigDocument;
|
|
12
14
|
readonly expanded: RuntimeConfigDocument;
|
|
13
15
|
get<T = unknown>(key: string): T | undefined;
|
|
14
16
|
}
|
|
15
17
|
export declare const primaryConfigToken: import("@zhin.js/plugin-runtime").Token<PrimaryConfig>;
|
|
16
|
-
export declare function createPrimaryConfig(document: RuntimeConfigDocument, environment: EnvStore): PrimaryConfig;
|
|
18
|
+
export declare function createPrimaryConfig(document: RuntimeConfigDocument, environment: EnvStore, compositionMode?: NonNullable<PrimaryConfig['compositionMode']>): PrimaryConfig;
|
|
19
|
+
/** Copies bindings so retained Scopes never mutate a leased generation's view. */
|
|
20
|
+
export declare function projectPrimaryConfigResources(resources: SnapshotState['resources'], config: PrimaryConfig): Map<PluginId, ReadonlyMap<TokenId, unknown>>;
|
package/lib/primary-config.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createToken } from '@zhin.js/plugin-runtime';
|
|
2
2
|
export const primaryConfigToken = createToken('zhin.primary-config', 'Validated Root configuration for the current generation');
|
|
3
|
-
export function createPrimaryConfig(document, environment) {
|
|
3
|
+
export function createPrimaryConfig(document, environment, compositionMode = 'unknown') {
|
|
4
4
|
const safeDocument = freezeConfig(structuredClone(document));
|
|
5
5
|
const expanded = freezeConfig(environment.expandMissingAsEmpty(safeDocument));
|
|
6
6
|
return Object.freeze({
|
|
7
|
+
compositionMode,
|
|
7
8
|
document: safeDocument,
|
|
8
9
|
expanded,
|
|
9
10
|
get(key) {
|
|
@@ -11,6 +12,13 @@ export function createPrimaryConfig(document, environment) {
|
|
|
11
12
|
},
|
|
12
13
|
});
|
|
13
14
|
}
|
|
15
|
+
/** Copies bindings so retained Scopes never mutate a leased generation's view. */
|
|
16
|
+
export function projectPrimaryConfigResources(resources, config) {
|
|
17
|
+
return new Map([...resources].map(([owner, bindings]) => [
|
|
18
|
+
owner,
|
|
19
|
+
new Map([...bindings, [primaryConfigToken.id, config]]),
|
|
20
|
+
]));
|
|
21
|
+
}
|
|
14
22
|
function freezeConfig(value) {
|
|
15
23
|
if (!value || typeof value !== 'object' || Object.isFrozen(value))
|
|
16
24
|
return value;
|
package/lib/project-graph.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PluginId } from '@zhin.js/plugin-runtime';
|
|
1
|
+
import { type PluginId, type RuntimeConfigDocument } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import type { PackageReference } from './manifest.js';
|
|
3
3
|
import { type PackageResolver, type ResolvedPackage } from './package-resolver.js';
|
|
4
4
|
export interface FeatureRequirementNode {
|
|
@@ -28,5 +28,5 @@ export declare class ProjectGraphError extends Error {
|
|
|
28
28
|
export declare class ProjectGraphService {
|
|
29
29
|
#private;
|
|
30
30
|
constructor(resolver: PackageResolver, engineVersionOrOptions?: string | ProjectGraphServiceOptions);
|
|
31
|
-
inspect(projectRoot: string): Promise<ProjectGraph>;
|
|
31
|
+
inspect(projectRoot: string, document?: RuntimeConfigDocument, pluginOrder?: readonly string[]): Promise<ProjectGraph>;
|
|
32
32
|
}
|
package/lib/project-graph.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { childPluginId, rootPluginId } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { PLUGIN_INSTANCE_MIGRATION_GUIDE, readApplicationPluginDeclaration } from './application-plugin-declaration.js';
|
|
2
3
|
import { assertFeatureApi, assertPackageEngine, runtimeEngineVersion, } from './package-contract.js';
|
|
3
4
|
import { PackageResolutionError, } from './package-resolver.js';
|
|
4
|
-
import { PLATFORM_FEATURE_CARRIER, PLATFORM_FEATURE_FACADE, declaredPackageDependency,
|
|
5
|
+
import { PLATFORM_FEATURE_CARRIER, PLATFORM_FEATURE_FACADE, declaredPackageDependency, mergeFeatureReferences, } from './platform-features.js';
|
|
5
6
|
export class ProjectGraphError extends Error {
|
|
6
7
|
constructor(message) {
|
|
7
8
|
super(message);
|
|
@@ -21,11 +22,19 @@ export class ProjectGraphService {
|
|
|
21
22
|
? []
|
|
22
23
|
: (engineVersionOrOptions.disabledPluginInstanceKeys ?? []));
|
|
23
24
|
}
|
|
24
|
-
async inspect(projectRoot) {
|
|
25
|
+
async inspect(projectRoot, document = {}, pluginOrder) {
|
|
26
|
+
const declaration = readApplicationPluginDeclaration(document, 'Root config', pluginOrder);
|
|
27
|
+
if (this.#disabledPluginInstanceKeys.size > 0) {
|
|
28
|
+
throw new ProjectGraphError(`Legacy disabled Plugin state must move to disable_plugins; see ${PLUGIN_INSTANCE_MIGRATION_GUIDE}`);
|
|
29
|
+
}
|
|
25
30
|
const rootPackage = await this.#resolver.root(projectRoot);
|
|
26
31
|
assertPackageType(rootPackage, 'plugin');
|
|
32
|
+
if (rootPackage.packageJson.zhin.type === 'plugin'
|
|
33
|
+
&& rootPackage.packageJson.zhin.plugins.length > 0) {
|
|
34
|
+
throw new ProjectGraphError(`Root package.json#zhin.plugins is no longer supported; see ${PLUGIN_INSTANCE_MIGRATION_GUIDE}`);
|
|
35
|
+
}
|
|
27
36
|
const packages = new Map();
|
|
28
|
-
const root = await this.#visitPlugin(rootPackage, rootPluginId(), 'root', undefined, [], packages, true);
|
|
37
|
+
const root = await this.#visitPlugin(rootPackage, rootPluginId(), 'root', undefined, [], packages, true, declaration);
|
|
29
38
|
for (const pkg of this.#resolver.workspacePackages())
|
|
30
39
|
addPackage(packages, pkg);
|
|
31
40
|
return Object.freeze({
|
|
@@ -34,7 +43,7 @@ export class ProjectGraphService {
|
|
|
34
43
|
buildOrder: topologicalBuildOrder(packages),
|
|
35
44
|
});
|
|
36
45
|
}
|
|
37
|
-
async #visitPlugin(pkg, id, instanceKey, parent, ancestors, packages, isRoot) {
|
|
46
|
+
async #visitPlugin(pkg, id, instanceKey, parent, ancestors, packages, isRoot, declaration) {
|
|
38
47
|
if (ancestors.includes(pkg.root)) {
|
|
39
48
|
throw new ProjectGraphError(`Plugin cycle detected: ${[...ancestors, pkg.root].join(' -> ')}`);
|
|
40
49
|
}
|
|
@@ -44,7 +53,8 @@ export class ProjectGraphService {
|
|
|
44
53
|
let featureCarrier;
|
|
45
54
|
let pluginFacade;
|
|
46
55
|
let featureRefs = manifest.features;
|
|
47
|
-
|
|
56
|
+
const pluginRefs = isRoot
|
|
57
|
+
? declaration.instances.filter(item => item.enabled).map(item => ({ package: item.package, instanceKey: item.instanceKey })) : [];
|
|
48
58
|
if (isRoot && manifest.platformFeatures !== false) {
|
|
49
59
|
const platform = await tryResolvePlatform(this.#resolver, pkg);
|
|
50
60
|
featureCarrier = platform.featureCarrier;
|
|
@@ -55,15 +65,8 @@ export class ProjectGraphService {
|
|
|
55
65
|
const inherited = carrierManifest.type === 'plugin' ? carrierManifest.features : [];
|
|
56
66
|
featureRefs = mergeFeatureReferences(manifest.features, inherited);
|
|
57
67
|
}
|
|
58
|
-
if (pluginFacade)
|
|
68
|
+
if (pluginFacade)
|
|
59
69
|
addPackage(packages, pluginFacade);
|
|
60
|
-
const facadeManifest = pluginFacade.packageJson.zhin;
|
|
61
|
-
const inheritedPlugins = facadeManifest.type === 'plugin' ? facadeManifest.plugins : [];
|
|
62
|
-
pluginRefs = mergeChildPluginReferences(manifest.plugins, inheritedPlugins);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (isRoot && this.#disabledPluginInstanceKeys.size > 0) {
|
|
66
|
-
pluginRefs = pluginRefs.filter((reference) => !this.#disabledPluginInstanceKeys.has(reference.instanceKey));
|
|
67
70
|
}
|
|
68
71
|
const featurePackages = new Set();
|
|
69
72
|
const features = await Promise.all(featureRefs.map(async (reference) => {
|
|
@@ -80,19 +83,17 @@ export class ProjectGraphService {
|
|
|
80
83
|
addPackage(packages, resolved);
|
|
81
84
|
return Object.freeze({ reference, package: resolved });
|
|
82
85
|
}));
|
|
83
|
-
const ownPluginKeys = new Set(manifest.plugins.map((item) => item.instanceKey));
|
|
84
86
|
const instanceKeys = new Set();
|
|
85
87
|
const children = await Promise.all(pluginRefs.map(async (reference) => {
|
|
86
88
|
if (instanceKeys.has(reference.instanceKey)) {
|
|
87
89
|
throw new ProjectGraphError(`Duplicate child instanceKey ${reference.instanceKey} in ${pkg.name}`);
|
|
88
90
|
}
|
|
89
91
|
instanceKeys.add(reference.instanceKey);
|
|
90
|
-
const
|
|
91
|
-
const resolved = await resolveReference(this.#resolver, resolveFrom, reference);
|
|
92
|
+
const resolved = await resolveReference(this.#resolver, pkg, reference);
|
|
92
93
|
if (!resolved)
|
|
93
94
|
return undefined;
|
|
94
95
|
assertPackageType(resolved, 'plugin');
|
|
95
|
-
return this.#visitPlugin(resolved, childPluginId(id, reference.instanceKey), reference.instanceKey, id, [...ancestors, pkg.root], packages, false);
|
|
96
|
+
return this.#visitPlugin(resolved, childPluginId(id, reference.instanceKey), reference.instanceKey, id, [...ancestors, pkg.root], packages, false, declaration);
|
|
96
97
|
}));
|
|
97
98
|
return Object.freeze({
|
|
98
99
|
id,
|
package/lib/root-runtime.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface RootRuntimeOptions {
|
|
|
17
17
|
readonly environmentVariables?: EnvironmentLayers;
|
|
18
18
|
readonly environmentSource?: EnvironmentLayersPort;
|
|
19
19
|
readonly config?: PluginConfigResolver | RuntimeConfigDocument | ConfigDocumentPort;
|
|
20
|
+
/** Explicit v2 instance declaration when config is supplied by a custom resolver. */
|
|
21
|
+
readonly application?: RuntimeConfigDocument;
|
|
20
22
|
readonly installResources?: RootResourceInstaller;
|
|
21
23
|
readonly isolation?: IsolatedPluginRuntimePort;
|
|
22
24
|
readonly onControlError?: ControlErrorHandler;
|