@tokensapi/dsh-plugin-check 0.3.4 → 0.4.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 +7 -5
- package/bin/dsh-plugin-check.mjs +20 -12
- package/docs/CHECKS.md +117 -18
- package/lib/client-check.mjs +98 -0
- package/lib/contract.mjs +130 -0
- package/lib/manifest-check.mjs +175 -138
- package/lib/patch-rows.mjs +126 -31
- package/lib/registry-check.mjs +78 -206
- package/lib/semver-rules.mjs +129 -0
- package/lib/smoke-check.mjs +13 -1
- package/lib/tarball.mjs +5 -1
- package/package.json +1 -1
package/lib/manifest-check.mjs
CHANGED
|
@@ -1,138 +1,175 @@
|
|
|
1
|
-
/* ============================================================
|
|
2
|
-
* 阶段①:清单体检(纯静态,秒级,离线)
|
|
3
|
-
* ============================================================
|
|
4
|
-
* 每一条规则都对应一种已经真实发生过的"把宿主搞崩/搞坏"路径,
|
|
5
|
-
* 规则说明写在 README;error 挡上架,warning 提示即将收紧或
|
|
6
|
-
* 潜在错配。判定为纯函数,便于单测与市场门禁复用。
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* 阶段①:清单体检(纯静态,秒级,离线)
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 每一条规则都对应一种已经真实发生过的"把宿主搞崩/搞坏"路径,
|
|
5
|
+
* 规则说明写在 README;error 挡上架,warning 提示即将收紧或
|
|
6
|
+
* 潜在错配。判定为纯函数,便于单测与市场门禁复用。
|
|
7
|
+
*
|
|
8
|
+
* 与会话内体检(registry-check.mjs)的关系:凡是只看清单就能判的
|
|
9
|
+
* 规则,两边**必须**调同一份实现(semver-rules / client-check /
|
|
10
|
+
* contract 里的谓词),否则同一个包在两条路径上会得出不同结论。
|
|
11
|
+
* tests/cross-path.test.mjs 是这条约束的闸门。
|
|
12
|
+
* ============================================================ */
|
|
13
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
14
|
+
import { join, resolve } from 'node:path'
|
|
15
|
+
import semver from 'semver'
|
|
16
|
+
import { checkPatchContent } from './patch-rows.mjs'
|
|
17
|
+
import { checkVersionRules } from './semver-rules.mjs'
|
|
18
|
+
import { checkClientDeclaration } from './client-check.mjs'
|
|
19
|
+
import {
|
|
20
|
+
BLOCKED_PACKAGE_NAMES,
|
|
21
|
+
HOST_NODE_ENGINES,
|
|
22
|
+
LIFECYCLE_SCRIPTS,
|
|
23
|
+
bundlePatchProblem,
|
|
24
|
+
isIdentityCore,
|
|
25
|
+
lifecycleMessage,
|
|
26
|
+
safePackageName,
|
|
27
|
+
} from './contract.mjs'
|
|
28
|
+
|
|
29
|
+
const SUSPICIOUS_FILE = /(\.env(\.|$)|id_rsa|\.pem$|\.p12$|\.key$)/u
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 只看清单就能判的规则,两条执行路径共用这一份。
|
|
33
|
+
* @param {object} manifest - package.json / registry 版本文档。
|
|
34
|
+
* @param {string | undefined} runtime - 目标 DSH 运行时版本。
|
|
35
|
+
* @returns {Array<{level: string, rule: string, message: string}>}
|
|
36
|
+
*/
|
|
37
|
+
export function checkManifestFields(manifest, runtime) {
|
|
38
|
+
const findings = []
|
|
39
|
+
const error = (rule, message) => findings.push({ level: 'error', rule, message })
|
|
40
|
+
const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
|
|
41
|
+
|
|
42
|
+
// M1 / M6 / M7
|
|
43
|
+
findings.push(...checkVersionRules(manifest, runtime))
|
|
44
|
+
|
|
45
|
+
// M1:包名形状。上游市场验证器在拿到清单之前就用这个谓词拒绝,
|
|
46
|
+
// 名字不合规的包无论内容多干净都装不进去。
|
|
47
|
+
if (typeof manifest.name === 'string' && manifest.name !== '') {
|
|
48
|
+
if (!safePackageName(manifest.name)) {
|
|
49
|
+
error('M1-manifest', `name "${manifest.name}" 不满足市场的包名约束`
|
|
50
|
+
+ `(小写字母/数字开头,只含 a-z 0-9 . _ -,可带一层 @scope,总长 ≤214);`
|
|
51
|
+
+ `不满足时受控安装会在验证阶段直接拒绝`)
|
|
52
|
+
}
|
|
53
|
+
if (BLOCKED_PACKAGE_NAMES.has(manifest.name)) {
|
|
54
|
+
error('M1-manifest', `name "${manifest.name}" 是宿主/市场自身的包名,被上游列入黑名单,永远装不进市场`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// M2
|
|
59
|
+
const scripts = manifest.scripts ?? {}
|
|
60
|
+
for (const script of LIFECYCLE_SCRIPTS) {
|
|
61
|
+
if (typeof scripts[script] === 'string') warning('M2-lifecycle', lifecycleMessage(script))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// M3
|
|
65
|
+
for (const name of Object.keys(manifest.dependencies ?? {})) {
|
|
66
|
+
if (isIdentityCore(name)) {
|
|
67
|
+
error('M3-core-peer', `${name} 出现在 dependencies;核心运行时必须声明为 peerDependencies,`
|
|
68
|
+
+ `否则宿主与插件各持一份实例,服务注册无法对上,典型症状是整个 Cowork 启动失败`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// M4:声明本身(补丁文件内容由调用方按各自的取文件方式补上)
|
|
73
|
+
const patch = manifest.dsh?.bundle?.patch
|
|
74
|
+
if (typeof patch !== 'string' || patch === '') {
|
|
75
|
+
error('M4-bundle', 'dsh.bundle.patch 缺失;宿主靠它把插件挂进加载树')
|
|
76
|
+
} else {
|
|
77
|
+
const problem = bundlePatchProblem(patch)
|
|
78
|
+
if (problem !== undefined) {
|
|
79
|
+
error('M4-bundle', `dsh.bundle.patch "${patch}" ${problem};`
|
|
80
|
+
+ `市场受控安装的验证器(上游 safeBundlePatch)会直接拒绝这个包,`
|
|
81
|
+
+ `用户端永远看不到一键安装按钮,只会看到手动安装命令`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// M8
|
|
86
|
+
const nodeEngine = manifest.engines?.node
|
|
87
|
+
if (nodeEngine === undefined) {
|
|
88
|
+
warning('M8-node-engines', 'engines.node 未声明;宿主运行在 Node 22+')
|
|
89
|
+
} else if (typeof nodeEngine !== 'string' || semver.validRange(nodeEngine) === null) {
|
|
90
|
+
warning('M8-node-engines', `engines.node "${nodeEngine}" 不是合法 SemVer 范围`)
|
|
91
|
+
} else if (semver.validRange(HOST_NODE_ENGINES) !== null
|
|
92
|
+
&& !semver.intersects(nodeEngine, HOST_NODE_ENGINES, { includePrerelease: true })) {
|
|
93
|
+
warning('M8-node-engines', `engines.node "${nodeEngine}" 与宿主的 "${HOST_NODE_ENGINES}" 无交集;`
|
|
94
|
+
+ `包管理器会在安装阶段报 unsupported engine`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// M9
|
|
98
|
+
if (manifest.license === undefined) warning('M9-license', 'license 未声明')
|
|
99
|
+
|
|
100
|
+
// M12
|
|
101
|
+
findings.push(...checkClientDeclaration(manifest))
|
|
102
|
+
|
|
103
|
+
// 宿主/Profile 侧字段被插件误抄
|
|
104
|
+
for (const field of ['profile', 'moduleFallback']) {
|
|
105
|
+
if (manifest.dsh?.[field] !== undefined) {
|
|
106
|
+
warning('M1-manifest', `dsh.${field} 是宿主/Profile 侧字段,插件包声明它没有作用,`
|
|
107
|
+
+ `多半是从 Profile 或宿主的 package.json 误抄过来的`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return findings
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 对一个插件目录执行全部清单规则。
|
|
116
|
+
* @param {string} pluginDir - 插件根目录(含 package.json)。
|
|
117
|
+
* @param {{ runtime?: string }} [options] - runtime 为目标 DSH 运行时
|
|
118
|
+
* 版本(如 0.1.3-alpha.1);给出时才执行 engine/peer 与其的交集判定。
|
|
119
|
+
* @returns {{ findings: Array<{level: 'error'|'warning', rule: string, message: string}>, manifest?: object, rows?: object[] }}
|
|
120
|
+
*/
|
|
121
|
+
export function checkManifest(pluginDir, options = {}) {
|
|
122
|
+
const findings = []
|
|
123
|
+
const error = (rule, message) => findings.push({ level: 'error', rule, message })
|
|
124
|
+
const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
|
|
125
|
+
|
|
126
|
+
const manifestPath = resolve(pluginDir, 'package.json')
|
|
127
|
+
if (!existsSync(manifestPath)) {
|
|
128
|
+
error('M1-manifest', 'package.json 不存在')
|
|
129
|
+
return { findings }
|
|
130
|
+
}
|
|
131
|
+
let manifest
|
|
132
|
+
try {
|
|
133
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
134
|
+
} catch (cause) {
|
|
135
|
+
error('M1-manifest', `package.json 不是合法 JSON: ${cause instanceof Error ? cause.message : String(cause)}`)
|
|
136
|
+
return { findings }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
findings.push(...checkManifestFields(manifest, options.runtime))
|
|
140
|
+
|
|
141
|
+
// M4/M5/M11 的补丁内容部分:目录路径直接读盘。
|
|
142
|
+
const patch = manifest.dsh?.bundle?.patch
|
|
143
|
+
let rows
|
|
144
|
+
if (typeof patch === 'string' && patch !== '') {
|
|
145
|
+
const patchPath = resolve(pluginDir, patch)
|
|
146
|
+
if (!existsSync(patchPath)) {
|
|
147
|
+
error('M4-bundle', `dsh.bundle.patch 指向的文件不存在: ${patch}`)
|
|
148
|
+
} else {
|
|
149
|
+
// 与会话内体检共用同一份实现,避免两条路径判定漂移。
|
|
150
|
+
const checked = checkPatchContent(
|
|
151
|
+
typeof manifest.name === 'string' ? manifest.name : '',
|
|
152
|
+
readFileSync(patchPath, 'utf8'),
|
|
153
|
+
)
|
|
154
|
+
findings.push(...checked.findings)
|
|
155
|
+
rows = checked.rows
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// M10:依赖文件系统,只在目录路径可判(registry 路径没有工作区,
|
|
160
|
+
// 这是有意的差异,不是漏 —— 见 docs/CHECKS.md §1)。
|
|
161
|
+
const filesList = Array.isArray(manifest.files) ? manifest.files : []
|
|
162
|
+
for (const entry of filesList) {
|
|
163
|
+
if (typeof entry === 'string' && SUSPICIOUS_FILE.test(entry)) {
|
|
164
|
+
error('M10-secrets', `files 白名单包含疑似凭据文件: ${entry}`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const candidate of ['.env', 'id_rsa']) {
|
|
168
|
+
const candidatePath = join(pluginDir, candidate)
|
|
169
|
+
if (existsSync(candidatePath) && statSync(candidatePath).isFile() && filesList.length === 0) {
|
|
170
|
+
warning('M10-secrets', `目录含 ${candidate} 且未用 files 白名单限定发布内容,npm pack 可能把它带上`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { findings, manifest, rows }
|
|
175
|
+
}
|
package/lib/patch-rows.mjs
CHANGED
|
@@ -7,23 +7,51 @@
|
|
|
7
7
|
* 不可静态求值(冒烟时跳过该行的 config)。
|
|
8
8
|
* ============================================================ */
|
|
9
9
|
import yaml from 'js-yaml'
|
|
10
|
+
import { RESERVED_ROW_IDS, RESERVED_ROW_NAMES } from './contract.mjs'
|
|
10
11
|
|
|
11
12
|
const JS_EXPR_PLACEHOLDER = Symbol('dsh-plugin-check:js-expr')
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 宽容标签前缀。
|
|
16
|
+
* js-yaml 的 multi type 查找是**对解析后的完整标签串做前缀匹配**,
|
|
17
|
+
* 所以这里必须写解析后的形式,而不是源码里的写法:
|
|
18
|
+
* - `!foo` → `!foo` (前缀 '!')
|
|
19
|
+
* - `!!js` → `tag:yaml.org,2002:js` (前缀 'tag:yaml.org,2002:js')
|
|
20
|
+
* - `!!js/function` → `tag:yaml.org,2002:js/function`(同一前缀覆盖)
|
|
21
|
+
*
|
|
22
|
+
* 只注册 '!' 曾经漏掉整个 `!!js` 家族 —— 而 `!!js` 恰恰是宿主唯一的
|
|
23
|
+
* 动态配置写法(标签定义见 deepseek-harness/scripts/cordis-yaml.ts:
|
|
24
|
+
* `new yaml.Type('tag:yaml.org,2002:js', { kind: 'scalar' })`),宿主
|
|
25
|
+
* 自带的 6 份 cordis.patch.yml 全部在用。于是"照抄宿主惯用法的插件"
|
|
26
|
+
* 被判成补丁不是合法 YAML → M4-bundle error。
|
|
27
|
+
*/
|
|
28
|
+
const TOLERANT_TAG_PREFIXES = ['!', 'tag:yaml.org,2002:js']
|
|
29
|
+
|
|
13
30
|
const tolerantSchema = yaml.DEFAULT_SCHEMA.extend(
|
|
14
|
-
|
|
15
|
-
kind,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
TOLERANT_TAG_PREFIXES.flatMap(prefix =>
|
|
32
|
+
['scalar', 'sequence', 'mapping'].map(kind => new yaml.Type(prefix, {
|
|
33
|
+
kind,
|
|
34
|
+
multi: true,
|
|
35
|
+
construct: () => ({ [JS_EXPR_PLACEHOLDER]: true }),
|
|
36
|
+
})),
|
|
37
|
+
),
|
|
19
38
|
)
|
|
20
39
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
40
|
+
/**
|
|
41
|
+
* 配置里是否携带无法静态求值的表达式(!!js 等自定义标签)。
|
|
42
|
+
* YAML 锚点可以自引用,递归必须带环保护,否则一份合法补丁能把体检
|
|
43
|
+
* 工具自己搞成栈溢出。
|
|
44
|
+
* @param {unknown} value
|
|
45
|
+
* @param {WeakSet<object>} [seen]
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function hasDynamicConfig(value, seen = new WeakSet()) {
|
|
23
49
|
if (value === null || typeof value !== 'object') return false
|
|
24
50
|
if (value[JS_EXPR_PLACEHOLDER] === true) return true
|
|
51
|
+
if (seen.has(value)) return false
|
|
52
|
+
seen.add(value)
|
|
25
53
|
const children = Array.isArray(value) ? value : Object.values(value)
|
|
26
|
-
return children.some(child => hasDynamicConfig(child))
|
|
54
|
+
return children.some(child => hasDynamicConfig(child, seen))
|
|
27
55
|
}
|
|
28
56
|
|
|
29
57
|
/**
|
|
@@ -82,27 +110,94 @@ export function extractPatchRows(content) {
|
|
|
82
110
|
})
|
|
83
111
|
}
|
|
84
112
|
candidates.forEach((entry, index) => visit(entry, `第 ${index + 1} 行`))
|
|
85
|
-
return { rows, problems }
|
|
113
|
+
return { rows, problems, entries: candidates }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* M11:补丁行的身份约束。
|
|
118
|
+
*
|
|
119
|
+
* 这一条查的不是"插件自己能不能跑",而是"装上之后用户的 Desktop
|
|
120
|
+
* 还起不起得来"——三种写法各自对应一个宿主硬失败:
|
|
121
|
+
*
|
|
122
|
+
* - 同层 id 重复:vendor/loader/src/config/group.ts:64 抛
|
|
123
|
+
* `TypeError: duplicate loader entry id: <id>`;dsh-plugin-desktop
|
|
124
|
+
* 还有一道前置检查 assertUniqueEntryIds(profile.ts:633-646,在
|
|
125
|
+
* :949 对合成后的整份行集调用)先抛 `duplicate loader entry id
|
|
126
|
+
* "<id>" in the composed profile`。
|
|
127
|
+
* - id 含 ':':EntryTree.sep === ':'(vendor/loader/src/config/tree.ts:8),
|
|
128
|
+
* 嵌套路径解析被破坏。
|
|
129
|
+
* - id/name 撞宿主保留身份:见 contract.mjs 的 RESERVED_ROW_IDS。
|
|
130
|
+
*
|
|
131
|
+
* 而市场安装路径装完**从不重新解析** cordis.patch.yml(service.ts:626-665
|
|
132
|
+
* 只比对版本号,且没有回滚),所以这类包一路绿灯装上、下次开机才炸。
|
|
133
|
+
*
|
|
134
|
+
* 唯一性按"层"判,与 assertUniqueEntryIds 同构:它每递归一层新建一个
|
|
135
|
+
* Set,所以父层与子层的同名 id 不算冲突。
|
|
136
|
+
*
|
|
137
|
+
* 查不到的部分(如实说明):跨插件、以及与宿主自身行集的 id 冲突需要
|
|
138
|
+
* 完整的宿主行清单,那要求本工具跟宿主同步演进,这里只查已知保留 id。
|
|
139
|
+
*
|
|
140
|
+
* @param {unknown[]} entries - extractPatchRows 返回的原始行(含 group 行)。
|
|
141
|
+
* @returns {Array<{level: 'error', rule: string, message: string}>}
|
|
142
|
+
*/
|
|
143
|
+
export function checkRowIdentity(entries) {
|
|
144
|
+
const findings = []
|
|
145
|
+
const error = (message) => findings.push({ level: 'error', rule: 'M11-row-identity', message })
|
|
146
|
+
|
|
147
|
+
const visitLevel = (level, at) => {
|
|
148
|
+
const seen = new Set()
|
|
149
|
+
for (const [index, entry] of level.entries()) {
|
|
150
|
+
if (entry === null || typeof entry !== 'object') continue
|
|
151
|
+
const where = `${at}第 ${index + 1} 行`
|
|
152
|
+
const id = entry.id
|
|
153
|
+
if (typeof id === 'string') {
|
|
154
|
+
if (seen.has(id)) {
|
|
155
|
+
error(`${where} 的 id "${id}" 与同层前面的行重复;`
|
|
156
|
+
+ `宿主合成加载树时会直接抛 duplicate loader entry id,`
|
|
157
|
+
+ `装上这个插件的用户下次启动整个 Desktop 都起不来(不只是本插件失效)`)
|
|
158
|
+
}
|
|
159
|
+
seen.add(id)
|
|
160
|
+
if (id.includes(':')) {
|
|
161
|
+
error(`${where} 的 id "${id}" 含 ":";`
|
|
162
|
+
+ `":" 是加载树的层级分隔符(EntryTree.sep),用在 id 里会破坏嵌套行的寻址`)
|
|
163
|
+
}
|
|
164
|
+
if (RESERVED_ROW_IDS.has(id)) {
|
|
165
|
+
error(`${where} 的 id "${id}" 是宿主保留行 id;`
|
|
166
|
+
+ `占用它会让宿主启动时抛错或让市场 provider 整体失效,请改用带包名前缀的 id`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (typeof entry.name === 'string' && RESERVED_ROW_NAMES.has(entry.name)) {
|
|
170
|
+
error(`${where} 的 name "${entry.name}" 是宿主保留包名;该行会被静默剥离并让市场 provider 失效`)
|
|
171
|
+
}
|
|
172
|
+
if (entry.group === true && Array.isArray(entry.config)) {
|
|
173
|
+
visitLevel(entry.config, `${where} group 内 `)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
visitLevel(Array.isArray(entries) ? entries : [], '')
|
|
179
|
+
return findings
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 对补丁内容执行 M4/M5 判定。
|
|
184
|
+
* 目录体检与会话内体检共用这一份实现:两边曾经各写各的,结果
|
|
185
|
+
* 会话内看不到 M5,把装上即崩的插件判成合格。
|
|
186
|
+
* @param {string} packageName - 本包 npm 包名。
|
|
187
|
+
* @param {string} content - cordis.patch.yml 全文。
|
|
188
|
+
* @returns {{ findings: Array<{level: 'error', rule: string, message: string}>, rows: object[] }}
|
|
189
|
+
*/
|
|
190
|
+
export function checkPatchContent(packageName, content) {
|
|
191
|
+
const findings = []
|
|
192
|
+
const error = (rule, message) => findings.push({ level: 'error', rule, message })
|
|
193
|
+
const { rows, problems, entries } = extractPatchRows(content)
|
|
194
|
+
for (const problem of problems) error('M4-bundle', `补丁行不合法: ${problem}`)
|
|
195
|
+
if (rows.length === 0 && problems.length === 0) error('M4-bundle', '补丁未声明任何插件行')
|
|
196
|
+
findings.push(...checkRowIdentity(entries))
|
|
197
|
+
for (const row of rows) {
|
|
198
|
+
if (packageName !== '' && row.name !== packageName && !row.name.startsWith(`${packageName}/`)) {
|
|
199
|
+
error('M5-row-scope', `补丁行 "${row.id ?? row.name}" 指向 ${row.name},超出本包命名空间(${packageName});插件只能挂载自己的入口`)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { findings, rows }
|
|
86
203
|
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 对补丁内容执行 M4/M5 判定。
|
|
90
|
-
* 目录体检与会话内体检共用这一份实现:两边曾经各写各的,结果
|
|
91
|
-
* 会话内看不到 M5,把装上即崩的插件判成合格。
|
|
92
|
-
* @param {string} packageName - 本包 npm 包名。
|
|
93
|
-
* @param {string} content - cordis.patch.yml 全文。
|
|
94
|
-
* @returns {{ findings: Array<{level: 'error', rule: string, message: string}>, rows: object[] }}
|
|
95
|
-
*/
|
|
96
|
-
export function checkPatchContent(packageName, content) {
|
|
97
|
-
const findings = []
|
|
98
|
-
const error = (rule, message) => findings.push({ level: 'error', rule, message })
|
|
99
|
-
const { rows, problems } = extractPatchRows(content)
|
|
100
|
-
for (const problem of problems) error('M4-bundle', `补丁行不合法: ${problem}`)
|
|
101
|
-
if (rows.length === 0 && problems.length === 0) error('M4-bundle', '补丁未声明任何插件行')
|
|
102
|
-
for (const row of rows) {
|
|
103
|
-
if (packageName !== '' && row.name !== packageName && !row.name.startsWith(`${packageName}/`)) {
|
|
104
|
-
error('M5-row-scope', `补丁行 "${row.id ?? row.name}" 指向 ${row.name},超出本包命名空间(${packageName});插件只能挂载自己的入口`)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return { findings, rows }
|
|
108
|
-
}
|