dsh-plugin 1.6.6 → 1.6.7
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/client/client.js +291 -291
- package/client/client.js.map +1 -1
- package/lib/http/install.js +3 -3
- package/lib/services/install/package-entry.js +59 -8
- package/lib/services/install/preflight.js +47 -17
- package/lib/services/install/task-queue.js +30 -15
- package/lib/types/services/install/package-entry.d.ts +42 -10
- package/lib/types/services/install/preflight.d.ts +9 -1
- package/lib/types/services/install/task-queue.d.ts +6 -4
- package/package.json +1 -1
- package/src/client/locales.ts +4 -4
- package/src/client/logic/urls.ts +3 -3
- package/src/server/http/install.ts +3 -3
- package/src/server/services/install/package-entry.ts +72 -9
- package/src/server/services/install/preflight.ts +47 -18
- package/src/server/services/install/task-queue.ts +28 -18
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
* 安装前预检:对 GitHub 源的插件,检查其分发(codeload tarball
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* 安装前预检:对 GitHub 源的插件,检查其分发(codeload tarball)里是否真的包含
|
|
7
|
+
* package.json 声明的文件(main / exports["."] / dsh.bundle.patch)。git 分发常不提交
|
|
8
|
+
* 构建产物(lib/ 等),这类残缺包装完会让宿主重启加载插件树时 ERR_MODULE_NOT_FOUND
|
|
9
|
+
* 直接崩溃 —— 预检在动手安装前就把它们拦下,避免白装一次。
|
|
10
10
|
* npm 包信任 registry 的完整性,直接放行;任何无法预检的情况也放行,交给装后
|
|
11
11
|
* 校验(verifyInstalledEntry)兜底,保证预检自身失败不会卡死安装流程。
|
|
12
12
|
*/
|
|
@@ -19,7 +19,7 @@ import { pipeline } from 'node:stream/promises'
|
|
|
19
19
|
import { promisify } from 'node:util'
|
|
20
20
|
import { githubRefOf, githubRepoOf } from '../profile/profile.ts'
|
|
21
21
|
import { githubReleaseTarget } from './release-target.ts'
|
|
22
|
-
import {
|
|
22
|
+
import { requiredDistributionFiles, resolveBundlePatchFiles } from './package-entry.ts'
|
|
23
23
|
|
|
24
24
|
const execFileAsync = promisify(execFile)
|
|
25
25
|
|
|
@@ -27,7 +27,8 @@ const execFileAsync = promisify(execFile)
|
|
|
27
27
|
* 否则 `/install` 请求永远不返回、任务不入队、前端进度卡 0%。超时后放行(交给装后校验)。 */
|
|
28
28
|
const PREFLIGHT_TIMEOUT_MS = 15_000
|
|
29
29
|
|
|
30
|
-
/** 预检结果:ok=false
|
|
30
|
+
/** 预检结果:ok=false 表示分发改 package.json 声明的文件(入口或 bundle 补丁),
|
|
31
|
+
* missing 为该文件在包内的相对路径;
|
|
31
32
|
* name 为 git 分发包 package.json 声明的包名(非 git 目标/无法确定时为 null),
|
|
32
33
|
* 供安装路由做「包名冲突」检测 —— 同名包名已被其他来源占用时,pnpm 装前必然撞车,
|
|
33
34
|
* 需要把晦涩的 CLI 报错转成明确的拦截。 */
|
|
@@ -57,9 +58,17 @@ export async function preflightTarget(target: string): Promise<PreflightResult>
|
|
|
57
58
|
const rootPrefix = rootPackage.slice(0, -'package.json'.length)
|
|
58
59
|
const meta = await readTarJson(tar, rootPackage)
|
|
59
60
|
if (meta === null) return { ok: true, missing: null, name: null }
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// 只校验作者声明过的文件:没声明入口的包(只挂技能的 bundle)没有可缺失的入口,
|
|
62
|
+
// 它的分发是否完整由 dsh.bundle.patch 是否在包里决定;补丁若按本包名 mount 自己,
|
|
63
|
+
// 入口又变成必须的(宿主会 import 本包)—— 所以判入口前先把补丁读出来。
|
|
64
|
+
const patchTexts: string[] = []
|
|
65
|
+
for (const file of resolveBundlePatchFiles(meta)) {
|
|
66
|
+
const text = await readTarText(tar, `${rootPrefix}${file}`)
|
|
67
|
+
if (text !== null) patchTexts.push(text)
|
|
68
|
+
}
|
|
69
|
+
const required = requiredDistributionFiles(meta, patchTexts).map((file) => `${rootPrefix}${file}`)
|
|
70
|
+
const missing = missingDistributionFileOfTarList(await tarListing(tar), required)
|
|
71
|
+
return { ok: missing === null, missing, name: typeof meta.name === 'string' ? meta.name : null }
|
|
63
72
|
} catch {
|
|
64
73
|
return { ok: true, missing: null, name: null }
|
|
65
74
|
} finally {
|
|
@@ -125,31 +134,51 @@ export function rootPackageEntryOfTarList(listing: string): string | null {
|
|
|
125
134
|
return candidates.length === 1 ? candidates[0] : null
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
/**
|
|
138
|
+
* 清单里第一个缺失的必需文件(成员名带包根前缀,由调用方拼好),全部都在则为 null。
|
|
139
|
+
* 清单不可读(tar 缺失/解压失败)时按第一个必需文件缺失处理:与逐文件查询时
|
|
140
|
+
* 「查不到就是缺」的判定一致,宁可拦下一次可疑安装,也不放一个空包过去。
|
|
141
|
+
* Exported for a small parser regression test.
|
|
142
|
+
*/
|
|
143
|
+
export function missingDistributionFileOfTarList(listing: string | null, required: string[]): string | null {
|
|
144
|
+
if (required.length === 0) return null
|
|
145
|
+
if (listing === null) return required[0]
|
|
146
|
+
const present = new Set(listing.split(/\r?\n/))
|
|
147
|
+
return required.find((file) => !present.has(file)) ?? null
|
|
148
|
+
}
|
|
149
|
+
|
|
128
150
|
async function rootPackageEntry(tar: string): Promise<string | null> {
|
|
151
|
+
const listing = await tarListing(tar)
|
|
152
|
+
return listing === null ? null : rootPackageEntryOfTarList(listing)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** tar 成员清单;`tar` 执行失败时返回 null(调用方按无法判定处理)。 */
|
|
156
|
+
async function tarListing(tar: string): Promise<string | null> {
|
|
129
157
|
try {
|
|
130
158
|
const { stdout } = await execFileAsync('tar', ['-tzf', tar], { maxBuffer: 16 * 1024 * 1024 })
|
|
131
|
-
return
|
|
159
|
+
return stdout
|
|
132
160
|
} catch {
|
|
133
161
|
return null
|
|
134
162
|
}
|
|
135
163
|
}
|
|
136
164
|
|
|
137
|
-
/** 从 tarball
|
|
138
|
-
async function
|
|
165
|
+
/** 从 tarball 提取一个文件的内容;文件不存在或提取失败返回 null。 */
|
|
166
|
+
async function readTarText(tar: string, entry: string): Promise<string | null> {
|
|
139
167
|
try {
|
|
140
168
|
const { stdout } = await execFileAsync('tar', ['-xzOf', tar, entry], { maxBuffer: 4 * 1024 * 1024 })
|
|
141
|
-
return
|
|
169
|
+
return stdout
|
|
142
170
|
} catch {
|
|
143
171
|
return null
|
|
144
172
|
}
|
|
145
173
|
}
|
|
146
174
|
|
|
147
|
-
/**
|
|
148
|
-
async function
|
|
175
|
+
/** 从 tarball 提取并解析一个 JSON 文件;文件不存在或解析失败返回 null。 */
|
|
176
|
+
async function readTarJson(tar: string, entry: string): Promise<Record<string, unknown> | null> {
|
|
177
|
+
const text = await readTarText(tar, entry)
|
|
178
|
+
if (text === null) return null
|
|
149
179
|
try {
|
|
150
|
-
|
|
151
|
-
return stdout.split(/\r?\n/).some((line) => line === entry)
|
|
180
|
+
return JSON.parse(text) as Record<string, unknown>
|
|
152
181
|
} catch {
|
|
153
|
-
return
|
|
182
|
+
return null
|
|
154
183
|
}
|
|
155
184
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 输出流式收集到任务上;卸载成功时尝试从运行中 loader 即时停用(见 loader.ts)。
|
|
9
9
|
*/
|
|
10
10
|
import { spawn, spawnSync } from 'node:child_process'
|
|
11
|
-
import {
|
|
11
|
+
import { requiredDistributionFiles, resolveBundlePatchFiles, type PackageManifest } from './package-entry.ts'
|
|
12
12
|
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
13
13
|
import { dirname, join, resolve } from 'node:path'
|
|
14
14
|
import type { InstallResult, InstallTask, Invocation, QueueItem } from './install-types.ts'
|
|
@@ -625,10 +625,12 @@ function spawnMutation(options: {
|
|
|
625
625
|
}
|
|
626
626
|
|
|
627
627
|
/**
|
|
628
|
-
*
|
|
629
|
-
* git: 分发常不提交构建产物(lib/ 等),pnpm
|
|
630
|
-
* ERR_MODULE_NOT_FOUND 直接崩溃、网页打不开 ——
|
|
631
|
-
*
|
|
628
|
+
* 安装后校验已装包里 package.json 声明的文件(main / exports["."].default 与
|
|
629
|
+
* dsh.bundle.patch)是否真实存在。git: 分发常不提交构建产物(lib/ 等),pnpm 装完没有
|
|
630
|
+
* 报错,但宿主重启加载插件树时会 ERR_MODULE_NOT_FOUND 直接崩溃、网页打不开 —— 这里在
|
|
631
|
+
* 登记「待重启」前就把这类残缺包拦下。没有声明过的文件不在这里追究:没声明入口的
|
|
632
|
+
* bundle 包(只挂技能)本来就不需要入口,宿主读的是它的 package.json 与声明的补丁。
|
|
633
|
+
* 返回 { name, missing }:name 为空表示无法定位目标包(跳过校验);missing 为缺失文件的路径。
|
|
632
634
|
*/
|
|
633
635
|
export function verifyInstalledEntry(profile: string, target: string): { name: string | null; missing: string | null } {
|
|
634
636
|
let profilePkg: Record<string, unknown> | null = null
|
|
@@ -645,21 +647,29 @@ export function verifyInstalledEntry(profile: string, target: string): { name: s
|
|
|
645
647
|
: deps[target] !== undefined ? target : Object.keys(deps).find((k) => deps[k] === target)
|
|
646
648
|
if (!name) return { name: null, missing: null }
|
|
647
649
|
const pkgDir = join(profileDirectory(profile), 'node_modules', name)
|
|
648
|
-
let
|
|
650
|
+
let required: string[] = []
|
|
649
651
|
try {
|
|
650
|
-
const meta = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')) as
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
652
|
+
const meta = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')) as PackageManifest
|
|
653
|
+
// 补丁按本包名 mount 自己时入口也是必须的,所以先读已装包里的补丁(读不到就当没有)。
|
|
654
|
+
const patchTexts = resolveBundlePatchFiles(meta).flatMap((file) => {
|
|
655
|
+
try {
|
|
656
|
+
return [readFileSync(join(pkgDir, file), 'utf8')]
|
|
657
|
+
} catch {
|
|
658
|
+
return []
|
|
659
|
+
}
|
|
660
|
+
})
|
|
661
|
+
required = requiredDistributionFiles(meta, patchTexts)
|
|
655
662
|
} catch {
|
|
656
663
|
return { name, missing: null }
|
|
657
664
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
665
|
+
for (const file of required) {
|
|
666
|
+
try {
|
|
667
|
+
if (!statSync(join(pkgDir, file)).isFile()) return { name, missing: file }
|
|
668
|
+
} catch {
|
|
669
|
+
return { name, missing: file }
|
|
670
|
+
}
|
|
662
671
|
}
|
|
672
|
+
return { name, missing: null }
|
|
663
673
|
}
|
|
664
674
|
|
|
665
675
|
/** 从 pnpm `Ignored build scripts:` 提示行解析被拦截的构建脚本 key(name@version,逗号分隔)。 */
|
|
@@ -749,21 +759,21 @@ export async function runPluginMutation(options: {
|
|
|
749
759
|
}
|
|
750
760
|
}
|
|
751
761
|
}
|
|
752
|
-
// pnpm
|
|
762
|
+
// pnpm 安装本身成功,但目标包声明分发的文件缺失(git 分发缺构建产物 / 缺声明的补丁)→ 判为失败:
|
|
753
763
|
// 撤销刚登记的待重启,避免用户重启时宿主加载残缺包直接崩溃。
|
|
754
764
|
// 全局 npm 安装不进 profile(profile deps 里没有这些包),跳过入口校验
|
|
755
765
|
if (options.action !== 'remove' && result.exitCode === 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
|
|
756
766
|
const verified = verifyInstalledEntry(options.profile, options.target)
|
|
757
767
|
if (verified.name !== null && verified.missing !== null) {
|
|
758
768
|
clearPendingRestart(options.displayTarget ?? options.target)
|
|
759
|
-
const detail = `[packaging] ${verified.name}:
|
|
769
|
+
const detail = `[packaging] ${verified.name}: declared file missing: ${verified.missing} (git distribution lacks build output — install the npm version or report to the author)`
|
|
760
770
|
if (options.task && options.task.status !== 'cancelled') {
|
|
761
771
|
options.task.status = 'failed'
|
|
762
772
|
options.task.exitCode = null
|
|
763
773
|
options.task.progress = 0
|
|
764
774
|
pushLine(options.task, detail)
|
|
765
775
|
}
|
|
766
|
-
return { exitCode: 1, timedOut: false, error: `
|
|
776
|
+
return { exitCode: 1, timedOut: false, error: `declared file missing: ${verified.missing}`, stdout: result.stdout, stderr: `${result.stderr}\n${detail}` }
|
|
767
777
|
}
|
|
768
778
|
}
|
|
769
779
|
// npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
|