@zhin.js/runtime 1.1.9 → 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/lib/config-composer.js +10 -1
- 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/manifest.d.ts +10 -0
- package/lib/manifest.js +50 -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/package.json +14 -14
package/lib/config-composer.js
CHANGED
|
@@ -14,6 +14,8 @@ export const HOST_CONFIG_SCHEMA = deepFreeze(hostConfigSchema);
|
|
|
14
14
|
/** Host configuration keys consumed by Runtime composition and Host installers. */
|
|
15
15
|
export const HOST_CONFIG_KEYS = Object.freeze(Object.keys(HOST_CONFIG_SCHEMA.properties));
|
|
16
16
|
const ROOT_FEATURE_CONFIG_KEYS = ['menuKeyword', 'menuStyle'];
|
|
17
|
+
/** Command routing keys the framework injects into every Plugin config schema. */
|
|
18
|
+
const COMMAND_CONFIG_KEYS = ['commandNamespace', 'disabledCommands'];
|
|
17
19
|
export class ConfigSchemaCollisionError extends Error {
|
|
18
20
|
plugin;
|
|
19
21
|
instanceKey;
|
|
@@ -214,7 +216,7 @@ async function readOwnSchema(node, modules, validatedSchemas) {
|
|
|
214
216
|
throw new TypeError(`${file} root schema must declare properties; composition keywords (anyOf/oneOf/allOf/$ref) are not supported`);
|
|
215
217
|
}
|
|
216
218
|
const properties = schemaProperties(schema);
|
|
217
|
-
for (const key of [
|
|
219
|
+
for (const key of [...COMMAND_CONFIG_KEYS, ...(!node.parent ? ROOT_FEATURE_CONFIG_KEYS : [])]) {
|
|
218
220
|
if (Object.hasOwn(properties, key))
|
|
219
221
|
throw new ConfigSchemaCollisionError(node.id, key);
|
|
220
222
|
}
|
|
@@ -231,6 +233,13 @@ async function readOwnSchema(node, modules, validatedSchemas) {
|
|
|
231
233
|
description: 'Explicit command namespace prepended after the Endpoint commandPrefix.',
|
|
232
234
|
'x-descriptionZh': '显式指令命名空间;在 Endpoint commandPrefix 之后参与路由。',
|
|
233
235
|
},
|
|
236
|
+
disabledCommands: {
|
|
237
|
+
type: 'array',
|
|
238
|
+
uniqueItems: true,
|
|
239
|
+
items: { type: 'string', minLength: 1, pattern: '^\\S+(?: \\S+)*$' },
|
|
240
|
+
description: 'Commands of this Plugin instance to leave out of routing, by local path without commandNamespace or parameters (e.g. "fanyi", "netease login"). A parent path also disables its subcommands.',
|
|
241
|
+
'x-descriptionZh': '不注册的指令:写插件内的指令路径(不含 commandNamespace 和参数,如 fanyi、netease login),写上级路径会连同子命令一起禁用。',
|
|
242
|
+
},
|
|
234
243
|
...properties,
|
|
235
244
|
},
|
|
236
245
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface EditorTempPattern {
|
|
2
|
+
readonly editor: string;
|
|
3
|
+
readonly test: (name: string) => boolean;
|
|
4
|
+
}
|
|
5
|
+
/** Ordered, documented list; each entry is covered by a unit test. */
|
|
6
|
+
export declare const EDITOR_TEMP_FILE_PATTERNS: readonly EditorTempPattern[];
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when `source` (a path or a bare file name) is an editor temp or
|
|
9
|
+
* backup file. Only the basename is inspected.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isEditorTempFile(source: string): boolean;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor temp, swap, lock, and backup files that must never trigger an HMR
|
|
3
|
+
* reload (#857 P2-11).
|
|
4
|
+
*
|
|
5
|
+
* Editors write these next to the real source while saving. Several of them
|
|
6
|
+
* keep the source extension (for example `.!97910!index.ts`), so an
|
|
7
|
+
* extension allow-list alone is not enough.
|
|
8
|
+
*
|
|
9
|
+
* Matching uses the basename only, so a directory such as `tmp/` or a file
|
|
10
|
+
* such as `tmp.ts` or `foo.tmp.ts` is still watched: those are ordinary
|
|
11
|
+
* sources a plugin may really import. Only the exact shapes below are skipped.
|
|
12
|
+
*/
|
|
13
|
+
import { basename } from 'node:path';
|
|
14
|
+
/** Ordered, documented list; each entry is covered by a unit test. */
|
|
15
|
+
export const EDITOR_TEMP_FILE_PATTERNS = Object.freeze([
|
|
16
|
+
// `.!<digits>!<name>`: atomic-save temp file seen in practice (`.!97910!index.ts`).
|
|
17
|
+
{ editor: 'atomic save (.!<pid>!name)', test: (name) => /^\.!\d+!/.test(name) },
|
|
18
|
+
// Vim swap files: `.index.ts.swp`, then `.swo`, `.swx` when the first one is taken.
|
|
19
|
+
{ editor: 'vim swap (*.swp, *.swo, *.swx)', test: (name) => /\.sw[pox]$/.test(name) },
|
|
20
|
+
// Vim `writebackup` probe: Vim creates and deletes `4913` to test the directory.
|
|
21
|
+
{ editor: 'vim probe (4913)', test: (name) => name === '4913' },
|
|
22
|
+
// Backup copies from Vim, Emacs, nano and others: `index.ts~`.
|
|
23
|
+
{ editor: 'backup (*~)', test: (name) => name.endsWith('~') },
|
|
24
|
+
// Emacs lock files: `.#index.ts` (usually a dangling symlink).
|
|
25
|
+
{ editor: 'emacs lock (.#*)', test: (name) => name.startsWith('.#') },
|
|
26
|
+
// Generic temp files: `index.ts.tmp`. `foo.tmp.ts` is a real source and still reloads.
|
|
27
|
+
{ editor: 'temp (*.tmp)', test: (name) => name.endsWith('.tmp') },
|
|
28
|
+
// JetBrains "safe write": `index.ts___jb_tmp___` and `index.ts___jb_old___`.
|
|
29
|
+
{
|
|
30
|
+
editor: 'JetBrains safe write (*___jb_tmp___, *___jb_old___)',
|
|
31
|
+
test: (name) => name.endsWith('___jb_tmp___') || name.endsWith('___jb_old___'),
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Returns true when `source` (a path or a bare file name) is an editor temp or
|
|
36
|
+
* backup file. Only the basename is inspected.
|
|
37
|
+
*/
|
|
38
|
+
export function isEditorTempFile(source) {
|
|
39
|
+
// Accept both separators so Windows paths behave the same on every host.
|
|
40
|
+
const name = basename(source.replaceAll('\\', '/'));
|
|
41
|
+
if (!name)
|
|
42
|
+
return false;
|
|
43
|
+
return EDITOR_TEMP_FILE_PATTERNS.some((pattern) => pattern.test(name));
|
|
44
|
+
}
|
package/lib/hmr-coordinator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeSourcePath } from './source-path.js';
|
|
2
2
|
import { InvalidationPlanner, } from './invalidation-planner.js';
|
|
3
|
+
import { isEditorTempFile } from './editor-temp-file.js';
|
|
3
4
|
export class HmrCoordinator {
|
|
4
5
|
options;
|
|
5
6
|
#pending = new Set();
|
|
@@ -24,6 +25,9 @@ export class HmrCoordinator {
|
|
|
24
25
|
}
|
|
25
26
|
this.#syncModuleState();
|
|
26
27
|
this.#unwatch = this.options.modules.watch((source) => {
|
|
28
|
+
// Any ModuleRuntime watcher may report editor temp/backup files; they never reload (#857).
|
|
29
|
+
if (isEditorTempFile(source))
|
|
30
|
+
return;
|
|
27
31
|
void this.enqueue(source).catch(() => undefined);
|
|
28
32
|
});
|
|
29
33
|
return () => this.stop();
|
package/lib/manifest.d.ts
CHANGED
|
@@ -53,3 +53,13 @@ export declare class ManifestValidationError extends Error {
|
|
|
53
53
|
constructor(issues: readonly string[]);
|
|
54
54
|
}
|
|
55
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') {
|
|
@@ -132,7 +174,11 @@ function parseReferences(value, source, issues, child) {
|
|
|
132
174
|
}];
|
|
133
175
|
});
|
|
134
176
|
}
|
|
135
|
-
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
|
+
}
|
|
136
182
|
if (typeof value !== 'string' || !value.startsWith('./')) {
|
|
137
183
|
issues.push(`${source} must be a package-relative path starting with ./`);
|
|
138
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/runtime",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"description": "Static Plugin graph, generation transaction and HMR Root runtime for Zhin.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -19,25 +19,25 @@
|
|
|
19
19
|
"es-module-lexer": "2.3.1",
|
|
20
20
|
"import-meta-resolve": "4.2.0",
|
|
21
21
|
"semver": "7.8.5",
|
|
22
|
-
"@zhin.js/feature-kit": "1.1.
|
|
22
|
+
"@zhin.js/feature-kit": "1.1.7",
|
|
23
23
|
"@zhin.js/plugin-runtime": "1.1.15"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^26.1.2",
|
|
27
27
|
"tsx": "^4.23.1",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
|
-
"@zhin.js/adapter": "1.1.
|
|
30
|
-
"@zhin.js/
|
|
31
|
-
"@zhin.js/
|
|
32
|
-
"@zhin.js/
|
|
33
|
-
"@zhin.js/
|
|
34
|
-
"@zhin.js/
|
|
35
|
-
"@zhin.js/
|
|
36
|
-
"@zhin.js/
|
|
37
|
-
"@zhin.js/
|
|
38
|
-
"@zhin.js/
|
|
39
|
-
"@zhin.js/
|
|
40
|
-
"@zhin.js/
|
|
29
|
+
"@zhin.js/adapter": "1.1.21",
|
|
30
|
+
"@zhin.js/agent-feature": "1.1.8",
|
|
31
|
+
"@zhin.js/command": "1.1.9",
|
|
32
|
+
"@zhin.js/component": "1.1.8",
|
|
33
|
+
"@zhin.js/layout": "1.1.8",
|
|
34
|
+
"@zhin.js/page": "1.1.8",
|
|
35
|
+
"@zhin.js/handler": "1.1.7",
|
|
36
|
+
"@zhin.js/middleware": "1.1.8",
|
|
37
|
+
"@zhin.js/prompt-section": "1.1.7",
|
|
38
|
+
"@zhin.js/mcp-feature": "1.1.8",
|
|
39
|
+
"@zhin.js/skill": "1.1.8",
|
|
40
|
+
"@zhin.js/tool": "1.1.8"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|