dsh-m 0.9.84 → 0.9.87
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/CHANGELOG.md +36 -0
- package/lib/client.js +23 -17
- package/lib/core/community.js +20 -7
- package/lib/core/exclude-governance.js +13 -3
- package/lib/core/host-api.js +23 -2
- package/lib/core/installed.js +68 -37
- package/lib/core/market.js +108 -5
- package/lib/core/merged-market.js +3 -2
- package/lib/core/packument.js +362 -0
- package/lib/core/profile-transaction.js +6 -2
- package/lib/core/release-age.js +14 -56
- package/lib/core/versions.js +7 -191
- package/package.json +1 -1
package/lib/core/versions.js
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
* npm:registry /latest(pnpm 安装本身会按 lock integrity 校验 tarball)。
|
|
4
4
|
* GitHub:优先最新 release/tag(更新提示只跟稳定版走,不跟 main HEAD——中间提交可能不稳定);
|
|
5
5
|
* 未认证限额 60 次/小时,自用足够。
|
|
6
|
+
*
|
|
7
|
+
* 0.9.86(ADR-0019):npm registry 元数据族(npmVersion/npmPackument/npmPackumentReadme/
|
|
8
|
+
* npmLatest/extractGithubRepo/isExactVersion 及其类型)实现已迁入 packument 访问器属主模块;
|
|
9
|
+
* 本文件原名 re-export 保全部 import 位零改动,自身只留 GitHub 族。
|
|
6
10
|
*/
|
|
7
11
|
import { gt } from 'semver';
|
|
8
12
|
import { fetchJsonLimited, HttpError } from './httpx.js';
|
|
9
|
-
import {
|
|
13
|
+
import { isExactVersion } from './packument.js';
|
|
14
|
+
export { npmVersion, npmPackument, npmPackumentReadme, npmLatest, extractGithubRepo, isExactVersion, } from './packument.js';
|
|
10
15
|
/** GitHub 匿名限额(60 次/小时/IP)用尽时返回可读提示(含重置等待分钟数),否则 null。 */
|
|
11
16
|
function githubRateLimitMessage(err) {
|
|
12
17
|
if (err instanceof HttpError && err.status === 403 && err.headers?.get('x-ratelimit-remaining') === '0') {
|
|
@@ -20,195 +25,6 @@ function githubRateLimitMessage(err) {
|
|
|
20
25
|
}
|
|
21
26
|
return null;
|
|
22
27
|
}
|
|
23
|
-
/** registry base 归一(去尾斜杠;空/缺省 → 官方 npmjs)。 */
|
|
24
|
-
function registryBase(registry) {
|
|
25
|
-
const base = typeof registry === 'string' && registry.trim() !== '' ? registry.trim() : 'https://registry.npmjs.org';
|
|
26
|
-
return base.replace(/\/+$/, '');
|
|
27
|
-
}
|
|
28
|
-
/** 精确 semver:接受 prerelease/build metadata,拒绝 v 前缀、range、tag 与脏尾缀。 */
|
|
29
|
-
const EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
30
|
-
export function isExactVersion(version) {
|
|
31
|
-
return EXACT_VERSION_RE.test(version);
|
|
32
|
-
}
|
|
33
|
-
function sleepAbortable(ms, signal) {
|
|
34
|
-
return new Promise((resolve) => {
|
|
35
|
-
const done = () => {
|
|
36
|
-
clearTimeout(t);
|
|
37
|
-
signal?.removeEventListener('abort', done);
|
|
38
|
-
resolve();
|
|
39
|
-
};
|
|
40
|
-
const t = setTimeout(done, ms);
|
|
41
|
-
signal?.addEventListener('abort', done, { once: true });
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* 精确版本元数据(兼容预检/integrity 锚定)。L2① 履约阶梯(ADR-0012):
|
|
46
|
-
* 显式 registry 参数 > 生效源 → 失败且生效源为镜像 → sync → 有界等待 10s → 同源重试一次
|
|
47
|
-
* → 仍败 → npmjs 兜底(sync+重试为尽力而为,npmjs 兜底是主安全网);
|
|
48
|
-
* 主源即 npmjs 时失败直接抛(权威源无镜像滞后可言)。
|
|
49
|
-
*/
|
|
50
|
-
export async function npmVersion(pkg, version, timeoutMs = 20_000, signal, registry, deps) {
|
|
51
|
-
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
|
52
|
-
throw new Error(`无效 npm 包名: ${pkg}`);
|
|
53
|
-
if (!isExactVersion(version))
|
|
54
|
-
throw new Error(`不是精确版本(拒绝 range/tag/前缀): ${version}`);
|
|
55
|
-
const fetcher = deps?.fetchJsonLimited ?? fetchJsonLimited;
|
|
56
|
-
const sync = deps?.syncNpmmirror ?? syncNpmmirrorPackage;
|
|
57
|
-
const wait = deps?.wait ?? sleepAbortable;
|
|
58
|
-
const leg = async (base) => {
|
|
59
|
-
const data = await fetcher(`${registryBase(base)}/${encodeURIComponent(pkg)}/${version}`, { timeoutMs, signal });
|
|
60
|
-
const resolved = typeof data.version === 'string' ? data.version : '';
|
|
61
|
-
if (!resolved)
|
|
62
|
-
throw new Error(`npm 未返回版本: ${pkg}@${version}`);
|
|
63
|
-
const peers = {};
|
|
64
|
-
if (data.peerDependencies !== null && typeof data.peerDependencies === 'object') {
|
|
65
|
-
for (const [name, range] of Object.entries(data.peerDependencies)) {
|
|
66
|
-
if (typeof range === 'string')
|
|
67
|
-
peers[name] = range;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
version: resolved,
|
|
72
|
-
integrity: typeof data.dist?.integrity === 'string' ? data.dist.integrity : undefined,
|
|
73
|
-
tarball: typeof data.dist?.tarball === 'string' ? data.dist.tarball : undefined,
|
|
74
|
-
peers,
|
|
75
|
-
};
|
|
76
|
-
};
|
|
77
|
-
const primary = registry?.trim() ? registry.trim() : await activeNpmRegistry();
|
|
78
|
-
try {
|
|
79
|
-
return await leg(primary);
|
|
80
|
-
}
|
|
81
|
-
catch (firstErr) {
|
|
82
|
-
// L2①(ADR-0012):仅 sync 真正受理(评审 R1-2:kill switch / 同步失败返回 false 时
|
|
83
|
-
// 不付 10s 等待成本、不发必 404 的同源重试——「风险与回退」承诺的行为回到现状)才重试镜像。
|
|
84
|
-
if (primary === NPM_MIRROR && (await sync(pkg))) {
|
|
85
|
-
await wait(10_000, signal);
|
|
86
|
-
try {
|
|
87
|
-
return await leg(primary);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
// 同源重试仍败 → npmjs 兜底
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (primary !== DEFAULT_NPM_REGISTRY)
|
|
94
|
-
return leg(DEFAULT_NPM_REGISTRY);
|
|
95
|
-
throw firstErr;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* 拉取完整 packument(NO_MATCHING_VERSION 退避重试前的预热/校验原语)。
|
|
100
|
-
* 返回该包已知的全部版本号;解析不出 versions 时返回空列表(不抛)。
|
|
101
|
-
* L1(ADR-0012/R1-6):registry 参数曾被忽略——现「显式参数 > 生效源」。
|
|
102
|
-
*/
|
|
103
|
-
export async function npmPackument(pkg, timeoutMs = 20_000, signal, registry, deps) {
|
|
104
|
-
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
|
105
|
-
throw new Error(`无效 npm 包名: ${pkg}`);
|
|
106
|
-
const fetcher = deps?.fetchJsonLimited ?? fetchJsonLimited;
|
|
107
|
-
const base = registry?.trim() ? registry.trim() : await activeNpmRegistry();
|
|
108
|
-
const data = await fetcher(`${registryBase(base)}/${encodeURIComponent(pkg)}`, {
|
|
109
|
-
timeoutMs,
|
|
110
|
-
signal,
|
|
111
|
-
maxBytes: 8 * 1024 * 1024,
|
|
112
|
-
});
|
|
113
|
-
const versions = data?.versions !== null && typeof data?.versions === 'object' ? Object.keys(data.versions) : [];
|
|
114
|
-
return { versions };
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* packument repository → "owner/repo"(仅 github.com;git+https / git@ / .git / #frag 归一;
|
|
118
|
-
* 非 GitHub 返回空串——README 相对路径锚定只对 GitHub raw/blob 有意义)。0.9.48。
|
|
119
|
-
*/
|
|
120
|
-
export function extractGithubRepo(v) {
|
|
121
|
-
const url = typeof v === 'string'
|
|
122
|
-
? v
|
|
123
|
-
: v && typeof v === 'object' && typeof v.url === 'string'
|
|
124
|
-
? v.url
|
|
125
|
-
: '';
|
|
126
|
-
if (!url)
|
|
127
|
-
return '';
|
|
128
|
-
const m = /github\.com[/:]([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?(?:[/?#]|$)/i.exec(url);
|
|
129
|
-
return m ? `${m[1]}/${m[2]}` : '';
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* npm packument readme 兜底(0.9.45 U10b 修复):市场/收藏详情 Modal 的 README 折叠页对
|
|
133
|
-
* 「未安装」条目无法走本地 profile 读取——回源 npm 兜底。安全基线同 npmPackument:HTTPS、
|
|
134
|
-
* 8MB 上限;无 readme 字段返回空串(不抛,客户端显示「没有 README」);L1(ADR-0012):
|
|
135
|
-
* 显式 registry 参数 > 生效源。
|
|
136
|
-
* 0.9.48:随附 repo(repository 归一为 owner/repo,仅 GitHub),供 README 相对链接/图片
|
|
137
|
-
* 锚定仓库基址;无 repository 或非 GitHub 时为空串。
|
|
138
|
-
* 0.9.50:两腿阶梯——腿1 `/latest` 版本文档优先(KB 级小载荷,超大 packument 超帽免疫;
|
|
139
|
-
* 超时收紧 `Math.min(timeoutMs, 5_000)`,同 npmLatest 首腿先例),readme 命中即短路;空或
|
|
140
|
-
* 腿失败落腿2 packument(20s 满额,行为同 0.9.45)。任一腿成功即成功——readme 空渲染
|
|
141
|
-
* 「没有 README」(含 latest 空白 readme + packument 失败的降级,不抛);两腿全败抛终末腿
|
|
142
|
-
* (packument)错误。两腿均在生效源,不跨源;两腿 readme 判空同一 trim 规则。repo 随到达
|
|
143
|
-
* 的腿取 repository(packument 到达时以 packument 为准)。
|
|
144
|
-
*/
|
|
145
|
-
export async function npmPackumentReadme(pkg, timeoutMs = 20_000, signal, registry, deps) {
|
|
146
|
-
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
|
147
|
-
throw new Error(`无效 npm 包名: ${pkg}`);
|
|
148
|
-
const fetcher = deps?.fetchJsonLimited ?? fetchJsonLimited;
|
|
149
|
-
const base = registry?.trim() ? registry.trim() : await activeNpmRegistry();
|
|
150
|
-
const extractReadme = (data) => typeof data?.readme === 'string' && data.readme.trim() !== '' ? data.readme : '';
|
|
151
|
-
// 腿1:/latest 版本文档(小载荷;超时收紧同 npmLatest 首腿先例)
|
|
152
|
-
let latestOk = false;
|
|
153
|
-
let latestRepo = '';
|
|
154
|
-
try {
|
|
155
|
-
const latestDoc = await fetcher(`${registryBase(base)}/${encodeURIComponent(pkg)}/latest`, { timeoutMs: Math.min(timeoutMs, 5_000), signal, maxBytes: 8 * 1024 * 1024 });
|
|
156
|
-
latestOk = true;
|
|
157
|
-
latestRepo = extractGithubRepo(latestDoc?.repository);
|
|
158
|
-
const latestReadme = extractReadme(latestDoc);
|
|
159
|
-
if (latestReadme)
|
|
160
|
-
return { readme: latestReadme, repo: latestRepo };
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
// latest 腿失败 → 落 packument 腿(终末腿语义见下)
|
|
164
|
-
}
|
|
165
|
-
// 腿2:packument(0.9.45 U10b 原兜底腿,行为不变)
|
|
166
|
-
try {
|
|
167
|
-
const data = await fetcher(`${registryBase(base)}/${encodeURIComponent(pkg)}`, { timeoutMs, signal, maxBytes: 8 * 1024 * 1024 });
|
|
168
|
-
return { readme: extractReadme(data), repo: extractGithubRepo(data?.repository) };
|
|
169
|
-
}
|
|
170
|
-
catch (packumentErr) {
|
|
171
|
-
// latest 腿已成功证明包在而无有效 readme → 降级成功(空 readme),不冒充失败
|
|
172
|
-
if (latestOk)
|
|
173
|
-
return { readme: '', repo: latestRepo };
|
|
174
|
-
throw packumentErr;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* 升级探测(检测读)。L3 权威链(ADR-0012):新鲜度敏感——npmjs 权威源优先
|
|
179
|
-
* (首腿超时收紧 `Math.min(timeoutMs, 5_000)`),失败降级生效源(≠npmjs 时);
|
|
180
|
-
* 不设 sync 腿(两腿全败属网络型失败,sync 只救镜像滞后)。显式 registry 参数优先。
|
|
181
|
-
*/
|
|
182
|
-
export async function npmLatest(pkg, timeoutMs = 20_000, signal, registry, deps) {
|
|
183
|
-
// 允许 scoped 包名:@scope/name(isSafePkgName 同款字符集)
|
|
184
|
-
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
|
185
|
-
throw new Error(`无效 npm 包名: ${pkg}`);
|
|
186
|
-
const fetcher = deps?.fetchJsonLimited ?? fetchJsonLimited;
|
|
187
|
-
const leg = async (base, ms) => {
|
|
188
|
-
const data = await fetcher(`${registryBase(base)}/${encodeURIComponent(pkg)}/latest`, { timeoutMs: ms, signal });
|
|
189
|
-
const version = typeof data.version === 'string' ? data.version : '';
|
|
190
|
-
if (!version)
|
|
191
|
-
throw new Error(`npm 未返回版本: ${pkg}`);
|
|
192
|
-
return {
|
|
193
|
-
version,
|
|
194
|
-
integrity: typeof data.dist?.integrity === 'string' ? data.dist.integrity : undefined,
|
|
195
|
-
tarball: typeof data.dist?.tarball === 'string' ? data.dist.tarball : undefined,
|
|
196
|
-
};
|
|
197
|
-
};
|
|
198
|
-
const explicit = registry?.trim();
|
|
199
|
-
if (explicit)
|
|
200
|
-
return leg(explicit, timeoutMs);
|
|
201
|
-
const firstLegMs = Math.min(timeoutMs, 5_000);
|
|
202
|
-
try {
|
|
203
|
-
return await leg(DEFAULT_NPM_REGISTRY, firstLegMs);
|
|
204
|
-
}
|
|
205
|
-
catch (firstErr) {
|
|
206
|
-
const alt = await activeNpmRegistry();
|
|
207
|
-
if (alt === DEFAULT_NPM_REGISTRY)
|
|
208
|
-
throw firstErr;
|
|
209
|
-
return leg(alt, timeoutMs);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
28
|
export async function githubTagSha(repo, tag, timeoutMs = 20_000, signal, budget, apiBase = 'https://api.github.com') {
|
|
213
29
|
// commits/{ref} 会自动解引用 annotated tag,返回的才是可用于 #sha 锁定的 commit
|
|
214
30
|
const data = await fetchJsonLimited(`${apiBase}/repos/${repo}/commits/${encodeURIComponent(tag)}`, {
|
|
@@ -386,7 +202,7 @@ export async function githubLatestTag(repo, timeoutMs = 20_000, signal, budget,
|
|
|
386
202
|
*/
|
|
387
203
|
export function isNewerVersion(candidate, current) {
|
|
388
204
|
// 只接受精确 semver(与收录语义一致:v 前缀/range/tag 不参与比较);非法输入不抛、一律 false
|
|
389
|
-
if (!
|
|
205
|
+
if (!isExactVersion(String(candidate ?? '')) || !isExactVersion(String(current ?? '')))
|
|
390
206
|
return false;
|
|
391
207
|
try {
|
|
392
208
|
return gt(String(candidate), String(current));
|