@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
package/lib/config-composer.js
CHANGED
|
@@ -149,6 +149,14 @@ async function readOwnSchema(node) {
|
|
|
149
149
|
if (schema.type !== undefined && schema.type !== 'object') {
|
|
150
150
|
throw new TypeError(`${file} root schema type must be object`);
|
|
151
151
|
}
|
|
152
|
+
// A compositional root (anyOf/oneOf/allOf/$ref without properties) would
|
|
153
|
+
// validate but pickOwnFields only copies top-level properties, silently
|
|
154
|
+
// projecting an empty ConfigView. Reject it explicitly instead.
|
|
155
|
+
if (schema.properties === undefined
|
|
156
|
+
&& (schema.anyOf !== undefined || schema.oneOf !== undefined
|
|
157
|
+
|| schema.allOf !== undefined || schema.$ref !== undefined)) {
|
|
158
|
+
throw new TypeError(`${file} root schema must declare properties; composition keywords (anyOf/oneOf/allOf/$ref) are not supported`);
|
|
159
|
+
}
|
|
152
160
|
return Object.freeze({
|
|
153
161
|
type: 'object',
|
|
154
162
|
additionalProperties: false,
|
|
@@ -47,31 +47,67 @@ function applyPatch(document, patch) {
|
|
|
47
47
|
function setValue(document, path, value) {
|
|
48
48
|
let target = document;
|
|
49
49
|
for (const [index, segment] of path.slice(0, -1).entries()) {
|
|
50
|
-
const existing = target
|
|
50
|
+
const existing = readChild(target, segment, path.slice(0, index + 1));
|
|
51
51
|
if (existing === undefined) {
|
|
52
|
+
// Missing intermediates are always created as records; arrays only ever
|
|
53
|
+
// come from the existing document (numeric segments index into them).
|
|
52
54
|
const created = {};
|
|
53
55
|
target[segment] = created;
|
|
54
56
|
target = created;
|
|
55
57
|
}
|
|
56
58
|
else {
|
|
57
|
-
target =
|
|
59
|
+
target = requireContainer(existing, path.slice(0, index + 1));
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
|
-
target
|
|
62
|
+
writeChild(target, lastSegment(path), value, path);
|
|
61
63
|
}
|
|
62
64
|
function removeValue(document, path) {
|
|
63
65
|
let target = document;
|
|
64
66
|
for (const [index, segment] of path.slice(0, -1).entries()) {
|
|
65
|
-
const existing = target
|
|
67
|
+
const existing = readChild(target, segment, path.slice(0, index + 1));
|
|
66
68
|
if (existing === undefined)
|
|
67
69
|
return;
|
|
68
|
-
target =
|
|
70
|
+
target = requireContainer(existing, path.slice(0, index + 1));
|
|
69
71
|
}
|
|
70
|
-
|
|
72
|
+
const segment = lastSegment(path);
|
|
73
|
+
if (Array.isArray(target)) {
|
|
74
|
+
target.splice(arrayIndex(segment, target, path), 1);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
delete target[segment];
|
|
71
78
|
}
|
|
72
79
|
function cloneDocument(value) {
|
|
73
80
|
return requireRecord(structuredClone(value), []);
|
|
74
81
|
}
|
|
82
|
+
function readChild(container, segment, path) {
|
|
83
|
+
if (Array.isArray(container))
|
|
84
|
+
return container[arrayIndex(segment, container, path)];
|
|
85
|
+
return container[segment];
|
|
86
|
+
}
|
|
87
|
+
function writeChild(container, segment, value, path) {
|
|
88
|
+
if (Array.isArray(container)) {
|
|
89
|
+
container[arrayIndex(segment, container, path)] = value;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
container[segment] = value;
|
|
93
|
+
}
|
|
94
|
+
/** Resolves a numeric path segment against an array, rejecting non-numeric segments and out-of-bounds indexes. */
|
|
95
|
+
function arrayIndex(segment, container, path) {
|
|
96
|
+
if (!/^(0|[1-9]\d*)$/.test(segment)) {
|
|
97
|
+
throw new ConfigPatchPathError(`Config path ${pointer(path)} requires an array index, got "${segment}"`);
|
|
98
|
+
}
|
|
99
|
+
const index = Number(segment);
|
|
100
|
+
if (index >= container.length) {
|
|
101
|
+
throw new ConfigPatchPathError(`Config path ${pointer(path)} is out of bounds (array length ${container.length})`);
|
|
102
|
+
}
|
|
103
|
+
return index;
|
|
104
|
+
}
|
|
105
|
+
function requireContainer(value, path) {
|
|
106
|
+
if (!value || typeof value !== 'object') {
|
|
107
|
+
throw new ConfigPatchPathError(`Config path ${pointer(path)} is not an object`);
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
75
111
|
function requireRecord(value, path) {
|
|
76
112
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
77
113
|
throw new ConfigPatchPathError(`Config path ${pointer(path)} is not an object`);
|
package/lib/environment-store.js
CHANGED
|
@@ -201,14 +201,19 @@ function expandString(input, lookup, onMissing) {
|
|
|
201
201
|
if (start < 0)
|
|
202
202
|
break;
|
|
203
203
|
let j = start + 2;
|
|
204
|
-
|
|
204
|
+
const first = input[j];
|
|
205
|
+
if (first === undefined || !isEnvKeyStart(first)) {
|
|
205
206
|
out += input.slice(i, start + 1);
|
|
206
207
|
i = start + 1;
|
|
207
208
|
continue;
|
|
208
209
|
}
|
|
209
210
|
j += 1;
|
|
210
|
-
while (j < input.length
|
|
211
|
+
while (j < input.length) {
|
|
212
|
+
const character = input[j];
|
|
213
|
+
if (character === undefined || !isEnvKeyChar(character))
|
|
214
|
+
break;
|
|
211
215
|
j += 1;
|
|
216
|
+
}
|
|
212
217
|
const key = input.slice(start + 2, j);
|
|
213
218
|
let fallback;
|
|
214
219
|
if (input[j] === ':' && (input[j + 1] === '-' || input[j + 1] === '=')) {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { type Dispose, type GenerationHandoff, type SnapshotState } from '@zhin.js/plugin-runtime';
|
|
1
|
+
import { type Dispose, type FeatureId, type GenerationHandoff, type SnapshotState } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import type { FeatureProvider } from '@zhin.js/feature-kit';
|
|
3
3
|
export type ProjectionState = Omit<SnapshotState, 'projections'>;
|
|
4
4
|
export interface ProjectedFeatures {
|
|
5
5
|
readonly state: SnapshotState;
|
|
6
|
-
readonly disposers:
|
|
6
|
+
readonly disposers: ReadonlyMap<FeatureId, Dispose>;
|
|
7
7
|
readonly handoff?: GenerationHandoff;
|
|
8
8
|
}
|
|
9
|
-
/** Builds
|
|
9
|
+
/** Builds selected Feature projections against one coherent candidate snapshot. */
|
|
10
10
|
export declare class FeatureProjector {
|
|
11
11
|
private readonly providers;
|
|
12
12
|
constructor(providers: Iterable<FeatureProvider>);
|
|
13
|
-
project(generation: number, base: ProjectionState): Promise<ProjectedFeatures>;
|
|
13
|
+
project(generation: number, base: ProjectionState, retained?: ReadonlyMap<FeatureId, unknown>): Promise<ProjectedFeatures>;
|
|
14
14
|
}
|
|
15
15
|
export declare function composeGenerationHandoffs(...handoffs: readonly (GenerationHandoff | undefined)[]): GenerationHandoff | undefined;
|
package/lib/feature-projector.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { DisposeStack, GenerationHandoffStack, createSnapshotView, } from '@zhin.js/plugin-runtime';
|
|
2
|
-
/** Builds
|
|
2
|
+
/** Builds selected Feature projections against one coherent candidate snapshot. */
|
|
3
3
|
export class FeatureProjector {
|
|
4
4
|
providers;
|
|
5
5
|
constructor(providers) {
|
|
6
6
|
this.providers = providers;
|
|
7
7
|
}
|
|
8
|
-
async project(generation, base) {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
async project(generation, base, retained = new Map()) {
|
|
9
|
+
// Slot HMR seeds this map with the committed projections. Only providers
|
|
10
|
+
// passed to this projector replace their entry; every other Feature keeps
|
|
11
|
+
// its live instance and therefore keeps its external resources running.
|
|
12
|
+
const projections = new Map(retained);
|
|
13
|
+
const disposers = new Map();
|
|
11
14
|
const handoffs = new GenerationHandoffStack();
|
|
12
15
|
const state = { ...base, projections };
|
|
13
16
|
try {
|
|
14
|
-
// A projection may capture its snapshot. Rebuilding every projection
|
|
15
|
-
// prevents unchanged Features from retaining an older generation.
|
|
16
17
|
for (const provider of this.providers) {
|
|
17
18
|
const slots = [...base.capabilities.values()].filter((slot) => slot.feature === provider.id);
|
|
18
19
|
const projection = await provider.runtime.project(slots, {
|
|
@@ -20,18 +21,18 @@ export class FeatureProjector {
|
|
|
20
21
|
});
|
|
21
22
|
projections.set(provider.id, projection.value);
|
|
22
23
|
if (projection.dispose)
|
|
23
|
-
disposers.
|
|
24
|
+
disposers.set(provider.id, projection.dispose);
|
|
24
25
|
if (projection.handoff)
|
|
25
26
|
handoffs.add(projection.handoff);
|
|
26
27
|
}
|
|
27
28
|
return {
|
|
28
29
|
state,
|
|
29
|
-
disposers
|
|
30
|
+
disposers,
|
|
30
31
|
handoff: handoffs.seal(),
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
catch (error) {
|
|
34
|
-
await rollback(disposers, error);
|
|
35
|
+
await rollback(disposers.values(), error);
|
|
35
36
|
throw error;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type Dispose, type PluginId } from '@zhin.js/plugin-runtime';
|
|
1
|
+
import { type Dispose, type FeatureId, type PluginId } from '@zhin.js/plugin-runtime';
|
|
2
2
|
export declare class GenerationAssets {
|
|
3
3
|
#private;
|
|
4
4
|
private constructor();
|
|
5
|
-
static create(scopeDisposers: Iterable<readonly [PluginId, Dispose]>, projectionDisposers:
|
|
6
|
-
|
|
7
|
-
replaceScopes(scopeOrder: readonly PluginId[], replacements: ReadonlyMap<PluginId, Dispose>, projectionDisposers:
|
|
5
|
+
static create(scopeDisposers: Iterable<readonly [PluginId, Dispose]>, projectionDisposers: ReadonlyMap<FeatureId, Dispose>): GenerationAssets;
|
|
6
|
+
replaceProjections(features: Iterable<FeatureId>, projectionDisposers: ReadonlyMap<FeatureId, Dispose>): GenerationAssets;
|
|
7
|
+
replaceScopes(scopeOrder: readonly PluginId[], replacements: ReadonlyMap<PluginId, Dispose>, projectionDisposers: ReadonlyMap<FeatureId, Dispose>): GenerationAssets;
|
|
8
8
|
dispose(): Promise<void>;
|
|
9
9
|
}
|
package/lib/generation-assets.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { DisposeStack, SharedLifetime, } from '@zhin.js/plugin-runtime';
|
|
2
2
|
export class GenerationAssets {
|
|
3
3
|
#scopeLifetimes;
|
|
4
|
+
// Projection ownership is per Feature, so one command transaction can
|
|
5
|
+
// retire CommandIndex without releasing AdapterIndex and its endpoints.
|
|
6
|
+
#projectionLifetimes;
|
|
4
7
|
#disposers = new DisposeStack();
|
|
5
|
-
constructor(scopeOrder, scopeLifetimes,
|
|
8
|
+
constructor(scopeOrder, scopeLifetimes, projectionLifetimes) {
|
|
6
9
|
this.#scopeLifetimes = scopeLifetimes;
|
|
10
|
+
this.#projectionLifetimes = projectionLifetimes;
|
|
7
11
|
assertScopeOrder(scopeOrder, scopeLifetimes);
|
|
8
12
|
// Scope order is parent-first. DisposeStack unwinds projections first,
|
|
9
13
|
// then Plugin leases children-first, so no child observes a closed parent.
|
|
@@ -14,8 +18,10 @@ export class GenerationAssets {
|
|
|
14
18
|
const lease = lifetime.acquire();
|
|
15
19
|
this.#disposers.add(() => lease.release());
|
|
16
20
|
}
|
|
17
|
-
for (const
|
|
18
|
-
|
|
21
|
+
for (const lifetime of projectionLifetimes.values()) {
|
|
22
|
+
const lease = lifetime.acquire();
|
|
23
|
+
this.#disposers.add(() => lease.release());
|
|
24
|
+
}
|
|
19
25
|
this.#disposers.seal();
|
|
20
26
|
}
|
|
21
27
|
static create(scopeDisposers, projectionDisposers) {
|
|
@@ -25,10 +31,22 @@ export class GenerationAssets {
|
|
|
25
31
|
throw new Error(`Duplicate Plugin Scope: ${owner}`);
|
|
26
32
|
lifetimes.set(owner, new SharedLifetime(dispose));
|
|
27
33
|
}
|
|
28
|
-
|
|
34
|
+
const projectionLifetimes = new Map([...projectionDisposers].map(([feature, dispose]) => [
|
|
35
|
+
feature,
|
|
36
|
+
new SharedLifetime(dispose),
|
|
37
|
+
]));
|
|
38
|
+
return new GenerationAssets([...lifetimes.keys()], lifetimes, projectionLifetimes);
|
|
29
39
|
}
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
replaceProjections(features, projectionDisposers) {
|
|
41
|
+
const lifetimes = new Map(this.#projectionLifetimes);
|
|
42
|
+
for (const feature of features) {
|
|
43
|
+
const dispose = projectionDisposers.get(feature);
|
|
44
|
+
if (dispose)
|
|
45
|
+
lifetimes.set(feature, new SharedLifetime(dispose));
|
|
46
|
+
else
|
|
47
|
+
lifetimes.delete(feature);
|
|
48
|
+
}
|
|
49
|
+
return new GenerationAssets([...this.#scopeLifetimes.keys()], this.#scopeLifetimes, lifetimes);
|
|
32
50
|
}
|
|
33
51
|
replaceScopes(scopeOrder, replacements, projectionDisposers) {
|
|
34
52
|
const owners = new Set(scopeOrder);
|
|
@@ -46,7 +64,11 @@ export class GenerationAssets {
|
|
|
46
64
|
throw new Error(`Cannot retain unknown Plugin Scope: ${owner}`);
|
|
47
65
|
lifetimes.set(owner, lifetime);
|
|
48
66
|
}
|
|
49
|
-
|
|
67
|
+
const projectionLifetimes = new Map([...projectionDisposers].map(([feature, dispose]) => [
|
|
68
|
+
feature,
|
|
69
|
+
new SharedLifetime(dispose),
|
|
70
|
+
]));
|
|
71
|
+
return new GenerationAssets(scopeOrder, lifetimes, projectionLifetimes);
|
|
50
72
|
}
|
|
51
73
|
dispose() {
|
|
52
74
|
return this.#disposers.dispose();
|
package/lib/hmr-coordinator.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface HmrCoordinatorOptions {
|
|
|
12
12
|
onRestartRequired(plan: ProcessInvalidationPlan): void | Promise<void>;
|
|
13
13
|
onError(error: unknown): void | Promise<void>;
|
|
14
14
|
onPlan?(plan: InvalidationPlan): void | Promise<void>;
|
|
15
|
+
/** Fires once after a generation transaction commits successfully. */
|
|
16
|
+
onReload?(plan: GenerationInvalidationPlan, durationMs: number): void | Promise<void>;
|
|
15
17
|
}
|
|
16
18
|
export declare class HmrCoordinator {
|
|
17
19
|
#private;
|
package/lib/hmr-coordinator.js
CHANGED
|
@@ -71,12 +71,17 @@ export class HmrCoordinator {
|
|
|
71
71
|
}
|
|
72
72
|
if (plan.kind === 'none')
|
|
73
73
|
continue;
|
|
74
|
+
const startedAt = performance.now();
|
|
74
75
|
for (const source of plan.changed) {
|
|
75
76
|
await this.options.modules.invalidate?.(source);
|
|
76
77
|
}
|
|
77
78
|
const restart = await this.options.runtime.reload(plan);
|
|
78
79
|
if (restart)
|
|
79
80
|
await this.options.onRestartRequired(restart);
|
|
81
|
+
else {
|
|
82
|
+
const durationMs = Number((performance.now() - startedAt).toFixed(1));
|
|
83
|
+
await this.options.onReload?.(plan, durationMs);
|
|
84
|
+
}
|
|
80
85
|
}
|
|
81
86
|
this.#resolveWaiters();
|
|
82
87
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export * from './module-runtime.js';
|
|
|
12
12
|
export * from './native-development-runtime.js';
|
|
13
13
|
export * from './node-discovery-host.js';
|
|
14
14
|
export * from './package-resolver.js';
|
|
15
|
+
export * from './platform-features.js';
|
|
16
|
+
export * from './primary-config.js';
|
|
15
17
|
export * from './project-graph.js';
|
|
16
18
|
export * from './process-restart.js';
|
|
17
19
|
export * from './restart-boundary.js';
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,8 @@ export * from './module-runtime.js';
|
|
|
12
12
|
export * from './native-development-runtime.js';
|
|
13
13
|
export * from './node-discovery-host.js';
|
|
14
14
|
export * from './package-resolver.js';
|
|
15
|
+
export * from './platform-features.js';
|
|
16
|
+
export * from './primary-config.js';
|
|
15
17
|
export * from './project-graph.js';
|
|
16
18
|
export * from './process-restart.js';
|
|
17
19
|
export * from './restart-boundary.js';
|
package/lib/manifest.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface ZhinPluginManifest {
|
|
|
12
12
|
readonly entry: string;
|
|
13
13
|
readonly engine?: string;
|
|
14
14
|
readonly runtime?: 'trusted' | 'isolated';
|
|
15
|
+
/**
|
|
16
|
+
* When true (default), Root plugins receive official Stable Features
|
|
17
|
+
* (`@zhin.js/adapter|command|component`) from the platform/CLI even if they
|
|
18
|
+
* are omitted from `features` / project dependencies. Set false to opt out.
|
|
19
|
+
*/
|
|
20
|
+
readonly platformFeatures?: boolean;
|
|
15
21
|
readonly features: readonly PackageReference[];
|
|
16
22
|
readonly plugins: readonly ChildPluginReference[];
|
|
17
23
|
}
|
package/lib/manifest.js
CHANGED
|
@@ -68,12 +68,14 @@ function parseZhinManifest(value, source, issues) {
|
|
|
68
68
|
if (runtime !== undefined && runtime !== 'trusted' && runtime !== 'isolated') {
|
|
69
69
|
issues.push(`${source}.runtime must be "trusted" or "isolated"`);
|
|
70
70
|
}
|
|
71
|
+
const platformFeatures = optionalBoolean(record.platformFeatures, `${source}.platformFeatures`, issues);
|
|
71
72
|
return Object.freeze({
|
|
72
73
|
protocol: 1,
|
|
73
74
|
type,
|
|
74
75
|
entry,
|
|
75
76
|
engine,
|
|
76
77
|
runtime: runtime,
|
|
78
|
+
platformFeatures,
|
|
77
79
|
features: parseReferences(record.features, `${source}.features`, issues, false),
|
|
78
80
|
plugins: parseReferences(record.plugins, `${source}.plugins`, issues, true),
|
|
79
81
|
});
|
|
@@ -94,10 +96,16 @@ function parseReferences(value, source, issues, child) {
|
|
|
94
96
|
: undefined;
|
|
95
97
|
if (!packageName || (child && !instanceKey))
|
|
96
98
|
return [];
|
|
97
|
-
|
|
99
|
+
// 支持两种来源:npm 包名(package 依赖)或 ./ 相对路径(monorepo 本地插件目录)
|
|
100
|
+
const isLocalPath = packageName.startsWith('./');
|
|
101
|
+
if (!isLocalPath && !isPackageName(packageName)) {
|
|
98
102
|
issues.push(`${itemSource}.package is not a valid package name`);
|
|
99
103
|
return [];
|
|
100
104
|
}
|
|
105
|
+
if (isLocalPath && packageName.split('/').includes('..')) {
|
|
106
|
+
issues.push(`${itemSource}.package must not escape the package root`);
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
101
109
|
if (instanceKey && !/^[a-z0-9][a-z0-9-]*$/.test(instanceKey)) {
|
|
102
110
|
issues.push(`${itemSource}.instanceKey is invalid`);
|
|
103
111
|
return [];
|
|
@@ -56,7 +56,12 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
56
56
|
return extname(normalized) !== '.md';
|
|
57
57
|
if (root === 'tools' || root === 'mcp')
|
|
58
58
|
return parts.length !== capability + 2;
|
|
59
|
-
|
|
59
|
+
if (isCapabilityEntry(parts.slice(capability + 1)))
|
|
60
|
+
return false;
|
|
61
|
+
// Support files inside capability directories (e.g. commands/_utils.ts)
|
|
62
|
+
// are not discovery entries: reloading the entry URL only bumps that
|
|
63
|
+
// entry's zhin-generation, so the importer closure keeps the old code.
|
|
64
|
+
return ['.js', '.json', '.ts'].includes(extname(normalized));
|
|
60
65
|
}
|
|
61
66
|
watch(listener) {
|
|
62
67
|
this.#assertOpen();
|
|
@@ -124,7 +129,7 @@ class PortableSourceWatcher {
|
|
|
124
129
|
if (!name)
|
|
125
130
|
return;
|
|
126
131
|
const source = resolve(this.root, name.toString());
|
|
127
|
-
if (isWatchedSource(source))
|
|
132
|
+
if (isWatchedSource(source) && !isIgnoredSource(this.root, source))
|
|
128
133
|
this.listener(source);
|
|
129
134
|
});
|
|
130
135
|
this.#watcher.on('error', () => this.#startPolling());
|
|
@@ -181,6 +186,25 @@ function isWatchedSource(source) {
|
|
|
181
186
|
const name = source.slice(source.lastIndexOf(sep) + 1);
|
|
182
187
|
return watchedExtensions.has(extname(source)) || name.startsWith('.env');
|
|
183
188
|
}
|
|
189
|
+
/** Mirrors sourceSnapshot: any path segment matching an ignored directory opts out. */
|
|
190
|
+
function isIgnoredSource(root, source) {
|
|
191
|
+
return relative(root, source).split(sep).some((segment) => ignoredDirectories.has(segment));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Discovery entries follow the typeScriptModules convention
|
|
195
|
+
* (feature-kit typescript-convention.ts): lowercase segment directories and
|
|
196
|
+
* lowercase .ts/.tsx module names only.
|
|
197
|
+
*/
|
|
198
|
+
function isCapabilityEntry(segments) {
|
|
199
|
+
const file = segments[segments.length - 1] ?? '';
|
|
200
|
+
return segments.slice(0, -1).every(isCapabilitySegment) && isCapabilityModule(file);
|
|
201
|
+
}
|
|
202
|
+
function isCapabilitySegment(value) {
|
|
203
|
+
return /^[a-z0-9][a-z0-9-]*$/u.test(value);
|
|
204
|
+
}
|
|
205
|
+
function isCapabilityModule(value) {
|
|
206
|
+
return /^[a-z0-9][a-z0-9-]*\.tsx?$/u.test(value);
|
|
207
|
+
}
|
|
184
208
|
function isExecutableSource(source) {
|
|
185
209
|
return ['.cjs', '.js', '.mjs', '.ts', '.tsx'].includes(extname(source));
|
|
186
210
|
}
|
|
@@ -3,11 +3,13 @@ export interface ResolvedPackage {
|
|
|
3
3
|
readonly name: string;
|
|
4
4
|
readonly root: string;
|
|
5
5
|
readonly packageJson: PackageJson;
|
|
6
|
-
readonly source: 'workspace' | 'node_modules';
|
|
6
|
+
readonly source: 'workspace' | 'node_modules' | 'local';
|
|
7
7
|
}
|
|
8
8
|
export interface PackageResolver {
|
|
9
9
|
root(root: string): Promise<ResolvedPackage>;
|
|
10
10
|
resolve(request: string, from: ResolvedPackage): Promise<ResolvedPackage>;
|
|
11
|
+
/** Load a package from an absolute package root (skips dependency declaration checks). */
|
|
12
|
+
loadPackage?(packageRoot: string): Promise<ResolvedPackage>;
|
|
11
13
|
workspacePackages(): readonly ResolvedPackage[];
|
|
12
14
|
}
|
|
13
15
|
export declare class PackageResolutionError extends Error {
|
|
@@ -18,6 +20,7 @@ export declare class NodePackageResolver implements PackageResolver {
|
|
|
18
20
|
#private;
|
|
19
21
|
static create(projectRoot: string): Promise<NodePackageResolver>;
|
|
20
22
|
root(root: string): Promise<ResolvedPackage>;
|
|
23
|
+
loadPackage(packageRoot: string): Promise<ResolvedPackage>;
|
|
21
24
|
workspacePackages(): readonly ResolvedPackage[];
|
|
22
25
|
resolve(request: string, from: ResolvedPackage): Promise<ResolvedPackage>;
|
|
23
26
|
}
|
package/lib/package-resolver.js
CHANGED
|
@@ -23,9 +23,8 @@ export class NodePackageResolver {
|
|
|
23
23
|
if (await exists(join(packageRoot, 'pnpm-workspace.yaml'))) {
|
|
24
24
|
throw new PackageResolutionError(`Nested workspace is not allowed: ${packageRoot}`);
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
26
|
+
// 注意:plugins/<x>/plugins/ 嵌套目录不再报错——顶层扫描只注册一层
|
|
27
|
+
// workspace 包;更深的本地插件经 manifest 的 './' 相对路径显式引用。
|
|
29
28
|
if (!await exists(join(packageRoot, 'package.json')))
|
|
30
29
|
continue;
|
|
31
30
|
const pkg = await resolver.#readPackage(packageRoot, 'workspace');
|
|
@@ -40,21 +39,31 @@ export class NodePackageResolver {
|
|
|
40
39
|
async root(root) {
|
|
41
40
|
return this.#readPackage(root, 'workspace');
|
|
42
41
|
}
|
|
42
|
+
async loadPackage(packageRoot) {
|
|
43
|
+
return this.#readPackage(packageRoot, 'node_modules');
|
|
44
|
+
}
|
|
43
45
|
workspacePackages() {
|
|
44
46
|
return [...this.#workspaceByName.values()];
|
|
45
47
|
}
|
|
46
48
|
async resolve(request, from) {
|
|
49
|
+
// 解析管线(按序短路,任一步命中即返回):
|
|
50
|
+
//
|
|
51
|
+
// 1. 本地路径('./' 开头):monorepo 本地插件目录,相对声明包根解析。
|
|
52
|
+
// 目录即声明——跳过依赖声明检查,也不入 node_modules。
|
|
53
|
+
if (request.startsWith('./'))
|
|
54
|
+
return this.#resolveLocal(request, from);
|
|
55
|
+
// 2. 声明检查(仅包名引用),按声明位置分级:
|
|
56
|
+
// - dependencies / optionalDependencies:硬要求,引用必须能在此声明;
|
|
57
|
+
// - peerDependencies:宽松声明——允许未安装,解析失败由引用方 optional 容错;
|
|
58
|
+
// - 三者皆无:拒绝(zhin manifest 引用必须可追溯到包依赖声明)。
|
|
47
59
|
const specification = declaredDependency(request, from.packageJson);
|
|
60
|
+
// 3. workspace byName:packages/* + plugins/* 顶层扫描结果优先命中。
|
|
61
|
+
// 'workspace:*' 未命中时继续走 node_modules——examples 等在扫描面之外,
|
|
62
|
+
// 但 pnpm 仍会把 workspace:* 链接进 node_modules。
|
|
48
63
|
const workspace = this.#workspaceByName.get(request);
|
|
49
|
-
if (
|
|
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) {
|
|
64
|
+
if (workspace)
|
|
56
65
|
return workspace;
|
|
57
|
-
|
|
66
|
+
// 4. node_modules 上溯:从声明包根逐级向上查找。
|
|
58
67
|
let current = from.root;
|
|
59
68
|
while (true) {
|
|
60
69
|
const packageRoot = join(current, 'node_modules', ...request.split('/'));
|
|
@@ -70,6 +79,13 @@ export class NodePackageResolver {
|
|
|
70
79
|
? `Workspace dependency ${request} declared by ${from.name} is missing`
|
|
71
80
|
: `Cannot resolve ${request} from ${from.name}`, request);
|
|
72
81
|
}
|
|
82
|
+
async #resolveLocal(request, from) {
|
|
83
|
+
const packageRoot = join(from.root, request);
|
|
84
|
+
if (await exists(join(packageRoot, 'package.json'))) {
|
|
85
|
+
return this.#readPackage(packageRoot, 'local');
|
|
86
|
+
}
|
|
87
|
+
throw new PackageResolutionError(`Cannot resolve ${request} from ${from.name}`, request);
|
|
88
|
+
}
|
|
73
89
|
async #readPackage(packageRoot, source) {
|
|
74
90
|
const normalized = await realpath(resolve(packageRoot));
|
|
75
91
|
const cached = this.#cache.get(normalized);
|
|
@@ -90,7 +106,10 @@ export class NodePackageResolver {
|
|
|
90
106
|
}
|
|
91
107
|
function declaredDependency(request, pkg) {
|
|
92
108
|
const specification = (pkg.dependencies?.[request]
|
|
93
|
-
?? pkg.optionalDependencies?.[request]
|
|
109
|
+
?? pkg.optionalDependencies?.[request]
|
|
110
|
+
// peerDependencies 是宽松声明:允许未安装。未安装时 resolve 抛
|
|
111
|
+
// PackageResolutionError,由引用方的 optional 标记统一容错。
|
|
112
|
+
?? pkg.peerDependencies?.[request]);
|
|
94
113
|
if (!specification) {
|
|
95
114
|
throw new PackageResolutionError(`${pkg.name} references ${request} in zhin manifest but does not declare it as a package dependency`, request);
|
|
96
115
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ChildPluginReference, PackageReference } from './manifest.js';
|
|
2
|
+
/**
|
|
3
|
+
* Package whose `zhin.features` define the Stable Feature composition
|
|
4
|
+
* (adapter / command / component / middleware).
|
|
5
|
+
*
|
|
6
|
+
* Root inherits these when it depends on `@zhin.js/core` directly, or on the
|
|
7
|
+
* `zhin.js` facade (which depends on `@zhin.js/core`).
|
|
8
|
+
*/
|
|
9
|
+
export declare const PLATFORM_FEATURE_CARRIER = "@zhin.js/core";
|
|
10
|
+
/**
|
|
11
|
+
* Install facade that may declare default child plugins and re-exports core
|
|
12
|
+
* authoring surfaces.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PLATFORM_FEATURE_FACADE = "zhin.js";
|
|
15
|
+
/**
|
|
16
|
+
* Merge user-declared features with inherited platform features.
|
|
17
|
+
* User references for the same package win (pin / override).
|
|
18
|
+
*/
|
|
19
|
+
export declare function mergeFeatureReferences(declared: readonly PackageReference[], inherited: readonly PackageReference[]): readonly PackageReference[];
|
|
20
|
+
/**
|
|
21
|
+
* Merge user-declared child plugins with facade defaults.
|
|
22
|
+
* User references for the same instanceKey win (pin / override).
|
|
23
|
+
*/
|
|
24
|
+
export declare function mergeChildPluginReferences(declared: readonly ChildPluginReference[], inherited: readonly ChildPluginReference[]): readonly ChildPluginReference[];
|
|
25
|
+
export declare function declaredPackageDependency(dependencies: Readonly<Record<string, string>> | undefined, optionalDependencies: Readonly<Record<string, string>> | undefined, name: string): string | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package whose `zhin.features` define the Stable Feature composition
|
|
3
|
+
* (adapter / command / component / middleware).
|
|
4
|
+
*
|
|
5
|
+
* Root inherits these when it depends on `@zhin.js/core` directly, or on the
|
|
6
|
+
* `zhin.js` facade (which depends on `@zhin.js/core`).
|
|
7
|
+
*/
|
|
8
|
+
export const PLATFORM_FEATURE_CARRIER = '@zhin.js/core';
|
|
9
|
+
/**
|
|
10
|
+
* Install facade that may declare default child plugins and re-exports core
|
|
11
|
+
* authoring surfaces.
|
|
12
|
+
*/
|
|
13
|
+
export const PLATFORM_FEATURE_FACADE = 'zhin.js';
|
|
14
|
+
/**
|
|
15
|
+
* Merge user-declared features with inherited platform features.
|
|
16
|
+
* User references for the same package win (pin / override).
|
|
17
|
+
*/
|
|
18
|
+
export function mergeFeatureReferences(declared, inherited) {
|
|
19
|
+
const seen = new Set(declared.map((item) => item.package));
|
|
20
|
+
const extras = inherited.filter((item) => !seen.has(item.package));
|
|
21
|
+
if (extras.length === 0)
|
|
22
|
+
return declared;
|
|
23
|
+
return Object.freeze([...declared, ...extras]);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Merge user-declared child plugins with facade defaults.
|
|
27
|
+
* User references for the same instanceKey win (pin / override).
|
|
28
|
+
*/
|
|
29
|
+
export function mergeChildPluginReferences(declared, inherited) {
|
|
30
|
+
const seen = new Set(declared.map((item) => item.instanceKey));
|
|
31
|
+
const extras = inherited.filter((item) => !seen.has(item.instanceKey));
|
|
32
|
+
if (extras.length === 0)
|
|
33
|
+
return declared;
|
|
34
|
+
return Object.freeze([...declared, ...extras]);
|
|
35
|
+
}
|
|
36
|
+
export function declaredPackageDependency(dependencies, optionalDependencies, name) {
|
|
37
|
+
return dependencies?.[name] ?? optionalDependencies?.[name];
|
|
38
|
+
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { DisposeStack, Scope, type Dispose, type GenerationHandoff, type GenerationHandoffRegistry, type PluginId, type PluginNodeSnapshot, type TokenId } from '@zhin.js/plugin-runtime';
|
|
1
|
+
import { DisposeStack, Scope, type Dispose, type FeatureId, type GenerationHandoff, type GenerationHandoffRegistry, 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
|
+
import { type PrimaryConfig } from './primary-config.js';
|
|
8
|
+
import type { RuntimeConfigDocument } from './config-composer.js';
|
|
7
9
|
export type PluginConfigResolver = (node: PluginGraphNode) => unknown;
|
|
8
10
|
export interface RootResourceContext {
|
|
9
11
|
readonly resources: Scope;
|
|
10
12
|
readonly lifecycle: DisposeStack;
|
|
11
13
|
readonly handoff: GenerationHandoffRegistry;
|
|
14
|
+
readonly config: PrimaryConfig;
|
|
12
15
|
}
|
|
13
16
|
export type RootResourceInstaller = (context: RootResourceContext) => void | Promise<void>;
|
|
14
17
|
export interface PluginAssemblySeed {
|
|
@@ -23,16 +26,19 @@ export declare class PluginScopeAssembler {
|
|
|
23
26
|
private readonly modules;
|
|
24
27
|
private readonly configResolver;
|
|
25
28
|
private readonly environment;
|
|
29
|
+
private readonly primaryConfigDocument;
|
|
26
30
|
private readonly installResources?;
|
|
27
31
|
private readonly isolation?;
|
|
28
32
|
readonly scopes: Map<PluginId, Scope>;
|
|
29
33
|
readonly tree: Map<PluginId, PluginNodeSnapshot>;
|
|
30
34
|
readonly config: Map<PluginId, unknown>;
|
|
31
35
|
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);
|
|
36
|
+
constructor(modules: ModuleRuntime, configResolver: PluginConfigResolver, environment: RuntimeEnvironment, primaryConfigDocument: RuntimeConfigDocument, installResources?: RootResourceInstaller | undefined, environmentLayers?: EnvironmentLayers, seed?: PluginAssemblySeed, isolation?: IsolatedPluginRuntimePort | undefined);
|
|
33
37
|
removeSubtrees(roots: readonly PluginId[]): void;
|
|
38
|
+
installSetupFeatureAliases(aliases: ReadonlyMap<string, FeatureId>): void;
|
|
34
39
|
setupTree(node: PluginGraphNode): Promise<void>;
|
|
35
40
|
synchronizeTree(node: PluginGraphNode): void;
|
|
36
41
|
createdScopeDisposers(): readonly (readonly [PluginId, Dispose])[];
|
|
37
42
|
generationHandoff(): GenerationHandoff | undefined;
|
|
43
|
+
setupCapabilities(): readonly Readonly<SetupCapabilityRegistration>[];
|
|
38
44
|
}
|