dsh-plugin 2.2.0 → 2.2.2
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 +515 -362
- package/client/client.js.map +1 -1
- package/lib/services/install/operation-log.js +1 -1
- package/lib/services/install/registry-fallback.js +5 -0
- package/lib/services/install/task-queue.js +40 -3
- package/lib/services/npm-release-age.js +10 -6
- package/lib/services/profile/installed-versions.js +32 -4
- package/lib/types/services/install/install-types.d.ts +5 -0
- package/lib/types/services/install/registry-fallback.d.ts +2 -0
- package/lib/types/services/install/task-queue.d.ts +5 -0
- package/lib/types/services/npm-release-age.d.ts +1 -1
- package/lib/types/services/profile/installed-versions.d.ts +3 -0
- package/package.json +1 -1
- package/src/client/components/modals/NotificationsModal.tsx +29 -4
- package/src/client/components/modals/modals.tsx +28 -4
- package/src/client/hooks/useCatalog.ts +45 -25
- package/src/client/hooks/useTaskQueue.ts +30 -8
- package/src/client/locales.ts +32 -8
- package/src/client/logic/failures.ts +34 -8
- package/src/client/logic/installed.ts +12 -5
- package/src/client/logic/release-age.ts +53 -5
- package/src/client/logic/urls.ts +11 -3
- package/src/server/services/install/install-types.ts +5 -0
- package/src/server/services/install/operation-log.ts +1 -1
- package/src/server/services/install/registry-fallback.ts +5 -0
- package/src/server/services/install/task-queue.ts +37 -3
- package/src/server/services/npm-release-age.ts +10 -6
- package/src/server/services/profile/installed-versions.ts +30 -4
|
@@ -657,6 +657,11 @@ function spawnMutation(options: {
|
|
|
657
657
|
task.status = result.timedOut || result.exitCode !== 0 ? 'failed' : 'done'
|
|
658
658
|
if (task.status === 'done') {
|
|
659
659
|
task.progress = 100
|
|
660
|
+
// 实际装上的版本:目录版本可能滞后,客户端账本必须记这个值,否则「有更新」永久失真
|
|
661
|
+
if (!isGlobalNpm && action !== 'remove') {
|
|
662
|
+
const installed = installedVersionOf(profile, target)
|
|
663
|
+
if (installed !== null) task.installedVersion = installed
|
|
664
|
+
}
|
|
660
665
|
// 市场自更新:若 bundle 已被取消勾选,重启后市场不会出现,结果页要明确提示
|
|
661
666
|
if ((action === 'add' || action === 'update') && (target === 'dsh-plugin' || target.startsWith('dsh-plugin@'))
|
|
662
667
|
&& !bundleIncludes(profile, 'dsh-plugin')) {
|
|
@@ -699,10 +704,38 @@ function spawnMutation(options: {
|
|
|
699
704
|
pushLine(task, result.timedOut ? '[timed out]' : `[exit ${result.exitCode ?? '?'}]${printedNothing ? ' (no output captured)' : ''}`)
|
|
700
705
|
}
|
|
701
706
|
}
|
|
702
|
-
|
|
707
|
+
// 带上最终生效的源:后续注解(如 [dsh-too-old])读元数据必须与安装同源,
|
|
708
|
+
// 否则换过源之后会去另一个源查,查不到就静默漏报
|
|
709
|
+
return activeRegistry === undefined ? result : { ...result, registry: activeRegistry }
|
|
703
710
|
})()
|
|
704
711
|
}
|
|
705
712
|
|
|
713
|
+
/**
|
|
714
|
+
* 安装成功后从 profile 的 `node_modules/<pkg>/package.json` 读**实际**版本;
|
|
715
|
+
* 定位不到包名或读不到返回 null。客户端拿它写版本账本 —— 目录里的版本可能滞后。
|
|
716
|
+
*/
|
|
717
|
+
export function installedVersionOf(profile: string, target: string): string | null {
|
|
718
|
+
const profileDir = profileDirectory(profile)
|
|
719
|
+
let deps: Record<string, string> = {}
|
|
720
|
+
try {
|
|
721
|
+
const pkg = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as { dependencies?: Record<string, string> }
|
|
722
|
+
deps = pkg.dependencies ?? {}
|
|
723
|
+
} catch {
|
|
724
|
+
return null
|
|
725
|
+
}
|
|
726
|
+
const targetRepo = githubRepoOf(target)?.toLowerCase()
|
|
727
|
+
const name = targetRepo !== undefined
|
|
728
|
+
? Object.keys(deps).find((k) => githubRepoOf(deps[k])?.toLowerCase() === targetRepo)
|
|
729
|
+
: deps[target] !== undefined ? target : Object.keys(deps).find((k) => deps[k] === target)
|
|
730
|
+
if (!name) return null
|
|
731
|
+
try {
|
|
732
|
+
const meta = JSON.parse(readFileSync(join(profileDir, 'node_modules', name, 'package.json'), 'utf8')) as { version?: unknown }
|
|
733
|
+
return typeof meta.version === 'string' && meta.version !== '' ? meta.version : null
|
|
734
|
+
} catch {
|
|
735
|
+
return null
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
706
739
|
/**
|
|
707
740
|
* 安装后校验已装包里 package.json 声明的文件(main / exports["."].default 与
|
|
708
741
|
* dsh.bundle.patch)是否真实存在。npm 包也可能漏发构建产物(lib/ 等),pnpm 装完没有
|
|
@@ -805,8 +838,9 @@ async function annotateDshTooOld(result: InstallResult, options: {
|
|
|
805
838
|
if (options.action === 'remove' || result.exitCode === 0) return result
|
|
806
839
|
// 全局 npm 装的是本机 CLI 工具,不进 profile,没有宿主版本要求
|
|
807
840
|
if ((options.globalNpm ?? []).length > 0) return result
|
|
808
|
-
//
|
|
809
|
-
|
|
841
|
+
// 用安装**实际**使用的源读元数据:换源重试后候选表首位可能并不是真正生效的那个,
|
|
842
|
+
// 用错源会读不到包而静默漏报;私有源/镜像里同样能读到包声明的 engines
|
|
843
|
+
const line = await dshTooOldMarker(options.target, { registry: result.registry ?? options.registries?.[0] })
|
|
810
844
|
if (line === null) return result
|
|
811
845
|
if (options.task && options.task.status !== 'cancelled') pushLine(options.task, line)
|
|
812
846
|
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line }
|
|
@@ -25,8 +25,11 @@ export const RELEASE_AGE_TIMEOUT_MS = 8000
|
|
|
25
25
|
const MAX_PACKUMENT_BYTES = 10 * 1024 * 1024
|
|
26
26
|
export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org'
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/** 会话内缓存(源 + 包名 → 结果):只存成功结果,一次网络抖动不会把判断永久粘住。
|
|
29
|
+
* 必须带过期时间 —— 插件中心可能连续开着几小时,作者发了新版本后不能一直沿用旧结果;
|
|
30
|
+
* 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错门禁。 */
|
|
31
|
+
const CACHE_TTL_MS = 10 * 60 * 1000
|
|
32
|
+
const releaseCache = new Map<string, { at: number; release: NpmRelease }>()
|
|
30
33
|
|
|
31
34
|
/** 从完整 packument 取 dist-tags.latest 对应版本及其发布时间;缺任一字段返回 null。 */
|
|
32
35
|
export function latestReleaseOf(packument: unknown): NpmRelease | null {
|
|
@@ -87,21 +90,22 @@ async function fetchPackument(url: string, proxy: string, timeoutMs: number): Pr
|
|
|
87
90
|
return getJson(url, timeoutMs)
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
/**
|
|
93
|
+
/** 查询一个包的最新版本与发布时间(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
|
|
91
94
|
export async function lookupReleaseAge(
|
|
92
95
|
packageName: string,
|
|
93
96
|
registry: string,
|
|
94
97
|
proxy: string,
|
|
95
98
|
timeoutMs = RELEASE_AGE_TIMEOUT_MS,
|
|
96
99
|
): Promise<NpmRelease | null> {
|
|
97
|
-
const cached = releaseCache.get(packageName)
|
|
98
|
-
if (cached !== undefined) return cached
|
|
99
100
|
const base = registry.replace(/\/+$/, '') || DEFAULT_NPM_REGISTRY
|
|
101
|
+
const key = `${base}\n${packageName}`
|
|
102
|
+
const cached = releaseCache.get(key)
|
|
103
|
+
if (cached !== undefined && Date.now() - cached.at < CACHE_TTL_MS) return cached.release
|
|
100
104
|
const body = await fetchPackument(`${base}/${encodeURIComponent(packageName)}`, proxy, timeoutMs)
|
|
101
105
|
if (body === null) return null
|
|
102
106
|
let parsed: unknown
|
|
103
107
|
try { parsed = JSON.parse(body) } catch { return null }
|
|
104
108
|
const release = latestReleaseOf(parsed)
|
|
105
|
-
if (release !== null) releaseCache.set(
|
|
109
|
+
if (release !== null) releaseCache.set(key, { at: Date.now(), release })
|
|
106
110
|
return release
|
|
107
111
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 存储:`DSH_HOME/profiles/<profile>/gro.ngilp-hsd-versions.json`
|
|
18
18
|
* { "<owner>/<repo>"(小写): { version, updatedAt, installedAt } }
|
|
19
19
|
*/
|
|
20
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
20
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs'
|
|
21
21
|
import { homedir } from 'node:os'
|
|
22
22
|
import { dirname, join } from 'node:path'
|
|
23
23
|
|
|
@@ -38,10 +38,25 @@ export function versionsFilePath(profile: string): string {
|
|
|
38
38
|
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile, 'gro.ngilp-hsd-versions.json')
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** 读取某 profile 的版本账本;缺失/损坏/字段不全一律归一化成完整形状。
|
|
42
|
+
* 安装一开始只会写入 npm 包名映射(版本要等任务结束才知道),所以磁盘上可能存在
|
|
43
|
+
* 只有 npmPackage 的记录 —— 原样下发会让客户端的版本比较拿到 undefined 直接抛错。 */
|
|
41
44
|
export function readInstalledVersions(profile: string): InstalledVersionTable {
|
|
42
45
|
try {
|
|
43
46
|
const raw = JSON.parse(readFileSync(versionsFilePath(profile), 'utf8')) as unknown
|
|
44
|
-
|
|
47
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return {}
|
|
48
|
+
const table: InstalledVersionTable = {}
|
|
49
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
50
|
+
if (value === null || typeof value !== 'object') continue
|
|
51
|
+
const rec = value as Partial<InstalledVersionRecord>
|
|
52
|
+
table[key] = {
|
|
53
|
+
version: typeof rec.version === 'string' ? rec.version : '',
|
|
54
|
+
updatedAt: typeof rec.updatedAt === 'string' ? rec.updatedAt : '',
|
|
55
|
+
installedAt: typeof rec.installedAt === 'string' ? rec.installedAt : '',
|
|
56
|
+
...(typeof rec.npmPackage === 'string' && rec.npmPackage !== '' ? { npmPackage: rec.npmPackage } : {}),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return table
|
|
45
60
|
} catch {
|
|
46
61
|
return {}
|
|
47
62
|
}
|
|
@@ -64,7 +79,14 @@ export function recordInstalledVersion(profile: string, repo: string, version: s
|
|
|
64
79
|
export function recordResolvedNpmPackage(profile: string, repo: string, npmPackage: string): void {
|
|
65
80
|
const table = readInstalledVersions(profile)
|
|
66
81
|
const key = repo.toLowerCase()
|
|
67
|
-
|
|
82
|
+
// 显式写全字段:这一步发生在安装开始时,版本还不知道 —— 但落盘记录必须是完整形状,
|
|
83
|
+
// 否则只带 npmPackage 的残缺记录会在客户端版本比较时崩掉整块界面
|
|
84
|
+
table[key] = {
|
|
85
|
+
version: table[key]?.version ?? '',
|
|
86
|
+
updatedAt: table[key]?.updatedAt ?? '',
|
|
87
|
+
installedAt: table[key]?.installedAt ?? '',
|
|
88
|
+
npmPackage,
|
|
89
|
+
}
|
|
68
90
|
writeVersions(profile, table)
|
|
69
91
|
}
|
|
70
92
|
|
|
@@ -78,5 +100,9 @@ export function removeInstalledVersion(profile: string, repo: string): void {
|
|
|
78
100
|
function writeVersions(profile: string, table: InstalledVersionTable): void {
|
|
79
101
|
const file = versionsFilePath(profile)
|
|
80
102
|
mkdirSync(dirname(file), { recursive: true })
|
|
81
|
-
|
|
103
|
+
// 先写临时文件再原子替换:直接覆盖时进程被杀/磁盘满会留下截断的 JSON,
|
|
104
|
+
// 下次读取整份账本都会失效(更新检测静默停摆),而用户看不到任何报错
|
|
105
|
+
const tmp = `${file}.tmp`
|
|
106
|
+
writeFileSync(tmp, JSON.stringify(table, null, 2) + '\n', 'utf8')
|
|
107
|
+
renameSync(tmp, file)
|
|
82
108
|
}
|