dsh-plugin 2.3.0 → 2.3.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/README.en.md +12 -12
- package/README.md +12 -12
- package/client/client.js +412 -345
- package/client/client.js.map +1 -1
- package/lib/http/install.js +12 -1
- package/lib/http/npm-release-age.js +7 -1
- package/lib/services/npm-release-age.js +41 -4
- package/lib/types/services/npm-release-age.d.ts +14 -2
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +40 -9
- package/src/client/components/catalog/PluginCard.tsx +26 -6
- package/src/client/components/modals/modals.tsx +4 -2
- package/src/client/components/views/InstalledView.tsx +62 -24
- package/src/client/data/host.ts +2 -0
- package/src/client/hooks/useTaskQueue.ts +1 -1
- package/src/client/locales.ts +12 -6
- package/src/client/logic/install-command.ts +6 -3
- package/src/client/styles/InstalledView.module.css +72 -19
- package/src/client/styles/List.module.css +33 -3
- package/src/server/http/install.ts +11 -1
- package/src/server/http/npm-release-age.ts +7 -1
- package/src/server/services/npm-release-age.ts +45 -5
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
background: var(--hub-text-tertiary);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
/* 主信息:名称 + 已装版本 +
|
|
224
|
+
/* 主信息:名称 + 已装版本 + 有更新徽标(同一行,名称超长时省略,徽标不被挤走) */
|
|
225
225
|
.rowMain {
|
|
226
226
|
display: flex;
|
|
227
227
|
align-items: baseline;
|
|
@@ -230,25 +230,56 @@
|
|
|
230
230
|
flex: 1 1 auto;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/* 右上角 Star / Fork:靠右单行,配色与插件市场列表一致(星标琥珀、Fork 次级灰) */
|
|
234
|
+
.rowStats {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
gap: 10px;
|
|
238
|
+
margin-left: auto;
|
|
239
|
+
flex-shrink: 0;
|
|
240
|
+
white-space: nowrap;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.rowStar {
|
|
244
|
+
font-size: 11px;
|
|
245
|
+
line-height: 16px;
|
|
246
|
+
color: var(--hub-warn);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.rowFork {
|
|
250
|
+
font-size: 11px;
|
|
251
|
+
line-height: 16px;
|
|
252
|
+
color: var(--hub-text-tertiary);
|
|
253
|
+
}
|
|
254
|
+
|
|
233
255
|
.rowTitle {
|
|
234
256
|
font-size: 12px;
|
|
235
257
|
line-height: 18px;
|
|
236
258
|
font-weight: 600;
|
|
237
259
|
color: var(--hub-text-primary);
|
|
260
|
+
flex: 0 1 auto;
|
|
238
261
|
min-width: 0;
|
|
239
262
|
white-space: nowrap;
|
|
240
263
|
overflow: hidden;
|
|
241
264
|
text-overflow: ellipsis;
|
|
265
|
+
/* 名称是主站详情页链接:不做成蓝色下划线,悬停才点明可点 */
|
|
266
|
+
text-decoration: none;
|
|
242
267
|
}
|
|
243
268
|
|
|
244
|
-
|
|
269
|
+
a.rowTitle:hover {
|
|
270
|
+
color: var(--hub-brand);
|
|
271
|
+
text-decoration: underline;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* 已装版本:等宽弱化小字。描边透明但保留 1px,与同行的「有更新」「插件市场」等高 */
|
|
245
275
|
.versionBadge {
|
|
246
276
|
flex-shrink: 0;
|
|
247
277
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
248
|
-
font-size:
|
|
249
|
-
line-height:
|
|
278
|
+
font-size: 10px;
|
|
279
|
+
line-height: 14px;
|
|
250
280
|
color: var(--hub-text-tertiary);
|
|
251
281
|
background: var(--hub-bg-hover);
|
|
282
|
+
border: 1px solid transparent;
|
|
252
283
|
border-radius: 4px;
|
|
253
284
|
padding: 0 5px;
|
|
254
285
|
}
|
|
@@ -256,8 +287,8 @@
|
|
|
256
287
|
/* 有更新徽标:品牌色实底小圆标 */
|
|
257
288
|
.updateBadge {
|
|
258
289
|
flex-shrink: 0;
|
|
259
|
-
font-size:
|
|
260
|
-
line-height:
|
|
290
|
+
font-size: 10px;
|
|
291
|
+
line-height: 14px;
|
|
261
292
|
font-weight: 500;
|
|
262
293
|
color: var(--hub-warn);
|
|
263
294
|
background: var(--hub-warn-tint);
|
|
@@ -274,8 +305,8 @@
|
|
|
274
305
|
width: 60px;
|
|
275
306
|
box-sizing: border-box;
|
|
276
307
|
text-align: center;
|
|
277
|
-
font-size:
|
|
278
|
-
line-height:
|
|
308
|
+
font-size: 10px;
|
|
309
|
+
line-height: 14px;
|
|
279
310
|
font-weight: 500;
|
|
280
311
|
border-radius: 4px;
|
|
281
312
|
padding: 0 4px;
|
|
@@ -300,8 +331,8 @@
|
|
|
300
331
|
与「有更新」实底区分,表达「这是示例项目,宿主不加载」 */
|
|
301
332
|
.exampleBadge {
|
|
302
333
|
flex-shrink: 0;
|
|
303
|
-
font-size:
|
|
304
|
-
line-height:
|
|
334
|
+
font-size: 10px;
|
|
335
|
+
line-height: 14px;
|
|
305
336
|
font-weight: 500;
|
|
306
337
|
color: var(--hub-brand);
|
|
307
338
|
background: var(--hub-brand-tint);
|
|
@@ -338,16 +369,34 @@
|
|
|
338
369
|
white-space: nowrap;
|
|
339
370
|
overflow: hidden;
|
|
340
371
|
text-overflow: ellipsis;
|
|
372
|
+
/* 仓库名是主站详情页链接:默认与普通文字无异,悬停才提示可点 */
|
|
373
|
+
text-decoration: none;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
a.rowRepo:hover {
|
|
377
|
+
color: var(--hub-brand);
|
|
378
|
+
text-decoration: underline;
|
|
341
379
|
}
|
|
342
380
|
|
|
343
381
|
.rowCategory {
|
|
344
382
|
flex-shrink: 0;
|
|
345
|
-
font-size:
|
|
346
|
-
line-height:
|
|
383
|
+
font-size: 10px;
|
|
384
|
+
line-height: 14px;
|
|
347
385
|
color: var(--hub-text-tertiary);
|
|
348
386
|
background: var(--hub-bg-hover);
|
|
387
|
+
border: 1px solid transparent;
|
|
349
388
|
border-radius: 4px;
|
|
350
|
-
padding: 0
|
|
389
|
+
padding: 0 5px;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* 来源标签 + 操作按钮行:独立成行靠右成组,名称行只留名称/版本/更新状态 */
|
|
393
|
+
.rowActionLine {
|
|
394
|
+
display: flex;
|
|
395
|
+
align-items: center;
|
|
396
|
+
justify-content: flex-end;
|
|
397
|
+
gap: 8px;
|
|
398
|
+
padding-left: 16px;
|
|
399
|
+
min-width: 0;
|
|
351
400
|
}
|
|
352
401
|
|
|
353
402
|
/* —— 行操作:详情 / 更新 / 卸载 —— */
|
|
@@ -359,14 +408,16 @@
|
|
|
359
408
|
flex-shrink: 0;
|
|
360
409
|
}
|
|
361
410
|
|
|
362
|
-
/*
|
|
411
|
+
/* 详情:默认无边框(低频操作不抢视觉),悬停才出现描边与底色成为按钮形状。
|
|
412
|
+
描边保留 1px 透明而非 none,保证与同行的更新/卸载按钮同高、文字基线对齐 */
|
|
363
413
|
.rowDetail {
|
|
364
414
|
font-family: inherit;
|
|
365
415
|
font-size: 12px;
|
|
366
416
|
line-height: 18px;
|
|
417
|
+
font-weight: 500;
|
|
367
418
|
color: var(--hub-text-secondary);
|
|
368
419
|
background: transparent;
|
|
369
|
-
border: 1px solid
|
|
420
|
+
border: 1px solid transparent;
|
|
370
421
|
border-radius: 6px;
|
|
371
422
|
padding: 2px 10px;
|
|
372
423
|
cursor: pointer;
|
|
@@ -376,7 +427,7 @@
|
|
|
376
427
|
|
|
377
428
|
.rowDetail:hover {
|
|
378
429
|
color: var(--hub-text-primary);
|
|
379
|
-
border-color: var(--hub-border-
|
|
430
|
+
border-color: var(--hub-border-2);
|
|
380
431
|
background: var(--hub-bg-hover);
|
|
381
432
|
}
|
|
382
433
|
|
|
@@ -403,7 +454,8 @@
|
|
|
403
454
|
border-color: var(--hub-warning);
|
|
404
455
|
}
|
|
405
456
|
|
|
406
|
-
/*
|
|
457
|
+
/* 更新:琥珀色实底小按钮(与插件市场「可更新」同色),仅在「有更新」时出现。
|
|
458
|
+
与详情/卸载同度量(1px 描边 + 2px 内距),否则实底按钮会比描边按钮窄一圈 */
|
|
407
459
|
.rowUpdate {
|
|
408
460
|
font-family: inherit;
|
|
409
461
|
font-size: 12px;
|
|
@@ -411,9 +463,9 @@
|
|
|
411
463
|
font-weight: 500;
|
|
412
464
|
color: #fff;
|
|
413
465
|
background: var(--hub-warn);
|
|
414
|
-
border:
|
|
466
|
+
border: 1px solid var(--hub-warn);
|
|
415
467
|
border-radius: 6px;
|
|
416
|
-
padding:
|
|
468
|
+
padding: 2px 10px;
|
|
417
469
|
cursor: pointer;
|
|
418
470
|
user-select: none;
|
|
419
471
|
transition: background 120ms ease;
|
|
@@ -428,6 +480,7 @@
|
|
|
428
480
|
font-family: inherit;
|
|
429
481
|
font-size: 12px;
|
|
430
482
|
line-height: 18px;
|
|
483
|
+
font-weight: 500;
|
|
431
484
|
color: var(--hub-danger);
|
|
432
485
|
background: transparent;
|
|
433
486
|
border: 1px solid var(--hub-danger-border-soft);
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
gap: 4px;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
/* 名称行:名称可收缩到省略号,版本与更新徽标不收缩 —— 长仓库名不会把状态挤没 */
|
|
56
|
+
.cardTitleRow {
|
|
56
57
|
display: flex;
|
|
57
58
|
align-items: center;
|
|
58
59
|
gap: 6px;
|
|
@@ -60,26 +61,55 @@
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
.cardTitle {
|
|
64
|
+
flex: 0 1 auto;
|
|
65
|
+
min-width: 0;
|
|
63
66
|
font-size: 13px;
|
|
64
67
|
line-height: 18px;
|
|
65
68
|
font-weight: 600;
|
|
66
69
|
white-space: nowrap;
|
|
67
70
|
overflow: hidden;
|
|
68
71
|
text-overflow: ellipsis;
|
|
72
|
+
/* 链接形态但不做成蓝色下划线:沿用标题颜色,悬停才点明可点 */
|
|
73
|
+
color: inherit;
|
|
74
|
+
text-decoration: none;
|
|
75
|
+
transition: color 120ms ease;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
a.cardTitle:hover {
|
|
79
|
+
color: var(--hub-brand);
|
|
80
|
+
text-decoration: underline;
|
|
69
81
|
}
|
|
70
82
|
|
|
83
|
+
/* 分类 / 验证徽标行:与名称行分开,长名称与多枚徽标互不抢宽度 */
|
|
84
|
+
.cardBadgeRow {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
flex-wrap: wrap;
|
|
88
|
+
gap: 6px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* 徽章统一度量:字号 / 行高 / 内距 / 描边一致,同屏并排时不会一大一小。
|
|
92
|
+
以 14px 行高 + 1px 描边收成 16px 高的轻量标签,不与名称行争视觉重量 */
|
|
71
93
|
.categoryBadge,
|
|
72
94
|
.verified,
|
|
95
|
+
.authorBadge,
|
|
73
96
|
.versionBadge,
|
|
74
97
|
.updateBadge {
|
|
75
98
|
flex-shrink: 0;
|
|
76
99
|
font-size: 10px;
|
|
77
|
-
line-height:
|
|
100
|
+
line-height: 14px;
|
|
78
101
|
border-radius: 4px;
|
|
79
|
-
padding: 0
|
|
102
|
+
padding: 0 5px;
|
|
80
103
|
border: 1px solid transparent;
|
|
81
104
|
}
|
|
82
105
|
|
|
106
|
+
/* 作者:等宽字体 + 中性灰,与分类(品牌蓝)、已验证(绿)区分,避免一行里多个彩色标签抢注意力 */
|
|
107
|
+
.authorBadge {
|
|
108
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
109
|
+
color: var(--hub-text-tertiary);
|
|
110
|
+
border-color: var(--hub-border-ghost);
|
|
111
|
+
}
|
|
112
|
+
|
|
83
113
|
.categoryBadge {
|
|
84
114
|
color: var(--hub-brand);
|
|
85
115
|
border-color: var(--hub-border-ghost);
|
|
@@ -12,6 +12,7 @@ import { isDshNpmPackageForRepo } from '../services/install/npm-resolve.ts'
|
|
|
12
12
|
import { probeUrl, systemProxy } from '../services/probe.ts'
|
|
13
13
|
import { loadSettings, type HubSettings } from '../services/settings.ts'
|
|
14
14
|
import { registryCandidates, registryHost, userNpmRegistry } from '../services/install/registry-fallback.ts'
|
|
15
|
+
import { lookupReleaseAge } from '../services/npm-release-age.ts'
|
|
15
16
|
import { profileDirectory } from '../services/profile/profile.ts'
|
|
16
17
|
import { readInstalled } from './catalog.ts'
|
|
17
18
|
import { COMMAND_TIMEOUT_MS, readJsonBody, requireTrustedPost, sendJson, type WebRoute, type WebServerService } from './shared.ts'
|
|
@@ -181,6 +182,15 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
181
182
|
}
|
|
182
183
|
ordered = [chosen, ...registries.filter((r) => r !== chosen)]
|
|
183
184
|
}
|
|
185
|
+
// 新安装钉住「已过供应链门禁」的版本:pnpm 对发布未满 24h 的版本会静默回退到很旧的版本,
|
|
186
|
+
// 用户装完才发现功能缺失且没有任何提示。更新已有目标时客户端已按门禁筛选,继续走 @latest。
|
|
187
|
+
let installSpec = pinVersion !== '' ? `${target}@${pinVersion}` : undefined
|
|
188
|
+
if (installSpec === undefined && already === null) {
|
|
189
|
+
// 解析源与安装首选源一致,钉住的版本一定存在于真正会用的那个源;解析失败则不钉,不阻断安装。
|
|
190
|
+
// 超时压到 5s:网络刚探通,慢也只在极端情况,不能为查版本把安装请求拖住 8s
|
|
191
|
+
const release = await lookupReleaseAge(target, ordered[0] ?? '', effectiveProxy, 5000)
|
|
192
|
+
if (release !== null && release.installable !== null) installSpec = `${target}@${release.installable.version}`
|
|
193
|
+
}
|
|
184
194
|
// 入队前复检:网络探测的 await 期间并发请求可能已通过上方检查,而此处到 startPluginMutation
|
|
185
195
|
// 之间无 await,JS 单线程保证同一目标至多一个任务,杜绝双击/并发双任务。
|
|
186
196
|
if (hasQueuedTarget(target)) {
|
|
@@ -204,7 +214,7 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
204
214
|
env: mutationEnv(settings),
|
|
205
215
|
// 候选源(首选在前):任务内遇到传输层失败时按顺序换源重试
|
|
206
216
|
registries: ordered,
|
|
207
|
-
installSpec
|
|
217
|
+
installSpec,
|
|
208
218
|
// 自定义安装入口渠道:日志/报错据此溯源
|
|
209
219
|
installChannel,
|
|
210
220
|
})
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { systemProxy } from '../services/probe.ts'
|
|
10
10
|
import { lookupReleaseAge, parseReleaseAgeRequest } from '../services/npm-release-age.ts'
|
|
11
|
+
import { userNpmRegistry } from '../services/install/registry-fallback.ts'
|
|
12
|
+
import { profileDirectory } from '../services/profile/profile.ts'
|
|
11
13
|
import { loadSettings } from '../services/settings.ts'
|
|
12
14
|
import { readJsonBody, requireTrustedPost, sendJson, type WebRoute, type WebServerService } from './shared.ts'
|
|
13
15
|
|
|
@@ -30,8 +32,12 @@ export function registerNpmReleaseAgeRoutes(webServer: WebServerService, profile
|
|
|
30
32
|
? settings.proxy
|
|
31
33
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
32
34
|
// 并发查询:单包各自带超时且失败即 null,整体不因任一包失败而失败
|
|
35
|
+
// 查询源与安装首选源同口径(设置 → 本机 npm 配置):安装钉住的版本必须来自真正要用的那个源
|
|
36
|
+
const registry = settings.npmRegistry !== ''
|
|
37
|
+
? settings.npmRegistry
|
|
38
|
+
: userNpmRegistry(profileDirectory(profile))
|
|
33
39
|
const results = await Promise.all(packages.map(async (name) =>
|
|
34
|
-
[name, await lookupReleaseAge(name,
|
|
40
|
+
[name, await lookupReleaseAge(name, registry, proxy)] as const))
|
|
35
41
|
sendJson(response, 200, { packages: Object.fromEntries(results) })
|
|
36
42
|
} catch (error) {
|
|
37
43
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
@@ -17,6 +17,15 @@ export interface NpmRelease {
|
|
|
17
17
|
publishedAt: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** 最新版 + 可钉住的安装版本:安装时钉住后者,pnpm 才不会把「发布未满门禁」的最新版回退成很旧的版本。 */
|
|
21
|
+
export interface NpmReleaseWithInstallable extends NpmRelease {
|
|
22
|
+
/** 已过供应链门禁的最高稳定版;时间表缺失或没有满足项时为 null(调用方退回不钉版本)。 */
|
|
23
|
+
installable: NpmRelease | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** pnpm profile 默认 minimumReleaseAge=1440 分钟;+30 分钟缓冲与客户端 release-age 判定同口径。 */
|
|
27
|
+
export const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000
|
|
28
|
+
|
|
20
29
|
/** 请求体包名数量上限:只为已安装插件按需查询,几十个足矣,封顶防滥用。 */
|
|
21
30
|
export const MAX_RELEASE_AGE_PACKAGES = 50
|
|
22
31
|
/** 单包查询超时(ms):慢网络下失败即返回 null,不拖住整个响应。 */
|
|
@@ -29,7 +38,7 @@ export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org'
|
|
|
29
38
|
* 必须带过期时间 —— 插件中心可能连续开着几小时,作者发了新版本后不能一直沿用旧结果;
|
|
30
39
|
* 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错门禁。 */
|
|
31
40
|
const CACHE_TTL_MS = 10 * 60 * 1000
|
|
32
|
-
const releaseCache = new Map<string, { at: number; release:
|
|
41
|
+
const releaseCache = new Map<string, { at: number; release: NpmReleaseWithInstallable }>()
|
|
33
42
|
|
|
34
43
|
/** 从完整 packument 取 dist-tags.latest 对应版本及其发布时间;缺任一字段返回 null。 */
|
|
35
44
|
export function latestReleaseOf(packument: unknown): NpmRelease | null {
|
|
@@ -44,6 +53,35 @@ export function latestReleaseOf(packument: unknown): NpmRelease | null {
|
|
|
44
53
|
return { version, publishedAt }
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 已过供应链门禁的最高稳定版本:安装时钉住它,pnpm 就不会把最新版静默回退成很旧的版本。
|
|
58
|
+
* 只看纯 x.y.z(预发布版不参与,避免把用户带到不兼容预览版);时间表缺失或无满足项返回 null。
|
|
59
|
+
*/
|
|
60
|
+
export function installableReleaseOf(packument: unknown, now = Date.now()): NpmRelease | null {
|
|
61
|
+
if (packument === null || typeof packument !== 'object') return null
|
|
62
|
+
const time = (packument as { time?: unknown }).time
|
|
63
|
+
if (time === null || typeof time !== 'object') return null
|
|
64
|
+
let best: NpmRelease | null = null
|
|
65
|
+
for (const [version, published] of Object.entries(time as Record<string, unknown>)) {
|
|
66
|
+
if (!/^\d+\.\d+\.\d+$/.test(version) || typeof published !== 'string') continue
|
|
67
|
+
const at = Date.parse(published)
|
|
68
|
+
if (Number.isNaN(at) || now - at < MIN_RELEASE_AGE_MS) continue
|
|
69
|
+
if (best === null || compareVersion(version, best.version) > 0) best = { version, publishedAt: published }
|
|
70
|
+
}
|
|
71
|
+
return best
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 纯数字段版本比较(installableReleaseOf 只在 x.y.z 里用)。 */
|
|
75
|
+
function compareVersion(a: string, b: string): number {
|
|
76
|
+
const pa = a.split('.')
|
|
77
|
+
const pb = b.split('.')
|
|
78
|
+
for (let i = 0; i < 3; i++) {
|
|
79
|
+
const d = Number.parseInt(pa[i] ?? '0', 10) - Number.parseInt(pb[i] ?? '0', 10)
|
|
80
|
+
if (d !== 0) return d
|
|
81
|
+
}
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
84
|
+
|
|
47
85
|
/** 请求体 → 去重后的合法包名列表;非数组 / 超量 / 含非法包名一律 null(拒绝非法输入)。 */
|
|
48
86
|
export function parseReleaseAgeRequest(body: unknown): string[] | null {
|
|
49
87
|
if (body === null || typeof body !== 'object') return null
|
|
@@ -90,13 +128,13 @@ async function fetchPackument(url: string, proxy: string, timeoutMs: number): Pr
|
|
|
90
128
|
return getJson(url, timeoutMs)
|
|
91
129
|
}
|
|
92
130
|
|
|
93
|
-
/**
|
|
131
|
+
/** 查询一个包的最新版本、发布时间与可钉住的安装版本(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
|
|
94
132
|
export async function lookupReleaseAge(
|
|
95
133
|
packageName: string,
|
|
96
134
|
registry: string,
|
|
97
135
|
proxy: string,
|
|
98
136
|
timeoutMs = RELEASE_AGE_TIMEOUT_MS,
|
|
99
|
-
): Promise<
|
|
137
|
+
): Promise<NpmReleaseWithInstallable | null> {
|
|
100
138
|
const base = registry.replace(/\/+$/, '') || DEFAULT_NPM_REGISTRY
|
|
101
139
|
const key = `${base}\n${packageName}`
|
|
102
140
|
const cached = releaseCache.get(key)
|
|
@@ -105,7 +143,9 @@ export async function lookupReleaseAge(
|
|
|
105
143
|
if (body === null) return null
|
|
106
144
|
let parsed: unknown
|
|
107
145
|
try { parsed = JSON.parse(body) } catch { return null }
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
146
|
+
const latest = latestReleaseOf(parsed)
|
|
147
|
+
if (latest === null) return null
|
|
148
|
+
const release: NpmReleaseWithInstallable = { ...latest, installable: installableReleaseOf(parsed) }
|
|
149
|
+
releaseCache.set(key, { at: Date.now(), release })
|
|
110
150
|
return release
|
|
111
151
|
}
|