@zhin.js/runtime 1.1.7 → 1.1.9

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/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './config-composer.js';
2
+ export * from './application-plugin-declaration.js';
2
3
  export { readPluginConfigSchema } from './plugin-config-schema.js';
3
4
  export * from './config-patch-planner.js';
4
5
  export * from './package-contract.js';
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './config-composer.js';
2
+ export * from './application-plugin-declaration.js';
2
3
  export { readPluginConfigSchema } from './plugin-config-schema.js';
3
4
  export * from './config-patch-planner.js';
4
5
  export * from './package-contract.js';
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
package/lib/manifest.js CHANGED
@@ -75,11 +75,25 @@ function parseZhinManifest(value, source, issues) {
75
75
  entry,
76
76
  engine,
77
77
  runtime: runtime,
78
+ metadata: parsePluginMetadata(record.metadata, `${source}.metadata`, issues),
78
79
  platformFeatures,
79
80
  features: parseReferences(record.features, `${source}.features`, issues, false),
80
81
  plugins: parseReferences(record.plugins, `${source}.plugins`, issues, true),
81
82
  });
82
83
  }
84
+ function parsePluginMetadata(value, source, issues) {
85
+ if (value === undefined)
86
+ return undefined;
87
+ const record = asRecord(value, source, issues);
88
+ for (const key of Object.keys(record)) {
89
+ if (key !== 'displayName' && key !== 'icon')
90
+ issues.push(`${source}.${key} is not supported`);
91
+ }
92
+ return Object.freeze({
93
+ ...(record.displayName !== undefined ? { displayName: readString(record, 'displayName', source, issues) } : {}),
94
+ ...(record.icon !== undefined ? { icon: readString(record, 'icon', source, issues) } : {}),
95
+ });
96
+ }
83
97
  function parseReferences(value, source, issues, child) {
84
98
  if (value === undefined)
85
99
  return [];
@@ -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(seed?.resources);
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 = createPrimaryConfig(this.primaryConfigDocument, environment);
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
+ }
@@ -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>>;
@@ -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;
@@ -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
  }
@@ -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, mergeChildPluginReferences, mergeFeatureReferences, } from './platform-features.js';
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
- let pluginRefs = manifest.plugins;
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 resolveFrom = ownPluginKeys.has(reference.instanceKey) ? pkg : (pluginFacade ?? pkg);
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,
@@ -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;