@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/registry-check.mjs
CHANGED
|
@@ -1,206 +1,78 @@
|
|
|
1
|
-
/* ============================================================
|
|
2
|
-
* 已发布包体检(
|
|
3
|
-
* ============================================================
|
|
4
|
-
* 对 npm registry 的版本文档跑清单规则,与目录体检(manifest-check)
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let holds = true
|
|
80
|
-
let valid = true
|
|
81
|
-
for (const comparator of comparators) {
|
|
82
|
-
if (comparator === '*' || comparator === 'x') continue
|
|
83
|
-
const match = /^(>=|<=|>|<|=|\^|~)?(.+)$/u.exec(comparator)
|
|
84
|
-
const boundary = parseVersion(match[2])
|
|
85
|
-
if (boundary === null) { valid = false; break }
|
|
86
|
-
const operator = match[1] ?? '='
|
|
87
|
-
if (operator === '^') {
|
|
88
|
-
const upper = boundary.parts[0] > 0
|
|
89
|
-
? { parts: [boundary.parts[0] + 1, 0, 0], prerelease: ['0'] }
|
|
90
|
-
: boundary.parts[1] > 0
|
|
91
|
-
? { parts: [0, boundary.parts[1] + 1, 0], prerelease: ['0'] }
|
|
92
|
-
: { parts: [0, 0, boundary.parts[2] + 1], prerelease: ['0'] }
|
|
93
|
-
holds = holds && comparatorHolds(version, '>=', boundary) && comparatorHolds(version, '<', upper)
|
|
94
|
-
} else if (operator === '~') {
|
|
95
|
-
const upper = { parts: [boundary.parts[0], boundary.parts[1] + 1, 0], prerelease: ['0'] }
|
|
96
|
-
holds = holds && comparatorHolds(version, '>=', boundary) && comparatorHolds(version, '<', upper)
|
|
97
|
-
} else {
|
|
98
|
-
holds = holds && comparatorHolds(version, operator, boundary)
|
|
99
|
-
}
|
|
100
|
-
if (!holds) break
|
|
101
|
-
}
|
|
102
|
-
if (!valid) continue
|
|
103
|
-
recognized = true
|
|
104
|
-
if (holds) return true
|
|
105
|
-
}
|
|
106
|
-
return recognized ? false : null
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/* --------------------------- 规则 --------------------------- */
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* 对一份 npm 版本清单跑体检规则。
|
|
113
|
-
* @param {object} manifest - registry 版本文档(scripts/dependencies/
|
|
114
|
-
* peerDependencies/dsh/engines/license/version)。
|
|
115
|
-
* @param {string} runtime - 目标 DSH 运行时版本。
|
|
116
|
-
* @returns {Array<{level: 'error'|'warning', rule: string, message: string}>}
|
|
117
|
-
*/
|
|
118
|
-
export function checkPublishedManifest(manifest, runtime) {
|
|
119
|
-
const findings = []
|
|
120
|
-
const error = (rule, message) => findings.push({ level: 'error', rule, message })
|
|
121
|
-
const warning = (rule, message) => findings.push({ level: 'warning', rule, message })
|
|
122
|
-
|
|
123
|
-
const scripts = manifest.scripts ?? {}
|
|
124
|
-
for (const script of LIFECYCLE_SCRIPTS) {
|
|
125
|
-
if (typeof scripts[script] === 'string') warning('M2-lifecycle', lifecycleMessage(script))
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
for (const name of Object.keys(manifest.dependencies ?? {})) {
|
|
129
|
-
if (isIdentityCore(name)) {
|
|
130
|
-
error('M3-core-peer', `${name} 出现在 dependencies;核心运行时必须是 peerDependencies,否则双内核实例会让 Cowork 启动失败`)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const bundle = manifest.dsh?.bundle ?? {}
|
|
135
|
-
if (typeof bundle.patch !== 'string' || bundle.patch === '') {
|
|
136
|
-
error('M4-bundle', 'dsh.bundle.patch 缺失;宿主靠它把插件挂进加载树')
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const engine = typeof manifest.dsh?.engine === 'string' ? manifest.dsh.engine : undefined
|
|
140
|
-
if (engine === undefined) {
|
|
141
|
-
warning('M6-engine', 'dsh.engine 未声明(目标运行时 SemVer 范围);'
|
|
142
|
-
+ '上游目前不强制读取该字段,但声明之后体检才能判断你与目标运行时是否相容')
|
|
143
|
-
} else {
|
|
144
|
-
const satisfied = versionSatisfies(runtime, engine)
|
|
145
|
-
if (satisfied === null) error('M6-engine', `dsh.engine 不是可识别的 SemVer 范围: ${engine}`)
|
|
146
|
-
else if (!satisfied) error('M6-engine', `dsh.engine "${engine}" 不覆盖当前运行时 ${runtime}`)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
for (const [name, range] of Object.entries(manifest.peerDependencies ?? {})) {
|
|
150
|
-
if (!name.startsWith('@deepseek-ai/dsh')) continue
|
|
151
|
-
const satisfied = versionSatisfies(runtime, String(range))
|
|
152
|
-
if (satisfied === null) error('M7-peer-range', `peer ${name} 的范围不可识别: ${range}`)
|
|
153
|
-
else if (!satisfied) warning('M7-peer-range', `peer ${name}@"${range}" 不含当前运行时 ${runtime};宿主升级后可能运行时断裂`)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (manifest.engines?.node === undefined) warning('M8-node-engines', 'engines.node 未声明;宿主运行在 Node 22+')
|
|
157
|
-
if (manifest.license === undefined) warning('M9-license', 'license 未声明')
|
|
158
|
-
|
|
159
|
-
const parsed = parseVersion(typeof manifest.version === 'string' ? manifest.version : '')
|
|
160
|
-
if (parsed !== null && parsed.prerelease.length > 0) {
|
|
161
|
-
warning('M1-manifest', `version ${manifest.version} 是预发布版;`
|
|
162
|
-
+ `市场受控安装只认稳定的精确版(上游 stableExactVersion),npm latest 停在预发布版时`
|
|
163
|
-
+ `用户端只会看到手动安装命令。预发布请发在 next 之类的 dist-tag 上`)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return findings
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 已发布包的完整清单侧体检:清单规则 + 包内补丁行检查。
|
|
171
|
-
* 取不到 tarball(离线、超时、体积过大)时降级为 warning 并如实
|
|
172
|
-
* 标注未检查,绝不因网络问题把插件判成不合格。
|
|
173
|
-
* @param {object} manifest - registry 版本文档(需含 dist.tarball)。
|
|
174
|
-
* @param {string} runtime - 目标 DSH 运行时版本。
|
|
175
|
-
* @param {{ maxBytes?: number, timeoutMs?: number }} [options]
|
|
176
|
-
* @returns {Promise<{ findings: object[], inspectedPatch: boolean }>}
|
|
177
|
-
*/
|
|
178
|
-
export async function checkPublishedPackage(manifest, runtime, options = {}) {
|
|
179
|
-
const findings = checkPublishedManifest(manifest, runtime)
|
|
180
|
-
const patch = typeof manifest.dsh?.bundle?.patch === 'string' ? manifest.dsh.bundle.patch : ''
|
|
181
|
-
const tarball = typeof manifest.dist?.tarball === 'string' ? manifest.dist.tarball : ''
|
|
182
|
-
// 补丁未声明时 M4 已在清单阶段报错,无需再取包。
|
|
183
|
-
if (patch === '' || tarball === '') return { findings, inspectedPatch: false }
|
|
184
|
-
|
|
185
|
-
const relative = patch.replace(/^\.?\//u, '')
|
|
186
|
-
const result = await fetchTarEntry(tarball, entry => entry === relative, options)
|
|
187
|
-
if (result.error !== undefined) {
|
|
188
|
-
findings.push({
|
|
189
|
-
level: 'warning',
|
|
190
|
-
rule: 'M4-bundle',
|
|
191
|
-
message: `未能读取包内 ${relative}(${result.error});本次跳过补丁行检查,完整体检请走 CLI`,
|
|
192
|
-
})
|
|
193
|
-
return { findings, inspectedPatch: false }
|
|
194
|
-
}
|
|
195
|
-
if (result.content === undefined) {
|
|
196
|
-
findings.push({
|
|
197
|
-
level: 'error',
|
|
198
|
-
rule: 'M4-bundle',
|
|
199
|
-
message: `dsh.bundle.patch 指向的文件不在发布内容里: ${patch};宿主取不到补丁就挂不上插件(检查 files 白名单)`,
|
|
200
|
-
})
|
|
201
|
-
return { findings, inspectedPatch: true }
|
|
202
|
-
}
|
|
203
|
-
const packageName = typeof manifest.name === 'string' ? manifest.name : ''
|
|
204
|
-
findings.push(...checkPatchContent(packageName, result.content).findings)
|
|
205
|
-
return { findings, inspectedPatch: true }
|
|
206
|
-
}
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* 已发布包体检(注册表版)
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 对 npm registry 的版本文档跑清单规则,与目录体检(manifest-check)
|
|
5
|
+
* **同一份实现** —— 不是"同一套语义"而已:清单侧规则全部来自
|
|
6
|
+
* checkManifestFields,两条路径不可能再对同一个包给出不同结论。
|
|
7
|
+
*
|
|
8
|
+
* 这里曾经自带一套手写的迷你 SemVer,理由是"会话内要零依赖";
|
|
9
|
+
* 那个前提本来就不成立(semver 是本包的 dependencies,宿主进程里
|
|
10
|
+
* 早就通过 cowork-plugin → manifest-check 加载了),代价却是把
|
|
11
|
+
* ">=0.1 <0.2"、"1.x"、"1.2.0 - 2.0.0" 这些完全合法的 npm 范围判成
|
|
12
|
+
* "不可识别" → error,合格插件被判不合格。已删除。
|
|
13
|
+
*
|
|
14
|
+
* 两个入口:
|
|
15
|
+
* - checkPublishedManifest 只看清单字段;
|
|
16
|
+
* - checkPublishedPackage 额外取 tarball 读出 cordis.patch.yml,
|
|
17
|
+
* 补上 M4/M5/M11 —— 只看清单会漏掉"补丁行 name 写成插件名"
|
|
18
|
+
* 这类装上即让整棵插件树崩溃的缺陷。
|
|
19
|
+
* 冒烟阶段无法在宿主进程执行,完整体检走 CLI 或 Plugin Check
|
|
20
|
+
* workflow。
|
|
21
|
+
* ============================================================ */
|
|
22
|
+
import { checkPatchContent } from './patch-rows.mjs'
|
|
23
|
+
import { fetchTarEntry } from './tarball.mjs'
|
|
24
|
+
import { checkManifestFields } from './manifest-check.mjs'
|
|
25
|
+
|
|
26
|
+
export { versionSatisfies } from './semver-rules.mjs'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 对一份 npm 版本清单跑体检规则。
|
|
30
|
+
* @param {object} manifest - registry 版本文档(scripts/dependencies/
|
|
31
|
+
* peerDependencies/dsh/engines/license/version)。
|
|
32
|
+
* @param {string} [runtime] - 目标 DSH 运行时版本;省略时只判范围合法性,
|
|
33
|
+
* 不判是否覆盖(以前这是必填位置参数且无 guard,传 undefined 会产生
|
|
34
|
+
* 一批凭空的 M6/M7 error)。
|
|
35
|
+
* @returns {Array<{level: 'error'|'warning', rule: string, message: string}>}
|
|
36
|
+
*/
|
|
37
|
+
export function checkPublishedManifest(manifest, runtime) {
|
|
38
|
+
return checkManifestFields(manifest, runtime)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 已发布包的完整清单侧体检:清单规则 + 包内补丁行检查。
|
|
43
|
+
* 取不到 tarball(离线、超时、体积过大)时降级为 warning 并如实
|
|
44
|
+
* 标注未检查,绝不因网络问题把插件判成不合格。
|
|
45
|
+
* @param {object} manifest - registry 版本文档(需含 dist.tarball)。
|
|
46
|
+
* @param {string} [runtime] - 目标 DSH 运行时版本。
|
|
47
|
+
* @param {{ maxBytes?: number, timeoutMs?: number }} [options]
|
|
48
|
+
* @returns {Promise<{ findings: object[], inspectedPatch: boolean }>}
|
|
49
|
+
*/
|
|
50
|
+
export async function checkPublishedPackage(manifest, runtime, options = {}) {
|
|
51
|
+
const findings = checkPublishedManifest(manifest, runtime)
|
|
52
|
+
const patch = typeof manifest.dsh?.bundle?.patch === 'string' ? manifest.dsh.bundle.patch : ''
|
|
53
|
+
const tarball = typeof manifest.dist?.tarball === 'string' ? manifest.dist.tarball : ''
|
|
54
|
+
// 补丁未声明时 M4 已在清单阶段报错,无需再取包。
|
|
55
|
+
if (patch === '' || tarball === '') return { findings, inspectedPatch: false }
|
|
56
|
+
|
|
57
|
+
const relative = patch.replace(/^\.?\//u, '')
|
|
58
|
+
const result = await fetchTarEntry(tarball, entry => entry === relative, options)
|
|
59
|
+
if (result.error !== undefined) {
|
|
60
|
+
findings.push({
|
|
61
|
+
level: 'warning',
|
|
62
|
+
rule: 'M4-bundle',
|
|
63
|
+
message: `未能读取包内 ${relative}(${result.error});本次跳过补丁行检查,完整体检请走 CLI`,
|
|
64
|
+
})
|
|
65
|
+
return { findings, inspectedPatch: false }
|
|
66
|
+
}
|
|
67
|
+
if (result.content === undefined) {
|
|
68
|
+
findings.push({
|
|
69
|
+
level: 'error',
|
|
70
|
+
rule: 'M4-bundle',
|
|
71
|
+
message: `dsh.bundle.patch 指向的文件不在发布内容里: ${patch};宿主取不到补丁就挂不上插件(检查 files 白名单)`,
|
|
72
|
+
})
|
|
73
|
+
return { findings, inspectedPatch: true }
|
|
74
|
+
}
|
|
75
|
+
const packageName = typeof manifest.name === 'string' ? manifest.name : ''
|
|
76
|
+
findings.push(...checkPatchContent(packageName, result.content).findings)
|
|
77
|
+
return { findings, inspectedPatch: true }
|
|
78
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
* 版本相关规则(M1 / M6 / M7)的唯一实现
|
|
3
|
+
* ============================================================
|
|
4
|
+
* 这三条规则以前在两处各写一遍:CLI 走 manifest-check.mjs(用真
|
|
5
|
+
* semver),会话内 package= 走 registry-check.mjs(用一套手写的迷你
|
|
6
|
+
* SemVer)。迷你版只认完整三段版本号,把 ">=0.1 <0.2"、"1.x"、
|
|
7
|
+
* "1.2.0 - 2.0.0" 这些完全合法的 npm 范围判成"不可识别"→ error,
|
|
8
|
+
* 于是同一个包在两条路径上会拿到相反的结论,而且合格插件被判不合格。
|
|
9
|
+
*
|
|
10
|
+
* 现在统一到真 semver,并且只此一份:两条路径调同一个函数,再想漂
|
|
11
|
+
* 也漂不了(tests/cross-path.test.mjs 是这条约束的闸门)。
|
|
12
|
+
*
|
|
13
|
+
* 判定口径:一律带 includePrerelease —— 目标运行时本身就长期是
|
|
14
|
+
* 0.1.3-alpha.1 这种预发布版,不带这个选项 semver 会把它排除在
|
|
15
|
+
* 任何范围之外,得出"谁都不兼容"的荒谬结论。
|
|
16
|
+
* ============================================================ */
|
|
17
|
+
import semver from 'semver'
|
|
18
|
+
import { isIdentityCore, isRuntimeVersioned } from './contract.mjs'
|
|
19
|
+
|
|
20
|
+
const OPTIONS = { includePrerelease: true }
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 版本是否落在范围内。
|
|
24
|
+
* @param {string} version - 具体版本。
|
|
25
|
+
* @param {string} range - SemVer 范围。
|
|
26
|
+
* @returns {boolean | null} 范围不合法时返回 null(保持既有对外契约)。
|
|
27
|
+
*/
|
|
28
|
+
export function versionSatisfies(version, range) {
|
|
29
|
+
if (semver.valid(version) === null) return null
|
|
30
|
+
if (semver.validRange(range, OPTIONS) === null) return null
|
|
31
|
+
return semver.satisfies(version, range, OPTIONS)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* M1:name 与 version 的基本合法性。
|
|
36
|
+
* @param {object} manifest - package.json / registry 版本文档。
|
|
37
|
+
* @returns {Array<{level: string, rule: string, message: string}>}
|
|
38
|
+
*/
|
|
39
|
+
export function checkIdentity(manifest) {
|
|
40
|
+
const findings = []
|
|
41
|
+
if (typeof manifest.name !== 'string' || manifest.name === '') {
|
|
42
|
+
findings.push({ level: 'error', rule: 'M1-manifest', message: 'name 缺失' })
|
|
43
|
+
}
|
|
44
|
+
if (typeof manifest.version !== 'string' || semver.valid(manifest.version) === null) {
|
|
45
|
+
findings.push({ level: 'error', rule: 'M1-manifest', message: 'version 不是合法 SemVer' })
|
|
46
|
+
} else if (semver.prerelease(manifest.version) !== null) {
|
|
47
|
+
findings.push({
|
|
48
|
+
level: 'warning',
|
|
49
|
+
rule: 'M1-manifest',
|
|
50
|
+
message: `version ${manifest.version} 是预发布版;`
|
|
51
|
+
+ `市场受控安装只认稳定的精确版(上游 stableExactVersion),npm latest 若停在预发布版,`
|
|
52
|
+
+ `用户端拿不到一键安装按钮,只会看到手动命令。预发布请发在 next 之类的 dist-tag 上`,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
return findings
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* M6:dsh.engine 声明的目标运行时范围。
|
|
60
|
+
* @param {object} manifest
|
|
61
|
+
* @param {string | undefined} runtime - 目标 DSH 运行时;未给时只判范围合法性。
|
|
62
|
+
* @returns {Array<{level: string, rule: string, message: string}>}
|
|
63
|
+
*/
|
|
64
|
+
export function checkEngine(manifest, runtime) {
|
|
65
|
+
const engine = typeof manifest.dsh?.engine === 'string' ? manifest.dsh.engine : undefined
|
|
66
|
+
if (engine === undefined) {
|
|
67
|
+
return [{
|
|
68
|
+
level: 'warning',
|
|
69
|
+
rule: 'M6-engine',
|
|
70
|
+
message: 'dsh.engine 未声明(目标 DSH 运行时 SemVer 范围,如 ">=0.1.3-alpha.1 <0.2");'
|
|
71
|
+
+ '上游目前不强制读取该字段,但声明之后体检才能判断你与目标运行时是否相容',
|
|
72
|
+
}]
|
|
73
|
+
}
|
|
74
|
+
if (semver.validRange(engine, OPTIONS) === null) {
|
|
75
|
+
return [{ level: 'error', rule: 'M6-engine', message: `dsh.engine 不是合法 SemVer 范围: ${engine}` }]
|
|
76
|
+
}
|
|
77
|
+
if (runtime !== undefined && runtime !== '' && !semver.satisfies(runtime, engine, OPTIONS)) {
|
|
78
|
+
return [{ level: 'error', rule: 'M6-engine', message: `dsh.engine "${engine}" 不覆盖目标运行时 ${runtime}` }]
|
|
79
|
+
}
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* M7:核心 peer 的范围。
|
|
85
|
+
*
|
|
86
|
+
* 两层判定的口径**故意不同**:
|
|
87
|
+
* - 范围合法性:全部 isIdentityCore 的包。以前只看 @deepseek-ai/dsh
|
|
88
|
+
* 前缀,@deepseek-ai/cordis 的范围写坏了完全不报,偏偏它正是 S2 冒烟
|
|
89
|
+
* 真正要装的那个包,范围写坏下一步必然装不出来。
|
|
90
|
+
* - 是否含目标运行时:只对 isRuntimeVersioned 的包。cordis 自成 4.x
|
|
91
|
+
* 版本线,拿它跟运行时 0.1.3-alpha.1 比是范畴错误(详见 contract.mjs)。
|
|
92
|
+
* @param {object} manifest
|
|
93
|
+
* @param {string | undefined} runtime
|
|
94
|
+
* @returns {Array<{level: string, rule: string, message: string}>}
|
|
95
|
+
*/
|
|
96
|
+
export function checkPeerRanges(manifest, runtime) {
|
|
97
|
+
const findings = []
|
|
98
|
+
for (const [name, range] of Object.entries(manifest.peerDependencies ?? {})) {
|
|
99
|
+
if (!isIdentityCore(name)) continue
|
|
100
|
+
if (semver.validRange(String(range), OPTIONS) === null) {
|
|
101
|
+
findings.push({ level: 'error', rule: 'M7-peer-range', message: `peer ${name} 的范围不合法: ${range}` })
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (!isRuntimeVersioned(name)) continue
|
|
105
|
+
if (runtime === undefined || runtime === '') continue
|
|
106
|
+
if (!semver.satisfies(runtime, String(range), OPTIONS)) {
|
|
107
|
+
findings.push({
|
|
108
|
+
level: 'warning',
|
|
109
|
+
rule: 'M7-peer-range',
|
|
110
|
+
message: `peer ${name}@"${range}" 不含目标运行时 ${runtime};宿主升级后此插件可能在运行时断裂(接口错配类故障)`,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return findings
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 三条一起跑,供两条执行路径直接复用。
|
|
119
|
+
* @param {object} manifest
|
|
120
|
+
* @param {string | undefined} runtime
|
|
121
|
+
* @returns {Array<{level: string, rule: string, message: string}>}
|
|
122
|
+
*/
|
|
123
|
+
export function checkVersionRules(manifest, runtime) {
|
|
124
|
+
return [
|
|
125
|
+
...checkIdentity(manifest),
|
|
126
|
+
...checkEngine(manifest, runtime),
|
|
127
|
+
...checkPeerRanges(manifest, runtime),
|
|
128
|
+
]
|
|
129
|
+
}
|
package/lib/smoke-check.mjs
CHANGED
|
@@ -193,7 +193,19 @@ export function checkSmoke(pluginDir, manifest, rows, options = {}) {
|
|
|
193
193
|
error('S4-apply', `冒烟子进程异常: ${reason}`)
|
|
194
194
|
return { findings, workspace: options.keepWorkspace ? workspace : undefined }
|
|
195
195
|
}
|
|
196
|
-
|
|
196
|
+
// 子进程写出的结果文件理论上总是合法 JSON,但它可能被入口代码
|
|
197
|
+
// 的 stdout 污染、被磁盘写满截断、或者被入口自己覆盖掉。那种时候
|
|
198
|
+
// 崩掉整个 CLI 等于把"这个插件冒烟失败"变成"体检工具坏了",
|
|
199
|
+
// 作者拿不到任何可用结论 —— 按 S4-apply error 如实报出来。
|
|
200
|
+
let results
|
|
201
|
+
try {
|
|
202
|
+
results = JSON.parse(readFileSync(resultPath, 'utf8'))
|
|
203
|
+
if (!Array.isArray(results)) throw new TypeError('结果不是数组')
|
|
204
|
+
} catch (cause) {
|
|
205
|
+
const reason = cause instanceof Error ? cause.message : String(cause)
|
|
206
|
+
error('S4-apply', `冒烟结果文件不可解析: ${reason}(多半是插件入口往 stdout/结果文件里写了东西)`)
|
|
207
|
+
return { findings, workspace: options.keepWorkspace ? workspace : undefined }
|
|
208
|
+
}
|
|
197
209
|
const passed = results.filter(result => result.status === 'ok').length
|
|
198
210
|
findings.push({ level: 'info', rule: 'S4-apply', message: `隔离冒烟: ${passed}/${results.length} 行通过(解析→import→应用)` })
|
|
199
211
|
for (const result of results) {
|
package/lib/tarball.mjs
CHANGED
|
@@ -88,7 +88,11 @@ export async function fetchTarEntry(url, match, options = {}) {
|
|
|
88
88
|
if (declared > maxBytes) return { error: `tarball 超过 ${Math.round(maxBytes / 1024 / 1024)}MB,跳过包内检查` }
|
|
89
89
|
const raw = Buffer.from(await response.arrayBuffer())
|
|
90
90
|
if (raw.length > maxBytes) return { error: `tarball 超过 ${Math.round(maxBytes / 1024 / 1024)}MB,跳过包内检查` }
|
|
91
|
-
|
|
91
|
+
// 上面两道闸只管住了**压缩后**的字节数,解压后的体积不受它约束
|
|
92
|
+
// —— 一个几百 KB 的 gzip 可以解成几 GB。maxOutputLength 让 zlib
|
|
93
|
+
// 在超限时抛错(ERR_BUFFER_TOO_LARGE)而不是把内存吃干。
|
|
94
|
+
const tar = gunzipSync(raw, { maxOutputLength: maxBytes })
|
|
95
|
+
return { content: readTarEntry(tar, match) }
|
|
92
96
|
} catch (cause) {
|
|
93
97
|
const reason = cause instanceof Error ? cause.message : String(cause)
|
|
94
98
|
return { error: cause?.name === 'AbortError' ? '下载 tarball 超时' : `读取 tarball 失败: ${reason}` }
|