dsh-plugin-pack 1.0.18 → 1.0.19
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/import.js +22 -0
- package/lib/lockfile.js +127 -0
- package/lib/repair.js +9 -1
- package/lib/server.js +38 -0
- package/package.json +1 -1
package/lib/import.js
CHANGED
|
@@ -18,6 +18,7 @@ import { readJson, readText, writeJson, writeText, ensureDir, isDir, isFile, rea
|
|
|
18
18
|
import { existingIds, rowId, appendInsertBlock, splitTopLevel, yamlScalar } from './patch-text.js';
|
|
19
19
|
import { isAbsoluteSpec, clearScanCache, resolvePackageDir } from './scan.js';
|
|
20
20
|
import { repairProfile } from './repair.js';
|
|
21
|
+
import { syncLockfile } from './lockfile.js';
|
|
21
22
|
|
|
22
23
|
export const PACK_FORMAT = 'dsh-plugin-pack@2';
|
|
23
24
|
/** 兼容旧的散文件布局。 */
|
|
@@ -543,6 +544,25 @@ export function importBundle(options = {}) {
|
|
|
543
544
|
|
|
544
545
|
record.links = links;
|
|
545
546
|
writeJson(path.join(backupRoot, 'record.json'), record);
|
|
547
|
+
|
|
548
|
+
/* 最后一步:让 pnpm-lock.yaml 跟上 package.json。
|
|
549
|
+
*
|
|
550
|
+
* 我们改了 dependencies,但锁文件不会自己更新。dsh 安装/卸载插件时会跑
|
|
551
|
+
* `pnpm install --frozen-lockfile`,两边对不上就抛
|
|
552
|
+
* ERR_PNPM_OUTDATED_LOCKFILE —— 用户看到的就是"卸载点了没反应"。
|
|
553
|
+
* 这一步只更新锁文件,不碰 node_modules;失败不影响导入本身。 */
|
|
554
|
+
let lockfile = { ok: false, skipped: true, reason: '没有修改依赖' };
|
|
555
|
+
if (Object.keys(record.pkgDepsBefore ?? {}).length) {
|
|
556
|
+
onProgress('同步 pnpm 锁文件', 98);
|
|
557
|
+
lockfile = syncLockfile(profile);
|
|
558
|
+
if (!lockfile.ok && !lockfile.skipped) {
|
|
559
|
+
warnings.push(
|
|
560
|
+
`锁文件同步失败:${lockfile.error ?? '未知原因'}。` +
|
|
561
|
+
`这会让 dsh 的插件卸载/安装报 ERR_PNPM_OUTDATED_LOCKFILE,` +
|
|
562
|
+
`可在 profile 目录手动跑 pnpm install --lockfile-only 修好。`,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
546
566
|
// 导入改了 profile 配置和插件目录,扫描缓存必须失效
|
|
547
567
|
clearScanCache();
|
|
548
568
|
onProgress(`完成:${applied.length} 个插件已处理`, 100);
|
|
@@ -564,6 +584,8 @@ export function importBundle(options = {}) {
|
|
|
564
584
|
disabledNames: enable ? [] : applied.filter((item) => item.action !== 'skipped').map((item) => item.name),
|
|
565
585
|
patch: patchPlan.summary,
|
|
566
586
|
dataRestored: record.dataDirs.length,
|
|
587
|
+
// 锁文件同步结果:ok=false 且没 skipped 时要提醒手动跑 pnpm install
|
|
588
|
+
lockfile,
|
|
567
589
|
warnings,
|
|
568
590
|
restartRequired: true,
|
|
569
591
|
};
|
package/lib/lockfile.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 同步 pnpm 锁文件。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须有这一步:我们改的是 `package.json` 的 dependencies,
|
|
5
|
+
* 但 `pnpm-lock.yaml` 不会自己更新。而 dsh 的插件卸载/安装会跑
|
|
6
|
+
*
|
|
7
|
+
* pnpm install --frozen-lockfile
|
|
8
|
+
*
|
|
9
|
+
* 锁文件一旦和 package.json 对不上,pnpm 直接抛
|
|
10
|
+
*
|
|
11
|
+
* ERR_PNPM_OUTDATED_LOCKFILE
|
|
12
|
+
* specifiers in the lockfile don't match specifiers in package.json
|
|
13
|
+
*
|
|
14
|
+
* 表现就是:卸载按钮点了没反应、装新插件也失败。
|
|
15
|
+
*
|
|
16
|
+
* 所以导入/删除依赖之后,必须补一次 `pnpm install --lockfile-only`:
|
|
17
|
+
* 只更新锁文件,不碰 node_modules,几秒就完。
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-plugin-pack/lib/lockfile
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
/** 找 pnpm 可执行文件;找不到返回 null。 */
|
|
26
|
+
function findPnpm() {
|
|
27
|
+
const candidates =
|
|
28
|
+
process.platform === 'win32'
|
|
29
|
+
? ['pnpm.cmd', 'pnpm.exe', 'pnpm']
|
|
30
|
+
: ['pnpm'];
|
|
31
|
+
|
|
32
|
+
for (const name of candidates) {
|
|
33
|
+
const result = spawnSync(name, ['--version'], {
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
shell: process.platform === 'win32',
|
|
36
|
+
timeout: 15000,
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
});
|
|
39
|
+
if (!result.error && result.status === 0) return name;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 让 `pnpm-lock.yaml` 与 `package.json` 对齐。
|
|
46
|
+
*
|
|
47
|
+
* 没有 package.json / 没有 pnpm / 没有 lockfile 时都不算错,直接跳过 ——
|
|
48
|
+
* 这一步是"顺手修好别再踩坑",不该阻断导入本身。
|
|
49
|
+
*
|
|
50
|
+
* @param {string} profileDir
|
|
51
|
+
* @param {object} [options]
|
|
52
|
+
* @param {number} [options.timeoutMs]
|
|
53
|
+
* @param {boolean} [options.force] 即使没有 lockfile 也生成一份
|
|
54
|
+
* @returns {{ ok: boolean, skipped?: boolean, reason?: string, error?: string }}
|
|
55
|
+
*/
|
|
56
|
+
export function syncLockfile(profileDir, options = {}) {
|
|
57
|
+
const profile = path.resolve(profileDir);
|
|
58
|
+
const pkgFile = path.join(profile, 'package.json');
|
|
59
|
+
const lockFile = path.join(profile, 'pnpm-lock.yaml');
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (!fs.existsSync(pkgFile)) return { ok: false, skipped: true, reason: '没有 package.json' };
|
|
63
|
+
// 本来就没有锁文件就别硬造一个(可能是 npm/yarn 项目)
|
|
64
|
+
if (!fs.existsSync(lockFile) && !options.force) {
|
|
65
|
+
return { ok: false, skipped: true, reason: '没有 pnpm-lock.yaml,跳过' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const pnpm = findPnpm();
|
|
69
|
+
if (!pnpm) return { ok: false, skipped: true, reason: '没找到 pnpm' };
|
|
70
|
+
|
|
71
|
+
const result = spawnSync(pnpm, ['install', '--lockfile-only', '--reporter=silent'], {
|
|
72
|
+
cwd: profile,
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
shell: process.platform === 'win32',
|
|
75
|
+
timeout: options.timeoutMs ?? 120000,
|
|
76
|
+
windowsHide: true,
|
|
77
|
+
env: { ...process.env, CI: '' },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (result.error) {
|
|
81
|
+
return { ok: false, error: String(result.error.message ?? result.error) };
|
|
82
|
+
}
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
const detail = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
85
|
+
return { ok: false, error: detail.slice(0, 400) || `pnpm 退出码 ${result.status}` };
|
|
86
|
+
}
|
|
87
|
+
return { ok: true };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { ok: false, error: String(error?.message ?? error) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 检查锁文件与 package.json 是否一致(不写盘)。
|
|
95
|
+
*
|
|
96
|
+
* @param {string} profileDir
|
|
97
|
+
* @returns {{ ok: boolean, skipped?: boolean, outdated?: boolean, error?: string }}
|
|
98
|
+
*/
|
|
99
|
+
export function checkLockfile(profileDir) {
|
|
100
|
+
const profile = path.resolve(profileDir);
|
|
101
|
+
const pkgFile = path.join(profile, 'package.json');
|
|
102
|
+
const lockFile = path.join(profile, 'pnpm-lock.yaml');
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
if (!fs.existsSync(pkgFile) || !fs.existsSync(lockFile)) {
|
|
106
|
+
return { ok: false, skipped: true };
|
|
107
|
+
}
|
|
108
|
+
const pnpm = findPnpm();
|
|
109
|
+
if (!pnpm) return { ok: false, skipped: true, reason: '没找到 pnpm' };
|
|
110
|
+
|
|
111
|
+
const result = spawnSync(pnpm, ['install', '--frozen-lockfile', '--lockfile-only'], {
|
|
112
|
+
cwd: profile,
|
|
113
|
+
encoding: 'utf8',
|
|
114
|
+
shell: process.platform === 'win32',
|
|
115
|
+
timeout: 120000,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
});
|
|
118
|
+
// 注意:这里不能用 --reporter=silent —— 它会把 ERR_PNPM_OUTDATED_LOCKFILE
|
|
119
|
+
// 这段错误文本一起吞掉,导致分不清"锁文件不一致"和"其它失败"。
|
|
120
|
+
const detail = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
121
|
+
if (/ERR_PNPM_OUTDATED_LOCKFILE/.test(detail)) return { ok: true, outdated: true };
|
|
122
|
+
if (result.status === 0) return { ok: true, outdated: false };
|
|
123
|
+
return { ok: false, error: detail.trim().slice(0, 400) || `pnpm 退出码 ${result.status}` };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return { ok: false, error: String(error?.message ?? error) };
|
|
126
|
+
}
|
|
127
|
+
}
|
package/lib/repair.js
CHANGED
|
@@ -21,6 +21,7 @@ import path from 'node:path';
|
|
|
21
21
|
|
|
22
22
|
import { readJson, writeJson, ensureDir, isDir } from './util.js';
|
|
23
23
|
import { clearScanCache } from './scan.js';
|
|
24
|
+
import { syncLockfile } from './lockfile.js';
|
|
24
25
|
|
|
25
26
|
/** 这个包是不是宿主自带(从 dsh 安装目录解析)。 */
|
|
26
27
|
function isOfficial(name) {
|
|
@@ -142,5 +143,12 @@ export function repairProfile({ profileDir, dryRun = false }) {
|
|
|
142
143
|
clearScanCache();
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
|
|
146
|
+
/* 删掉依赖之后锁文件也要跟上,否则 dsh 卸载/安装插件时会因为
|
|
147
|
+
* ERR_PNPM_OUTDATED_LOCKFILE 失败。 */
|
|
148
|
+
let lockfile = null;
|
|
149
|
+
if (!dryRun && removed.length) {
|
|
150
|
+
lockfile = syncLockfile(profile);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { ok: true, linked, removed, unlinked: unlinked, warnings, lockfile };
|
|
146
154
|
}
|
package/lib/server.js
CHANGED
|
@@ -41,6 +41,7 @@ import { runJob, listJobs, getJob, runningJobs, clearFinished, openWaiting, getW
|
|
|
41
41
|
import { fetchBundleFrom, parseSource } from './fetch-bundle.js';
|
|
42
42
|
import { setPluginsEnabled, getEnabledState } from './enable.js';
|
|
43
43
|
import { repairProfile } from './repair.js';
|
|
44
|
+
import { syncLockfile, checkLockfile } from './lockfile.js';
|
|
44
45
|
import { dshHome as resolveDshHome, profileDir as resolveProfileDir, profileName as resolveProfileName, humanBytes } from './home.js';
|
|
45
46
|
import { readJson, writeJson, isDir, isFile } from './util.js';
|
|
46
47
|
|
|
@@ -767,6 +768,43 @@ export function mountRoutes(ctx, config = {}) {
|
|
|
767
768
|
'plugin-pack: repair',
|
|
768
769
|
);
|
|
769
770
|
|
|
771
|
+
/* pnpm 锁文件:dsh 卸载/安装插件时会用 --frozen-lockfile 校验,
|
|
772
|
+
* 对不上就报 ERR_PNPM_OUTDATED_LOCKFILE(表现是"卸载点了没反应")。
|
|
773
|
+
* GET /plugin-pack/lockfile 查是否一致
|
|
774
|
+
* POST /plugin-pack/lockfile/sync 同步(= pnpm install --lockfile-only)
|
|
775
|
+
*/
|
|
776
|
+
register(
|
|
777
|
+
{
|
|
778
|
+
kind: 'exact',
|
|
779
|
+
path: `${PREFIX}/lockfile`,
|
|
780
|
+
handler: async (request, response) => {
|
|
781
|
+
if (request.method !== 'GET') {
|
|
782
|
+
sendJson(response, 405, { ok: false, error: '只支持 GET' });
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const local = contextOf(config);
|
|
786
|
+
sendJson(response, 200, checkLockfile(local.profileDir));
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
'plugin-pack: lockfile',
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
register(
|
|
793
|
+
{
|
|
794
|
+
kind: 'exact',
|
|
795
|
+
path: `${PREFIX}/lockfile/sync`,
|
|
796
|
+
handler: async (request, response) => {
|
|
797
|
+
if (request.method !== 'POST') {
|
|
798
|
+
sendJson(response, 405, { ok: false, error: '只支持 POST' });
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const local = contextOf(config);
|
|
802
|
+
sendJson(response, 200, syncLockfile(local.profileDir));
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
'plugin-pack: lockfile-sync',
|
|
806
|
+
);
|
|
807
|
+
|
|
770
808
|
/* 待决策任务:导入遇到重名时登记,界面切走再回来靠它捡回弹窗。
|
|
771
809
|
* GET /plugin-pack/waiting 列出待决策任务(含冲突列表)
|
|
772
810
|
* POST /plugin-pack/waiting/cancel 取消(用户切走/关页面/点取消)
|
package/package.json
CHANGED