@coda-console/protocol 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/package.json +1 -1
- package/schemas/plugin/extension-points.json +8 -2
- package/schemas/plugin/mcp-servers.json +1 -0
- package/schemas/plugin/setting.json +2 -1
- package/validate.mjs +112 -47
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Coda 与插件之间的约定,Coda 和插件都引用它,独立演进:
|
|
4
4
|
|
|
5
|
-
- **插件清单的类型**(`schemas/plugin/`):插件目录 `package.json` 里 `contributes` 的每个键是一种类型,各有一份 JSON Schema(draft 2020-12,`$id` 为 `https://coda.pub/schemas/plugin/v1/<文件>`)。`plugin.json` 是整份清单,`setting.json`
|
|
5
|
+
- **插件清单的类型**(`schemas/plugin/`):插件目录 `package.json` 里 `contributes` 的每个键是一种类型,各有一份 JSON Schema(draft 2020-12,`$id` 为 `https://coda.pub/schemas/plugin/v1/<文件>`)。`plugin.json` 是整份清单,`setting.json` 是一项设置的声明。插件写的合乎它用到的类型就对;写给别的插件扩展点的键由那个插件声明,扩展点可以带自己的 schema(`extensionPoints.<名>.schema`,声明它的插件目录里的一个文件),写给它的东西合乎那份 schema 才对。
|
|
6
6
|
- **通用校验器**(`validate.mjs`):用 Ajv 对清单做 schema 校验。JSON Schema 说不了的只用五个通用关键字(`x-unique-by`、`x-max-contains`、`x-ref`、`x-placeholders`、`x-file`,照 Kubernetes 的 `x-kubernetes-*`),每个在哪里写都是同一个意思,写法见文件开头。校验器不随功能变长:类型加字段就是改 schema。
|
|
7
7
|
- **Agent 接入协议**(`docs/agent-protocol.md`):Coda 怎样用 ACP 接 Agent,以及 Coda 加的扩展(会话指令、插话、`_coda/wait`、执行失败、工具调用的审核)。
|
|
8
8
|
|
|
@@ -13,16 +13,24 @@ npm install @coda-console/protocol
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```js
|
|
16
|
-
import { manifestProblem, valueProblem, TYPES, SCHEMAS } from '@coda-console/protocol/validate';
|
|
16
|
+
import { manifestProblem, pointProblem, valueProblem, TYPES, SCHEMAS } from '@coda-console/protocol/validate';
|
|
17
17
|
|
|
18
18
|
// 整份清单;给出插件目录时,连同它点名的文件一起查(x-file)。合乎类型返回 null,否则是一句说明:哪里、schema 怎么说。
|
|
19
19
|
const problem = manifestProblem(JSON.parse(readFileSync('plugin/package.json', 'utf8')), 'plugin');
|
|
20
20
|
// 某个类型里的一部分,如输入框模式的参数(插件运行时给出的)。
|
|
21
21
|
valueProblem('composer-modes.json#/$defs/parameters', parameters, '插件给出的参数');
|
|
22
|
+
// 写给别的插件扩展点的东西,按那个扩展点的 schema(已读出的对象);给出写它的插件目录时连同它点名的文件一起查。
|
|
23
|
+
pointProblem(panelsSchema, manifest.contributes.panels, 'contributes.panels', 'plugin');
|
|
22
24
|
```
|
|
23
25
|
|
|
26
|
+
扩展点的 schema 和这里的并列:可以 `$ref` 协议的 schema(`common.json#/$defs/line40`),可以用五个关键字;它自己写的 `$id` 不算数,指针只指它校验的那份值里的位置。
|
|
27
|
+
|
|
24
28
|
`TYPES` 是 Coda 自己的类型(`contributes` 的键),`SCHEMAS` 是全部 schema(按文件名)。
|
|
25
29
|
|
|
30
|
+
## 项目中的多选选项
|
|
31
|
+
|
|
32
|
+
设置的 `items.anyOf` 选项可附带 `workflowKind`(`check`、`premerge`、`deploy`、`preview`、`release`)。全局设置显示全部选项;项目设置只显示该项目有的流程类别,未写此字段的选项始终显示。宿主负责按项目提供可选项,流程执行时仍需按运行的提交判断可用操作。
|
|
33
|
+
|
|
26
34
|
## 谁在用
|
|
27
35
|
|
|
28
36
|
- **Coda**:读插件时用它校验清单,再整理成自己的结构;只另看插件身份(ID、设置名在 ID 之下),以及装到一台机器上才知道的(来源、已装插件的冲突)。
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://coda.pub/schemas/plugin/v1/extension-points.json",
|
|
4
4
|
"title": "contributes.extensionPoints:插件声明的扩展点",
|
|
5
|
-
"description": "扩展点名 → 声明。只写 description 的,别的插件用 contributes.extensions 接一条命令;写了 contributes 的,任何插件在它的 contributes
|
|
5
|
+
"description": "扩展点名 → 声明。只写 description 的,别的插件用 contributes.extensions 接一条命令;写了 contributes 的,任何插件在它的 contributes 里用这个键写的东西归它,原样交给声明它的插件读,合乎它的 schema 才装得上。",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"propertyNames": {"$ref": "common.json#/$defs/id"},
|
|
8
8
|
"additionalProperties": {
|
|
@@ -46,12 +46,18 @@
|
|
|
46
46
|
"type": "object",
|
|
47
47
|
"propertyNames": {"pattern": "^(\\*|[A-Za-z][A-Za-z0-9]{0,39})(\\.(\\*|[A-Za-z][A-Za-z0-9]{0,39})){0,7}$"},
|
|
48
48
|
"additionalProperties": {"$ref": "common.json#/$defs/line40"}
|
|
49
|
+
},
|
|
50
|
+
"schema": {
|
|
51
|
+
"description": "别的插件在这个键下写的东西的 JSON Schema(draft 2020-12):本插件目录里的 .json 文件,至多 256 KiB。它和协议的类型并列,可以引用它们(common.json#/$defs/line40)、用协议的五个关键字;指针只指它自己校验的那份值里的位置",
|
|
52
|
+
"type": "string",
|
|
53
|
+
"pattern": "\\.json$",
|
|
54
|
+
"x-file": {"type": "file", "maxBytes": 262144, "schema": true}
|
|
49
55
|
}
|
|
50
56
|
},
|
|
51
57
|
"required": ["description"],
|
|
52
58
|
"additionalProperties": false,
|
|
53
59
|
"if": {"required": ["contributes"]},
|
|
54
60
|
"then": {"description": "写了 contributes 的扩展点也写 title", "required": ["title"]},
|
|
55
|
-
"else": {"description": "只有写了 contributes 的扩展点才写 title 与
|
|
61
|
+
"else": {"description": "只有写了 contributes 的扩展点才写 title、commands 与 schema", "not": {"anyOf": [{"required": ["title"]}, {"required": ["commands"]}, {"required": ["schema"]}]}}
|
|
56
62
|
}
|
|
57
63
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"maxProperties": 20,
|
|
8
8
|
"propertyNames": {"description": "服务器名只用小写字母、数字和「-」,以字母开头,至多 40 个字符,不能是 Coda 自己的 coda", "$ref": "common.json#/$defs/id", "not": {"const": "coda"}},
|
|
9
9
|
"additionalProperties": {
|
|
10
|
+
"description": "一个服务器:由 Agent 在本机启动、经标准输入输出说话的程序(stdio),写 command 与 args;远程的(http、sse)现在不支持",
|
|
10
11
|
"type": "object",
|
|
11
12
|
"properties": {
|
|
12
13
|
"type": {"description": "现在只支持由 Agent 在本机启动的服务器(stdio)", "const": "stdio"},
|
|
@@ -167,7 +167,8 @@
|
|
|
167
167
|
"properties": {
|
|
168
168
|
"const": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$"},
|
|
169
169
|
"title": {"type": "string", "maxLength": 400},
|
|
170
|
-
"description": {"type": "string", "maxLength": 400}
|
|
170
|
+
"description": {"type": "string", "maxLength": 400},
|
|
171
|
+
"workflowKind": {"description": "在项目设置里只在该项目有这一类流程时显示;全局设置列出全部选项", "enum": ["check", "premerge", "deploy", "preview", "release"]}
|
|
171
172
|
},
|
|
172
173
|
"required": ["const"],
|
|
173
174
|
"additionalProperties": false
|
package/validate.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// A plugin is right when what it writes is of the types it names: each key of a manifest's `contributes` is a type with a
|
|
2
2
|
// JSON Schema of its own (schemas/plugin/, `https://coda.pub/schemas/plugin/v1/…`), and a key that is no type of Coda's is
|
|
3
|
-
// one a plugin's extension point declares, for that plugin to read
|
|
3
|
+
// one a plugin's extension point declares, for that plugin to read: of the schema that point names, a file of the declaring
|
|
4
|
+
// plugin's (`extensionPoints.<point>.schema`), when it names one. Checking one is checking it against those schemas,
|
|
4
5
|
// nothing else: a field joins a type by joining its schema, never by a rule here. Coda checks a plugin with this module
|
|
5
6
|
// when it reads one, and coda-plugins' contract check does where a plugin is written.
|
|
6
7
|
// What JSON Schema cannot say of one value alone it says with five keywords, each meaning one thing wherever it is written,
|
|
@@ -13,10 +14,12 @@
|
|
|
13
14
|
// (`2/properties`: two levels up, then down); what it names satisfies `match` when that is given;
|
|
14
15
|
// - `x-placeholders`: { names?, config? } — every `${…}` in the text is one of `names`, or `config:<setting>` naming one of
|
|
15
16
|
// the plugin's settings that satisfies `config`;
|
|
16
|
-
// - `x-file`: { type, under?, suffix?, contains?, maxBytes?, magic?, absent? } — the value is a path in the plugin's
|
|
17
|
-
// (with `under` before it and `suffix` after it) that never leaves it, of that type, holding `contains`, at most
|
|
18
|
-
// `maxBytes` long, starting with `magic
|
|
17
|
+
// - `x-file`: { type, under?, suffix?, contains?, maxBytes?, magic?, schema?, absent? } — the value is a path in the plugin's
|
|
18
|
+
// directory (with `under` before it and `suffix` after it) that never leaves it, of that type, holding `contains`, at most
|
|
19
|
+
// `maxBytes` long, starting with `magic`, with `schema` a JSON Schema these keywords compile; with `absent`, a path there
|
|
20
|
+
// is none at. Checked when the directory is known.
|
|
19
21
|
// It imports nothing but Node and Ajv.
|
|
22
|
+
import { createHash } from 'node:crypto';
|
|
20
23
|
import { closeSync, openSync, readdirSync, readFileSync, readSync, statSync } from 'node:fs';
|
|
21
24
|
import { isAbsolute, join, normalize, sep } from 'node:path';
|
|
22
25
|
import { fileURLToPath } from 'node:url';
|
|
@@ -30,73 +33,111 @@ export const SCHEMAS = Object.fromEntries(readdirSync(SCHEMA_DIR).filter(file =>
|
|
|
30
33
|
// The types a plugin may contribute to Coda itself, as the manifest's schema names them.
|
|
31
34
|
export const TYPES = Object.keys(SCHEMAS['plugin.json'].properties.contributes.properties);
|
|
32
35
|
|
|
33
|
-
// What a pointer names, from where a keyword is: the manifest's root, or levels up from the value itself.
|
|
36
|
+
// What a pointer names, from where a keyword is: the manifest's root, or levels up from the value itself; and where that is.
|
|
34
37
|
function locate(pointer, { rootData, instancePath }) {
|
|
35
38
|
const relative = /^(\d+)(.*)$/.exec(pointer);
|
|
36
39
|
const up = relative ? Number(relative[1]) : 0, rest = relative ? relative[2] : pointer;
|
|
37
40
|
const here = instancePath.split('/').slice(1).map(unescape);
|
|
38
|
-
if (up > here.length) return undefined;
|
|
41
|
+
if (up > here.length) return { at: undefined, path: '' };
|
|
39
42
|
const steps = [...(relative ? here.slice(0, here.length - up) : []), ...rest.split('/').slice(1).map(unescape)];
|
|
40
|
-
return steps.reduce((at, step) => at !== null && typeof at === 'object' && Object.hasOwn(at, step) ? at[step] : undefined, rootData)
|
|
43
|
+
return { at: steps.reduce((at, step) => at !== null && typeof at === 'object' && Object.hasOwn(at, step) ? at[step] : undefined, rootData),
|
|
44
|
+
path: steps.map(step => `/${escape(step)}`).join('') };
|
|
41
45
|
}
|
|
42
46
|
const unescape = step => step.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
47
|
+
const escape = step => String(step).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
43
48
|
const fail = (validate, keyword, message, parentSchema) => { validate.errors = [{ keyword, message, params: {}, parentSchema }]; return false; };
|
|
44
49
|
|
|
50
|
+
// What each keyword is written with, checked where a schema using it is compiled: one written wrong is refused there, as an
|
|
51
|
+
// extension point's schema is (x-file `schema`), not where a value is checked against it.
|
|
52
|
+
const SCHEMA_VALUE = { type: ['object', 'boolean'] };
|
|
53
|
+
const META = {
|
|
54
|
+
'x-max-contains': { type: 'object', properties: { match: SCHEMA_VALUE, max: { type: 'integer', minimum: 0 } }, required: ['match', 'max'], additionalProperties: false },
|
|
55
|
+
'x-unique-by': { type: 'array', minItems: 1, items: { anyOf: [{ type: 'string' },
|
|
56
|
+
{ type: 'object', properties: { part: { type: 'string' } }, required: ['part'], additionalProperties: false }] } },
|
|
57
|
+
'x-ref': { type: 'object', properties: { pointer: { type: 'string', pattern: '^(?:\\d+(?:/.*)?|/.*)$' }, part: { type: 'string' }, match: SCHEMA_VALUE },
|
|
58
|
+
required: ['pointer'], additionalProperties: false },
|
|
59
|
+
'x-placeholders': { type: 'object', properties: { names: { type: 'array', items: { type: 'string' } }, config: SCHEMA_VALUE }, additionalProperties: false },
|
|
60
|
+
'x-file': { type: 'object', properties: { type: { enum: ['file', 'directory'] }, under: { type: 'string' }, suffix: { type: 'string' }, contains: { type: 'string' },
|
|
61
|
+
maxBytes: { type: 'integer', minimum: 0 }, magic: { type: 'string' }, schema: { type: 'boolean' }, absent: { type: 'boolean' } },
|
|
62
|
+
required: ['type'], additionalProperties: false },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// A validator that answers now: one of a schema written `$async` answers later, with a promise that would pass for a yes.
|
|
66
|
+
function now(validate) {
|
|
67
|
+
if (validate.$async) throw new Error('不能是异步的($async)');
|
|
68
|
+
return validate;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A schema a keyword is written with (`match`, `config`), for a value elsewhere in what is checked: compiled on its own as a
|
|
72
|
+
// validator that answers now, and run as one part of the whole — with what the whole is checked with (`self`, Ajv's context:
|
|
73
|
+
// the plugin's directory), on the whole value, at the value's place in it — so that its pointers and files are as the
|
|
74
|
+
// schema around it has them.
|
|
75
|
+
function inner(ajv, schema) {
|
|
76
|
+
const validate = now(ajv.compile(schema));
|
|
77
|
+
return (self, value, path, { rootData }) => validate.call(self, value, { instancePath: path, rootData, dynamicAnchors: {} });
|
|
78
|
+
}
|
|
79
|
+
|
|
45
80
|
function keywords(ajv) {
|
|
46
81
|
ajv.addKeyword({
|
|
47
|
-
keyword: 'x-max-contains', type: 'object', schemaType: 'object',
|
|
82
|
+
keyword: 'x-max-contains', type: 'object', schemaType: 'object', metaSchema: META['x-max-contains'],
|
|
48
83
|
compile({ match, max }) {
|
|
49
|
-
const
|
|
50
|
-
return
|
|
84
|
+
const matches = inner(ajv, match);
|
|
85
|
+
return function (value, cxt) {
|
|
86
|
+
return Object.entries(value).filter(([key, each]) => matches(this, each, `${cxt.instancePath}/${escape(key)}`, cxt)).length <= max;
|
|
87
|
+
};
|
|
51
88
|
},
|
|
52
89
|
});
|
|
53
90
|
ajv.addKeyword({
|
|
54
|
-
keyword: 'x-unique-by', type: 'array', schemaType: 'array', errors: true,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
for (const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
keyword: 'x-unique-by', type: 'array', schemaType: 'array', metaSchema: META['x-unique-by'], errors: true,
|
|
92
|
+
compile(keys, parentSchema) {
|
|
93
|
+
const by = keys.map(key => typeof key === 'object' ? { part: new RegExp(key.part, 'u') } : { key });
|
|
94
|
+
return function unique(items) {
|
|
95
|
+
for (const { key, part } of by) {
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const item of items) {
|
|
98
|
+
const value = part ? (typeof item === 'string' ? part.exec(item)?.[1] : undefined)
|
|
99
|
+
: item && typeof item === 'object' && Object.hasOwn(item, key) ? item[key] : undefined;
|
|
100
|
+
if (value === undefined) continue;
|
|
101
|
+
const said = JSON.stringify(value);
|
|
102
|
+
if (seen.has(said)) return fail(unique, 'x-unique-by', part ? `不能重复:${value}` : `${key} 不能重复:${value}`, parentSchema);
|
|
103
|
+
seen.add(said);
|
|
104
|
+
}
|
|
65
105
|
}
|
|
66
|
-
|
|
67
|
-
|
|
106
|
+
return true;
|
|
107
|
+
};
|
|
68
108
|
},
|
|
69
109
|
});
|
|
70
110
|
// A reference: whether `name` is a key (or element) of what `pointer` names, and that satisfies `match`.
|
|
71
|
-
|
|
72
|
-
const at = locate(ref.pointer, cxt);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
111
|
+
function resolves(self, ref, matches, name, cxt) {
|
|
112
|
+
const { at, path } = locate(ref.pointer, cxt);
|
|
113
|
+
const key = Array.isArray(at) ? at.indexOf(name) : at !== null && typeof at === 'object' && Object.hasOwn(at, name) ? name : -1;
|
|
114
|
+
return key !== -1 && (!matches || matches(self, at[key], `${path}/${escape(key)}`, cxt));
|
|
115
|
+
}
|
|
77
116
|
ajv.addKeyword({
|
|
78
|
-
keyword: 'x-ref', type: 'string', schemaType: 'object',
|
|
117
|
+
keyword: 'x-ref', type: 'string', schemaType: 'object', metaSchema: META['x-ref'],
|
|
79
118
|
compile(ref) {
|
|
80
|
-
const
|
|
81
|
-
return (value, cxt)
|
|
119
|
+
const matches = ref.match !== undefined ? inner(ajv, ref.match) : null, part = ref.part !== undefined ? new RegExp(ref.part, 'u') : null;
|
|
120
|
+
return function (value, cxt) {
|
|
82
121
|
const name = part ? part.exec(value)?.[1] : value;
|
|
83
|
-
return name !== undefined && resolves(ref,
|
|
122
|
+
return name !== undefined && resolves(this, ref, matches, name, cxt);
|
|
84
123
|
};
|
|
85
124
|
},
|
|
86
125
|
});
|
|
87
126
|
ajv.addKeyword({
|
|
88
|
-
keyword: 'x-placeholders', type: 'string', schemaType: 'object',
|
|
127
|
+
keyword: 'x-placeholders', type: 'string', schemaType: 'object', metaSchema: META['x-placeholders'],
|
|
89
128
|
compile(allowed) {
|
|
90
|
-
const config = allowed.config ? { pointer: '/contributes/configuration/properties' } : null,
|
|
91
|
-
return (value, cxt)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
129
|
+
const config = allowed.config !== undefined ? { pointer: '/contributes/configuration/properties' } : null, matches = config && inner(ajv, allowed.config);
|
|
130
|
+
return function (value, cxt) {
|
|
131
|
+
return [...value.matchAll(/\$\{([^}]*)\}/g)].every(([, place]) => {
|
|
132
|
+
if ((allowed.names ?? []).includes(place)) return true;
|
|
133
|
+
const setting = /^config:(.+)$/.exec(place)?.[1];
|
|
134
|
+
return !!(config && setting && resolves(this, config, matches, setting, cxt));
|
|
135
|
+
});
|
|
136
|
+
};
|
|
96
137
|
},
|
|
97
138
|
});
|
|
98
139
|
ajv.addKeyword({
|
|
99
|
-
keyword: 'x-file', type: 'string', schemaType: 'object', errors: true,
|
|
140
|
+
keyword: 'x-file', type: 'string', schemaType: 'object', metaSchema: META['x-file'], errors: true,
|
|
100
141
|
validate: function file(spec, value, parentSchema) {
|
|
101
142
|
const dir = this?.dir;
|
|
102
143
|
const path = normalize(spec.under ? join(spec.under, `${value}${spec.suffix ?? ''}`) : `${value}${spec.suffix ?? ''}`);
|
|
@@ -114,6 +155,8 @@ function keywords(ajv) {
|
|
|
114
155
|
try { readSync(fd, head, 0, head.length, 0); } finally { closeSync(fd); }
|
|
115
156
|
if (head.toString('latin1') !== spec.magic) return fail(file, 'x-file', `${path} 不是 ${spec.magic.replace(/\W/g, '')}`, parentSchema);
|
|
116
157
|
}
|
|
158
|
+
if (spec.schema) try { declared(JSON.parse(readFileSync(join(dir, path), 'utf8'))); }
|
|
159
|
+
catch (e) { return fail(file, 'x-file', `${path} 不是可用的 JSON Schema:${e.message}`, parentSchema); }
|
|
117
160
|
return true;
|
|
118
161
|
},
|
|
119
162
|
});
|
|
@@ -128,6 +171,20 @@ const validator = file => {
|
|
|
128
171
|
if (!compiled.has(file)) compiled.set(file, ajv.getSchema(`${BASE}${file}`));
|
|
129
172
|
return compiled.get(file);
|
|
130
173
|
};
|
|
174
|
+
// The schema an extension point declares, compiled once by what it says, under an ID of its own beside these (whatever `$id`
|
|
175
|
+
// it wrote), so that it builds on them as they build on one another (`common.json#/$defs/line40`): its validator, and the
|
|
176
|
+
// schema as compiled, for what it says of a failing value. One that answers later (`$async`) is refused.
|
|
177
|
+
const points = new Map();
|
|
178
|
+
function declared(schema) {
|
|
179
|
+
const text = JSON.stringify(schema);
|
|
180
|
+
if (!points.has(text)) {
|
|
181
|
+
// `true` and `false` say all they say as they are; what is no schema at all Ajv refuses.
|
|
182
|
+
const object = schema !== null && typeof schema === 'object' && !Array.isArray(schema);
|
|
183
|
+
const own = object ? { ...schema, $id: `${BASE}point-${createHash('sha256').update(text).digest('hex').slice(0, 16)}.json` } : schema;
|
|
184
|
+
points.set(text, { validate: now((object && ajv.getSchema(own.$id)) || ajv.compile(own)), schema: own });
|
|
185
|
+
}
|
|
186
|
+
return points.get(text);
|
|
187
|
+
}
|
|
131
188
|
|
|
132
189
|
// Where in the manifest a value is, as a person writes it: `contributes.composerModes[0].in`.
|
|
133
190
|
const place = (path, base) => path.split('/').slice(1).map(unescape)
|
|
@@ -140,11 +197,11 @@ const nodes = function* (node) {
|
|
|
140
197
|
yield node;
|
|
141
198
|
for (const child of Object.values(node)) yield* nodes(child);
|
|
142
199
|
};
|
|
143
|
-
function described(error, { roots = true } = {}) {
|
|
200
|
+
function described(error, { roots = true, schemas = [] } = {}) {
|
|
144
201
|
if (!error.parentSchema) return undefined;
|
|
145
202
|
if (error.parentSchema.description) return roots || !error.parentSchema.$id ? error.parentSchema.description : undefined;
|
|
146
203
|
const steps = error.schemaPath.replace(/^#\//, '').split('/').map(unescape).slice(0, -1);
|
|
147
|
-
for (const root of Object.values(SCHEMAS)) for (const from of nodes(root)) {
|
|
204
|
+
for (const root of [...schemas, ...Object.values(SCHEMAS)]) for (const from of nodes(root)) {
|
|
148
205
|
const path = [from];
|
|
149
206
|
for (const step of steps) path.push(path.at(-1)?.[step]);
|
|
150
207
|
if (path.at(-1) !== error.parentSchema) continue;
|
|
@@ -155,16 +212,17 @@ function described(error, { roots = true } = {}) {
|
|
|
155
212
|
}
|
|
156
213
|
// What is wrong, said once: the first error, by its place and what the schema says of the value there.
|
|
157
214
|
// What a whole type says of itself is too much for a key it lacks or one written twice: those say only what is wrong.
|
|
158
|
-
|
|
215
|
+
// `schemas`: those besides these the value was checked against (an extension point's).
|
|
216
|
+
export function explain(errors, base = '', schemas = []) {
|
|
159
217
|
const [error] = errors;
|
|
160
218
|
const at = place(error.instancePath, base) || 'package.json', name = error.propertyName ? `「${error.propertyName}」` : '';
|
|
161
219
|
if (error.keyword === 'x-unique-by') return `${at}${name}:${error.message}`;
|
|
162
|
-
if (error.keyword === 'required') { const said = described(error, { roots: false }); return `${at} 缺 ${error.params.missingProperty}${said ? `:${said}` : ''}`; }
|
|
220
|
+
if (error.keyword === 'required') { const said = described(error, { roots: false, schemas }); return `${at} 缺 ${error.params.missingProperty}${said ? `:${said}` : ''}`; }
|
|
163
221
|
if (error.keyword === 'additionalProperties' || error.keyword === 'unevaluatedProperties')
|
|
164
222
|
return `${at} 里有不认识的键:${error.params.additionalProperty ?? error.params.unevaluatedProperty}`;
|
|
165
|
-
const said = described(error);
|
|
223
|
+
const said = described(error, { schemas });
|
|
166
224
|
if (error.keyword === 'type') return `${at}${name} 须为${TYPE_NAMES[error.params.type] ?? error.params.type}${said ? `:${said}` : ''}`;
|
|
167
|
-
if (error.keyword === 'x-file') { const own = described(error, { roots: false }); return `${at}${name}:${error.message}${own ? `(${own})` : ''}`; }
|
|
225
|
+
if (error.keyword === 'x-file') { const own = described(error, { roots: false, schemas }); return `${at}${name}:${error.message}${own ? `(${own})` : ''}`; }
|
|
168
226
|
return `${at}${name}:${said ?? error.message}`;
|
|
169
227
|
}
|
|
170
228
|
|
|
@@ -180,3 +238,10 @@ export function valueProblem(ref, value, where) {
|
|
|
180
238
|
if (!validate) throw new Error(`没有这个 schema:${ref}`);
|
|
181
239
|
return validate.call({ dir: null }, value) ? null : explain(validate.errors, where);
|
|
182
240
|
}
|
|
241
|
+
// What a plugin writes for another's extension point (`value`, under `where`, as `contributes.panels`), checked against the
|
|
242
|
+
// schema that point declares (the declaring plugin's file, read): null, or what is wrong. `dir`: the writing plugin's
|
|
243
|
+
// directory, for the files it names. The schema sees the value alone: its pointers name places in it.
|
|
244
|
+
export function pointProblem(schema, value, where, dir = null) {
|
|
245
|
+
const point = declared(schema);
|
|
246
|
+
return point.validate.call({ dir }, value) ? null : explain(point.validate.errors, where, [point.schema]);
|
|
247
|
+
}
|