@dsh-workbench/module-tools 1.0.0-beta.1 → 1.0.1
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/package.json +2 -2
- package/src/market-index.mjs +128 -9
- package/src/release-publish.mjs +169 -0
- package/src/release-target.mjs +266 -0
- package/src/release-util.mjs +24 -1
- package/src/release.mjs +103 -443
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/module-tools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"semver": "^7.8.5",
|
|
34
34
|
"typescript": "5.9.3",
|
|
35
35
|
"typescript-eslint": "^8.71.0",
|
|
36
|
-
"@dsh-workbench/shared": "1.0.
|
|
36
|
+
"@dsh-workbench/shared": "1.0.1"
|
|
37
37
|
},
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
package/src/market-index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
MARKET_INDEX_README_LINES,
|
|
@@ -69,6 +70,22 @@ export function sanitizeManifest(manifest) {
|
|
|
69
70
|
return next;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function authorParts(manifest) {
|
|
74
|
+
const author = manifest?.author;
|
|
75
|
+
const raw =
|
|
76
|
+
typeof author === 'string'
|
|
77
|
+
? author.trim()
|
|
78
|
+
: author !== null && typeof author === 'object' && typeof author.name === 'string'
|
|
79
|
+
? author.name.trim()
|
|
80
|
+
: '';
|
|
81
|
+
const declaredEmail =
|
|
82
|
+
author !== null && typeof author === 'object' && typeof author.email === 'string' ? author.email.trim() : '';
|
|
83
|
+
const match = /^(.*?)\s*<([^>]*)>/.exec(raw);
|
|
84
|
+
const name = (match === null ? raw : match[1]).trim();
|
|
85
|
+
const email = match === null ? declaredEmail : match[2].trim() || declaredEmail;
|
|
86
|
+
return { name, email };
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
function authorOf(manifest) {
|
|
73
90
|
const author = manifest.author;
|
|
74
91
|
if (typeof author === 'string') return author.trim();
|
|
@@ -76,23 +93,41 @@ function authorOf(manifest) {
|
|
|
76
93
|
return '';
|
|
77
94
|
}
|
|
78
95
|
|
|
96
|
+
export function identityOf(manifest) {
|
|
97
|
+
const { name, email } = authorParts(manifest);
|
|
98
|
+
if (name === '') return undefined;
|
|
99
|
+
return email === '' ? { name } : { name, email };
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
function descriptionOf(manifest) {
|
|
80
103
|
const description = typeof manifest.description === 'string' ? manifest.description.trim() : '';
|
|
81
104
|
return description === '' ? undefined : { zh: description };
|
|
82
105
|
}
|
|
83
106
|
|
|
84
|
-
|
|
85
|
-
const
|
|
107
|
+
function titleOf(manifest) {
|
|
108
|
+
const title = manifest.title;
|
|
109
|
+
if (typeof title === 'string') {
|
|
110
|
+
const text = title.trim();
|
|
111
|
+
return text === '' ? undefined : { zh: text };
|
|
112
|
+
}
|
|
113
|
+
if (title === null || typeof title !== 'object' || Array.isArray(title)) return undefined;
|
|
114
|
+
const zh = typeof title.zh === 'string' ? title.zh.trim() : '';
|
|
115
|
+
const en = typeof title.en === 'string' ? title.en.trim() : '';
|
|
116
|
+
if (zh === '' && en === '') return undefined;
|
|
117
|
+
return { ...(zh === '' ? {} : { zh }), ...(en === '' ? {} : { en }) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function entryMetaOf(manifest) {
|
|
86
121
|
const author = authorOf(manifest);
|
|
87
122
|
const homepage = typeof manifest.homepage === 'string' ? manifest.homepage.trim() : '';
|
|
88
123
|
const keywords = Array.isArray(manifest.keywords)
|
|
89
124
|
? manifest.keywords.flatMap((one) => (typeof one === 'string' && one.trim() !== '' ? [one.trim()] : []))
|
|
90
125
|
: [];
|
|
91
126
|
const order = manifest.dsh?.workbench?.card?.order;
|
|
92
|
-
const
|
|
127
|
+
const title = titleOf(manifest);
|
|
93
128
|
const description = descriptionOf(manifest);
|
|
94
129
|
return {
|
|
95
|
-
|
|
130
|
+
...(title === undefined ? {} : { title }),
|
|
96
131
|
...(description === undefined ? {} : { description }),
|
|
97
132
|
...(author === '' ? {} : { author }),
|
|
98
133
|
...(homepage === '' ? {} : { homepage }),
|
|
@@ -102,6 +137,15 @@ export function indexEntryOf(input) {
|
|
|
102
137
|
...(keywords.length === 0 ? {} : { keywords }),
|
|
103
138
|
...(typeof order === 'number' ? { sortKeys: { order } } : {}),
|
|
104
139
|
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function indexEntryOf(input) {
|
|
144
|
+
const manifest = input.manifest;
|
|
145
|
+
const dsh = manifest.engines?.dsh;
|
|
146
|
+
return {
|
|
147
|
+
name: manifest.name,
|
|
148
|
+
...entryMetaOf(manifest),
|
|
105
149
|
versions: [
|
|
106
150
|
{
|
|
107
151
|
version: input.version,
|
|
@@ -253,10 +297,15 @@ function assertClean(exec, workdir) {
|
|
|
253
297
|
}
|
|
254
298
|
}
|
|
255
299
|
|
|
256
|
-
function commitOf(exec, workdir, message) {
|
|
300
|
+
function commitOf(exec, workdir, message, identity) {
|
|
257
301
|
const added = runGit(exec, workdir, ['add', '-A'], { allowFailure: true });
|
|
258
302
|
if (added.status !== 0) throw new MarketIndexError(`git add 没过:${tailOf(`${added.stdout}\n${added.stderr}`)}`);
|
|
259
|
-
const
|
|
303
|
+
const who = [];
|
|
304
|
+
if (identity !== undefined) {
|
|
305
|
+
who.push('-c', `user.name=${identity.name}`);
|
|
306
|
+
if (identity.email !== undefined) who.push('-c', `user.email=${identity.email}`);
|
|
307
|
+
}
|
|
308
|
+
const committed = runGit(exec, workdir, [...who, 'commit', '-m', message], { allowFailure: true });
|
|
260
309
|
const text = `${committed.stdout}\n${committed.stderr}`;
|
|
261
310
|
if (committed.status !== 0 && !/nothing to commit|no changes added|无文件要提交/i.test(text)) {
|
|
262
311
|
throw new MarketIndexError(`git commit 没过:${tailOf(text)}`);
|
|
@@ -379,7 +428,7 @@ export function updateMarketIndex(options) {
|
|
|
379
428
|
report({ phase: 'artifacts' });
|
|
380
429
|
artifactPath = copyArtifacts(root, workdir, unit, manifest);
|
|
381
430
|
report({ phase: 'artifacts-copied' });
|
|
382
|
-
const sha = commitOf(exec, workdir, `chore(release): ${entryName}
|
|
431
|
+
const sha = commitOf(exec, workdir, `chore(release): ${entryName}`, identityOf(manifest));
|
|
383
432
|
report({ phase: 'artifacts-committed', artifactCommit: sha });
|
|
384
433
|
const pushed = pushOf(exec, workdir, branch);
|
|
385
434
|
if (pushed.status !== 0) {
|
|
@@ -400,7 +449,7 @@ export function updateMarketIndex(options) {
|
|
|
400
449
|
const entry = indexEntryOf({ manifest, version, source, at: now() });
|
|
401
450
|
report({ phase: 'index' });
|
|
402
451
|
writeEntry(workdir, indexPath, entry);
|
|
403
|
-
let indexSha = commitOf(exec, workdir, `chore(market): ${entryName}
|
|
452
|
+
let indexSha = commitOf(exec, workdir, `chore(market): ${entryName}`, identityOf(manifest));
|
|
404
453
|
report({ phase: 'index-committed', indexCommit: indexSha });
|
|
405
454
|
const pushed = pushOf(exec, workdir, branch);
|
|
406
455
|
if (pushed.status !== 0) {
|
|
@@ -412,7 +461,7 @@ export function updateMarketIndex(options) {
|
|
|
412
461
|
);
|
|
413
462
|
}
|
|
414
463
|
writeEntry(workdir, indexPath, entry);
|
|
415
|
-
indexSha = commitOf(exec, workdir, `chore(market): ${entryName}
|
|
464
|
+
indexSha = commitOf(exec, workdir, `chore(market): ${entryName}`, identityOf(manifest));
|
|
416
465
|
report({ phase: 'index-committed', indexCommit: indexSha });
|
|
417
466
|
const retried = pushOf(exec, workdir, branch);
|
|
418
467
|
if (retried.status !== 0) {
|
|
@@ -451,6 +500,76 @@ export function updateMarketIndex(options) {
|
|
|
451
500
|
}
|
|
452
501
|
}
|
|
453
502
|
|
|
503
|
+
export function refreshMarketEntry(options) {
|
|
504
|
+
const { plan, exec, home, manifest } = options;
|
|
505
|
+
let base = { kind: 'git', url: plan.source.url, entry: manifest.name };
|
|
506
|
+
let phase = 'start';
|
|
507
|
+
try {
|
|
508
|
+
const workdir = worktreeOf(plan, exec, home);
|
|
509
|
+
const indexPath = plan.source.path ?? MARKET_FILE;
|
|
510
|
+
assertClean(exec, workdir);
|
|
511
|
+
const branch = plan.source.ref ?? currentBranch(exec, workdir);
|
|
512
|
+
base = { ...base, branch };
|
|
513
|
+
const file = join(workdir, indexPath);
|
|
514
|
+
if (!existsSync(file)) throw new MarketIndexError(`索引里还没有 ${indexPath} —— 先正常发一版`, 'index-write');
|
|
515
|
+
const parsed = parseMarketIndexJson(readFileSync(file, 'utf8'));
|
|
516
|
+
if (!parsed.ok) {
|
|
517
|
+
throw new MarketIndexError(
|
|
518
|
+
`${indexPath} 不是 format 1:${parsed.problems.map((one) => one.detail).join(';')}`,
|
|
519
|
+
'index-write',
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
const existing = parsed.index.plugins.find((one) => one.name === manifest.name);
|
|
523
|
+
if (existing === undefined) {
|
|
524
|
+
throw new MarketIndexError(`${manifest.name} 还不在索引里 —— 先正常发一版`, 'index-write');
|
|
525
|
+
}
|
|
526
|
+
const latest = existing.versions[0]?.version ?? '';
|
|
527
|
+
const meta = entryMetaOf(manifest);
|
|
528
|
+
const next = { ...existing };
|
|
529
|
+
for (const key of ['title', 'description', 'author', 'homepage', 'metadata']) {
|
|
530
|
+
if (Object.hasOwn(meta, key)) next[key] = meta[key];
|
|
531
|
+
else delete next[key];
|
|
532
|
+
}
|
|
533
|
+
if (isDeepStrictEqual(next, existing)) {
|
|
534
|
+
return { ...base, updated: false, unchanged: true, version: latest, phase: 'done' };
|
|
535
|
+
}
|
|
536
|
+
writeEntry(workdir, indexPath, next);
|
|
537
|
+
let indexSha = commitOf(exec, workdir, `chore(market): refresh ${manifest.name}`, identityOf(manifest));
|
|
538
|
+
const pushed = pushOf(exec, workdir, branch);
|
|
539
|
+
if (pushed.status !== 0) {
|
|
540
|
+
const replayed = resetToRemote(exec, workdir, branch);
|
|
541
|
+
if (!replayed) {
|
|
542
|
+
throw new MarketIndexError(`元数据没推上去(本地 commit ${indexSha}):${tailOf(pushed.stderr)}`, 'index-write');
|
|
543
|
+
}
|
|
544
|
+
writeEntry(workdir, indexPath, next);
|
|
545
|
+
indexSha = commitOf(exec, workdir, `chore(market): refresh ${manifest.name}`, identityOf(manifest));
|
|
546
|
+
const retried = pushOf(exec, workdir, branch);
|
|
547
|
+
if (retried.status !== 0) {
|
|
548
|
+
throw new MarketIndexError(`元数据没推上去:${tailOf(retried.stderr)}`, 'index-write');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const head = runGit(exec, workdir, ['rev-parse', 'HEAD']);
|
|
552
|
+
const headSha = head.status === 0 ? head.stdout.trim() : '';
|
|
553
|
+
const remoteHead = remoteHeadOf(exec, workdir, branch);
|
|
554
|
+
if (headSha === '' || remoteHead !== headSha) {
|
|
555
|
+
phase = 'remote-check';
|
|
556
|
+
throw new MarketIndexError(
|
|
557
|
+
`索引没推上去:远端 ${branch} 是 ${shortOf(remoteHead)},本地是 ${shortOf(headSha)}`,
|
|
558
|
+
'git-market-pushed',
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
return { ...base, updated: true, version: latest, commit: indexSha, phase: 'done' };
|
|
562
|
+
} catch (error) {
|
|
563
|
+
return {
|
|
564
|
+
...base,
|
|
565
|
+
updated: false,
|
|
566
|
+
phase,
|
|
567
|
+
code: error instanceof MarketIndexError ? error.step : undefined,
|
|
568
|
+
reason: messageOf(error),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
454
573
|
function hasHead(exec, workdir) {
|
|
455
574
|
return runGit(exec, workdir, ['rev-parse', '--verify', 'HEAD'], { allowFailure: true }).status === 0;
|
|
456
575
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { NeedsUser, ReleaseError, publishTagOf, tailOf } from './release-util.mjs';
|
|
6
|
+
import { npmrcArgs } from './release-target.mjs';
|
|
7
|
+
|
|
8
|
+
const REQUIRED_FILES = ['dist/host.js', 'client.js', 'cordis.patch.yml', 'package.json', 'CHANGELOG.md'];
|
|
9
|
+
|
|
10
|
+
function parseJson(text) {
|
|
11
|
+
const raw = String(text ?? '');
|
|
12
|
+
const start = raw.search(/[[{]/);
|
|
13
|
+
if (start === -1) return undefined;
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(raw.slice(start));
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function quote(value) {
|
|
22
|
+
const text = String(value);
|
|
23
|
+
return /[\s"'$`\\]/.test(text) ? `'${text.replace(/'/g, `'\\''`)}'` : text;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function localFiles(exec, root) {
|
|
27
|
+
const result = exec(
|
|
28
|
+
'npm',
|
|
29
|
+
['pack', '--dry-run', '--json', '--ignore-scripts', '--cache', join(tmpdir(), 'dsh-wb-npm-cache')],
|
|
30
|
+
{ cwd: root, capture: true, allowFailure: true },
|
|
31
|
+
);
|
|
32
|
+
if (result.status !== 0) throw new ReleaseError(`核不了本地包内容:${tailOf(`${result.stdout}\n${result.stderr}`)}`);
|
|
33
|
+
const parsed = parseJson(result.stdout);
|
|
34
|
+
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
35
|
+
const files = Array.isArray(first?.files) ? first.files.map((one) => String(one.path ?? one)) : [];
|
|
36
|
+
if (files.length === 0) throw new ReleaseError('npm pack 没列出包内容');
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function checkPublished(files) {
|
|
41
|
+
const missing = REQUIRED_FILES.filter((one) => !files.includes(one));
|
|
42
|
+
if (missing.length > 0) throw new ReleaseError(`包里少了 ${missing.join(' / ')} —— 装上会起不来`);
|
|
43
|
+
const leaked = files.filter(
|
|
44
|
+
(one) => one.startsWith('src/') || one.endsWith('.npmrc') || one.startsWith('template') || one.includes('.env'),
|
|
45
|
+
);
|
|
46
|
+
if (leaked.length > 0) throw new ReleaseError(`包里混进了不该发的:${leaked.slice(0, 3).join('、')}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function checkGitFiles(files) {
|
|
50
|
+
const missing = REQUIRED_FILES.filter((one) => !files.includes(one));
|
|
51
|
+
if (missing.length > 0) throw new ReleaseError(`tag 里少了 ${missing.join(' / ')} —— 装上会起不来`);
|
|
52
|
+
const leaked = files.filter((one) => one.endsWith('.npmrc') || one.includes('.env'));
|
|
53
|
+
if (leaked.length > 0) throw new ReleaseError(`tag 里混进了凭据文件:${leaked.slice(0, 3).join('、')}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registryExists(exec, info, name, version) {
|
|
57
|
+
const argv = ['view', `${name}@${version}`, 'version', '--json', '--registry', info.registry, ...npmrcArgs(info.npmrc)];
|
|
58
|
+
const result = exec('npm', argv, { capture: true, allowFailure: true });
|
|
59
|
+
if (result.status === 0) return true;
|
|
60
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
61
|
+
if (/E404|404|No matching version|not found|不存在/i.test(text)) return false;
|
|
62
|
+
throw new ReleaseError(`查团队源失败(${info.registry}):${tailOf(text)}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function publishArgsOf(info, version) {
|
|
66
|
+
const tag = publishTagOf(version);
|
|
67
|
+
return [
|
|
68
|
+
'publish',
|
|
69
|
+
'--registry',
|
|
70
|
+
info.registry,
|
|
71
|
+
'--access',
|
|
72
|
+
info.access,
|
|
73
|
+
'--ignore-scripts',
|
|
74
|
+
...(tag === undefined ? [] : ['--tag', tag]),
|
|
75
|
+
...npmrcArgs(info.npmrc),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function otpCommandOf(root, info, version) {
|
|
80
|
+
const tag = publishTagOf(version);
|
|
81
|
+
return [
|
|
82
|
+
`cd ${quote(root)}`,
|
|
83
|
+
'&&',
|
|
84
|
+
'npm',
|
|
85
|
+
'publish',
|
|
86
|
+
'--registry',
|
|
87
|
+
info.registry,
|
|
88
|
+
'--access',
|
|
89
|
+
info.access,
|
|
90
|
+
'--ignore-scripts',
|
|
91
|
+
...(tag === undefined ? [] : ['--tag', tag]),
|
|
92
|
+
...npmrcArgs(info.npmrc),
|
|
93
|
+
].join(' ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function publishRegistry(exec, root, info, manifest, version) {
|
|
97
|
+
const result = exec('npm', publishArgsOf(info, version), { cwd: root, capture: true, allowFailure: true });
|
|
98
|
+
if (result.status === 0) return;
|
|
99
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
100
|
+
if (/EOTP|one-time password|OTP/i.test(text)) {
|
|
101
|
+
throw new NeedsUser(
|
|
102
|
+
'otp',
|
|
103
|
+
'源要求一次性验证码。构建和门禁都过了,请在终端跑下面这条,按提示输入验证码;完了跟我说「好了」,我来核对。',
|
|
104
|
+
otpCommandOf(root, info, version),
|
|
105
|
+
{ target: info.target, name: manifest.name, version, registry: info.registry },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (info.target === 'public' && /E403|403|taken|already exists|Forbidden|cannot publish over/i.test(text)) {
|
|
109
|
+
throw new NeedsUser('name', '这个名字在 npm 上已经被占了。换一个:`@<user>/<name>` 或加一个后缀?', undefined, {
|
|
110
|
+
target: info.target,
|
|
111
|
+
name: manifest.name,
|
|
112
|
+
version,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (/E403|403|already exists|Forbidden/i.test(text)) {
|
|
116
|
+
throw new NeedsUser('scope', '团队源不让发这个包名(可能被占了,或没有权限)。让管理员确认 scope / 权限后跟我说。', undefined, {
|
|
117
|
+
target: info.target,
|
|
118
|
+
name: manifest.name,
|
|
119
|
+
version,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (/E402|402|E404|404|scope|payment|not found/i.test(text)) {
|
|
123
|
+
throw new NeedsUser('scope', '团队源要求包名带 scope。用 `@<scope>/<name>` 吗?', undefined, {
|
|
124
|
+
target: info.target,
|
|
125
|
+
name: manifest.name,
|
|
126
|
+
version,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
throw new ReleaseError(`npm publish 没过:${tailOf(text)}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function verifyRegistry(exec, info, name, version) {
|
|
133
|
+
const view = exec(
|
|
134
|
+
'npm',
|
|
135
|
+
['view', `${name}@${version}`, 'version', 'dist.tarball', '--json', '--registry', info.registry, ...npmrcArgs(info.npmrc)],
|
|
136
|
+
{ capture: true, allowFailure: true },
|
|
137
|
+
);
|
|
138
|
+
if (view.status !== 0) {
|
|
139
|
+
throw new ReleaseError(`发完了,但在源上查不到 ${name}@${version} —— 去 registry 确认一下`);
|
|
140
|
+
}
|
|
141
|
+
const packed = exec(
|
|
142
|
+
'npm',
|
|
143
|
+
['pack', `${name}@${version}`, '--dry-run', '--json', '--ignore-scripts', '--registry', info.registry, ...npmrcArgs(info.npmrc)],
|
|
144
|
+
{ capture: true, allowFailure: true },
|
|
145
|
+
);
|
|
146
|
+
if (packed.status !== 0) throw new ReleaseError(`发完了,但核不了包:${tailOf(`${packed.stdout}\n${packed.stderr}`)}`);
|
|
147
|
+
const parsed = parseJson(packed.stdout);
|
|
148
|
+
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
149
|
+
const files = Array.isArray(first?.files) ? first.files.map((one) => String(one.path ?? one)) : [];
|
|
150
|
+
if (files.length === 0) throw new ReleaseError('发完了,但 npm pack 没列出包内容');
|
|
151
|
+
checkPublished(files);
|
|
152
|
+
return files;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function packTarball(exec, info, version) {
|
|
156
|
+
mkdirSync(info.out, { recursive: true });
|
|
157
|
+
const result = exec(
|
|
158
|
+
'npm',
|
|
159
|
+
['pack', '--json', '--ignore-scripts', '--pack-destination', info.out],
|
|
160
|
+
{ capture: true, allowFailure: true },
|
|
161
|
+
);
|
|
162
|
+
if (result.status !== 0) throw new ReleaseError(`npm pack 没过:${tailOf(`${result.stdout}\n${result.stderr}`)}`);
|
|
163
|
+
const parsed = parseJson(result.stdout);
|
|
164
|
+
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
165
|
+
const filename = typeof first?.filename === 'string' ? first.filename : info.fileNameOf(version);
|
|
166
|
+
const file = join(info.out, filename);
|
|
167
|
+
if (!existsSync(file)) throw new ReleaseError(`npm pack 说生成了 ${filename},但文件不在 ${info.out}`);
|
|
168
|
+
return file;
|
|
169
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
NeedsUser,
|
|
6
|
+
ReleaseError,
|
|
7
|
+
argValue,
|
|
8
|
+
artifactsDirOf,
|
|
9
|
+
hasFlag,
|
|
10
|
+
normalizeRegistry,
|
|
11
|
+
tailOf,
|
|
12
|
+
} from './release-util.mjs';
|
|
13
|
+
|
|
14
|
+
const TARGETS = ['team', 'public', 'tarball', 'git', 'market', 'local'];
|
|
15
|
+
const PUBLIC_REGISTRIES = ['https://registry.npmjs.org/', 'https://registry.npmmirror.com/'];
|
|
16
|
+
|
|
17
|
+
function isPublicRegistry(registry) {
|
|
18
|
+
return PUBLIC_REGISTRIES.includes(normalizeRegistry(registry));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findNpmrc(root) {
|
|
22
|
+
let dir = resolve(root);
|
|
23
|
+
for (;;) {
|
|
24
|
+
const file = join(dir, '.npmrc');
|
|
25
|
+
if (existsSync(file)) return file;
|
|
26
|
+
const parent = dirname(dir);
|
|
27
|
+
if (parent === dir) return undefined;
|
|
28
|
+
dir = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasAuth(file) {
|
|
33
|
+
if (file === undefined) return false;
|
|
34
|
+
try {
|
|
35
|
+
return /(_authToken|_auth\s*=|always-auth)/i.test(readFileSync(file, 'utf8'));
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function registryInNpmrc(file, name) {
|
|
42
|
+
if (file === undefined) return '';
|
|
43
|
+
let general = '';
|
|
44
|
+
const scope = name.startsWith('@') ? name.slice(0, name.indexOf('/')) : '';
|
|
45
|
+
for (const raw of readFileSync(file, 'utf8').split('\n')) {
|
|
46
|
+
const line = raw.trim();
|
|
47
|
+
if (line === '' || line.startsWith('#') || line.startsWith(';')) continue;
|
|
48
|
+
const at = line.indexOf('=');
|
|
49
|
+
if (at === -1) continue;
|
|
50
|
+
const key = line.slice(0, at).trim();
|
|
51
|
+
const value = normalizeRegistry(line.slice(at + 1).trim());
|
|
52
|
+
if (value === '') continue;
|
|
53
|
+
if (scope !== '' && key === `${scope}:registry`) return value;
|
|
54
|
+
if (key === 'registry') general = value;
|
|
55
|
+
}
|
|
56
|
+
return general;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function configuredRegistry(root, manifest) {
|
|
60
|
+
const npmrc = findNpmrc(root);
|
|
61
|
+
return (
|
|
62
|
+
normalizeRegistry(process.env.DSH_WB_PUBLISH_REGISTRY) ||
|
|
63
|
+
normalizeRegistry(manifest.publishConfig?.registry) ||
|
|
64
|
+
registryInNpmrc(npmrc, String(manifest.name ?? ''))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function targetOf(root, manifest, args, indexPlan) {
|
|
69
|
+
const asked = argValue(args, '--target');
|
|
70
|
+
if (asked !== undefined) {
|
|
71
|
+
if (!TARGETS.includes(asked)) throw new ReleaseError(`--target 只认 ${TARGETS.join(' / ')}`);
|
|
72
|
+
return asked;
|
|
73
|
+
}
|
|
74
|
+
if (hasFlag(args, '--public')) return 'public';
|
|
75
|
+
if (hasFlag(args, '--git') || argValue(args, '--git') !== undefined) return 'git';
|
|
76
|
+
if (argValue(args, '--out') !== undefined) return 'tarball';
|
|
77
|
+
const flagged = normalizeRegistry(argValue(args, '--registry'));
|
|
78
|
+
if (flagged !== '') return isPublicRegistry(flagged) ? 'public' : 'team';
|
|
79
|
+
if (indexPlan?.policy === 'artifacts-in-repo') return 'market';
|
|
80
|
+
const configured = configuredRegistry(root, manifest);
|
|
81
|
+
if (configured !== '') return isPublicRegistry(configured) ? 'public' : 'team';
|
|
82
|
+
return 'tarball';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function licenseFileOf(root) {
|
|
86
|
+
for (const name of ['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING']) {
|
|
87
|
+
if (existsSync(join(root, name))) return name;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function licenseOf(root, manifest, target, args) {
|
|
93
|
+
const given = argValue(args, '--license') ?? manifest.license;
|
|
94
|
+
if (typeof given === 'string' && given.trim() !== '') return given.trim();
|
|
95
|
+
const file = licenseFileOf(root);
|
|
96
|
+
if (file !== undefined) return `SEE LICENSE IN ${file}`;
|
|
97
|
+
if (target === 'public') {
|
|
98
|
+
throw new NeedsUser('license', '公开发布需要一个许可证。用 MIT、Apache-2.0,还是仓库里已有的 LICENSE?', undefined, {
|
|
99
|
+
target,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (target === 'team') return 'UNLICENSED';
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function repositoryOf(root, exec) {
|
|
107
|
+
const result = exec('git', ['-C', root, 'remote', 'get-url', 'origin'], { capture: true, allowFailure: true });
|
|
108
|
+
const url = result.status === 0 ? result.stdout.trim() : '';
|
|
109
|
+
return url === '' ? undefined : { type: 'git', url };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function loginCommandOf(registry) {
|
|
113
|
+
return `npm login --registry ${registry}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function npmrcArgs(npmrc) {
|
|
117
|
+
return npmrc === undefined ? [] : ['--userconfig', npmrc];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function checkLogin(exec, info) {
|
|
121
|
+
const argv = ['whoami', '--registry', info.registry, ...npmrcArgs(info.npmrc)];
|
|
122
|
+
const result = exec('npm', argv, { capture: true, allowFailure: true });
|
|
123
|
+
if (result.status === 0) return;
|
|
124
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
125
|
+
if (/E404|404|not found/i.test(text)) return;
|
|
126
|
+
if (/ENEEDAUTH|not logged in|401|Unauthorized|E401|E403|403/i.test(text)) {
|
|
127
|
+
throw new NeedsUser(
|
|
128
|
+
'login',
|
|
129
|
+
`${info.target === 'public' ? 'npm' : '团队源'}还没登录。请在终端跑 \`${loginCommandOf(info.registry)}\`,走完登录/验证器再跟我说「好了」,我接着发。`,
|
|
130
|
+
loginCommandOf(info.registry),
|
|
131
|
+
{ target: info.target, registry: info.registry },
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
throw new ReleaseError(`连不上源(${info.registry}):${tailOf(text)}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function registryInfoOf(root, manifest, args, target, fallbackRegistry) {
|
|
138
|
+
const name = String(manifest.name ?? '');
|
|
139
|
+
const askedNpmrc = argValue(args, '--npmrc');
|
|
140
|
+
const sourceNpmrc = askedNpmrc === undefined ? findNpmrc(root) : resolve(askedNpmrc);
|
|
141
|
+
if (askedNpmrc !== undefined && !existsSync(sourceNpmrc)) {
|
|
142
|
+
throw new ReleaseError(`--npmrc 指的文件不存在:${sourceNpmrc}`);
|
|
143
|
+
}
|
|
144
|
+
const npmrc = askedNpmrc !== undefined ? sourceNpmrc : hasAuth(sourceNpmrc) ? sourceNpmrc : undefined;
|
|
145
|
+
const flagged = normalizeRegistry(argValue(args, '--registry'));
|
|
146
|
+
let registry;
|
|
147
|
+
if (target === 'public') {
|
|
148
|
+
registry = flagged || 'https://registry.npmjs.org/';
|
|
149
|
+
if (!isPublicRegistry(registry)) {
|
|
150
|
+
throw new ReleaseError('public 目标要公网源;团队私有源用 --target team');
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
registry =
|
|
154
|
+
flagged ||
|
|
155
|
+
normalizeRegistry(process.env.DSH_WB_PUBLISH_REGISTRY) ||
|
|
156
|
+
normalizeRegistry(manifest.publishConfig?.registry) ||
|
|
157
|
+
normalizeRegistry(fallbackRegistry) ||
|
|
158
|
+
registryInNpmrc(sourceNpmrc, name);
|
|
159
|
+
if (registry === '') {
|
|
160
|
+
throw new NeedsUser(
|
|
161
|
+
'registry',
|
|
162
|
+
'这个工坊还没配团队源。让公司把 registry 和 token 写进 `.npmrc`;或者我先给你打一个安装包。',
|
|
163
|
+
undefined,
|
|
164
|
+
{ target },
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (isPublicRegistry(registry)) {
|
|
168
|
+
throw new ReleaseError('这个源是公共 npm —— 用 --target public 明确公开发布');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const access = argValue(args, '--access') ?? manifest.publishConfig?.access ?? (target === 'public' ? 'public' : 'restricted');
|
|
172
|
+
if (access !== 'restricted' && access !== 'public') {
|
|
173
|
+
throw new ReleaseError(`--access 只认 restricted / public(收到「${access}」)`);
|
|
174
|
+
}
|
|
175
|
+
if (target === 'public' && access !== 'public') throw new ReleaseError('public 目标只接受 --access public');
|
|
176
|
+
return { target, kind: 'registry', name, registry, access, npmrc };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function tarballInfoOf(root, manifest, args, home) {
|
|
180
|
+
const out = resolve(argValue(args, '--out') ?? artifactsDirOf(home, String(manifest.dsh?.workbench?.unit ?? '')));
|
|
181
|
+
const fileNameOf = (version) => `${String(manifest.name).replace(/^@/, '').replace(/[/\\]/g, '-')}-${version}.tgz`;
|
|
182
|
+
const fileOf = (version) => join(out, fileNameOf(version));
|
|
183
|
+
return {
|
|
184
|
+
target: 'tarball',
|
|
185
|
+
kind: 'tarball',
|
|
186
|
+
out,
|
|
187
|
+
fileNameOf,
|
|
188
|
+
fileOf,
|
|
189
|
+
exists: (version) => existsSync(fileOf(version)),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function gitInfoOf(root, manifest, args, exec) {
|
|
194
|
+
const unit = String(manifest.dsh?.workbench?.unit ?? '');
|
|
195
|
+
const top = exec('git', ['-C', root, 'rev-parse', '--show-toplevel'], { capture: true, allowFailure: true });
|
|
196
|
+
if (top.status !== 0) {
|
|
197
|
+
throw new NeedsUser(
|
|
198
|
+
'git-remote',
|
|
199
|
+
'这个模块目录还不是 git 仓库。先给它一个仓库/配好权限,或改打一个安装包。',
|
|
200
|
+
undefined,
|
|
201
|
+
{ target: 'git' },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const toplevel = top.stdout.trim();
|
|
205
|
+
const branchResult = exec('git', ['-C', toplevel, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
206
|
+
capture: true,
|
|
207
|
+
allowFailure: true,
|
|
208
|
+
});
|
|
209
|
+
const branch = branchResult.status === 0 ? branchResult.stdout.trim() : '';
|
|
210
|
+
if (branch === '' || branch === 'HEAD') {
|
|
211
|
+
throw new NeedsUser('git-branch', '这个仓库现在不在一个分支上(detached HEAD)。先切回一个分支,再发。', undefined, {
|
|
212
|
+
target: 'git',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const remote = argValue(args, '--git') ?? 'origin';
|
|
216
|
+
const remoteResult = exec('git', ['-C', toplevel, 'remote', 'get-url', remote], { capture: true, allowFailure: true });
|
|
217
|
+
if (remoteResult.status !== 0 || remoteResult.stdout.trim() === '') {
|
|
218
|
+
throw new NeedsUser('git-remote', `这个仓库没有可推送的远端 ${remote}。先加一个远端,或改打一个安装包。`, undefined, {
|
|
219
|
+
target: 'git',
|
|
220
|
+
remote,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const url = remoteResult.stdout.trim();
|
|
224
|
+
const packagePath = relative(toplevel, root).split(sep).join('/') || '.';
|
|
225
|
+
const status = exec('git', ['-C', toplevel, 'status', '--porcelain', '--untracked-files=all'], {
|
|
226
|
+
capture: true,
|
|
227
|
+
allowFailure: true,
|
|
228
|
+
});
|
|
229
|
+
if (status.status !== 0) throw new ReleaseError(`读不了 git 状态:${tailOf(status.stderr)}`);
|
|
230
|
+
const prefix = packagePath === '.' ? '' : `${packagePath}/`;
|
|
231
|
+
const allowed = ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'CHANGELOG.md', 'design.json'];
|
|
232
|
+
const dirty = status.stdout
|
|
233
|
+
.trim()
|
|
234
|
+
.split('\n')
|
|
235
|
+
.filter((line) => line.trim() !== '')
|
|
236
|
+
.map((line) => line.slice(3).trim())
|
|
237
|
+
.filter((path) => {
|
|
238
|
+
if (!path.startsWith(prefix)) return true;
|
|
239
|
+
const rel = path.slice(prefix.length);
|
|
240
|
+
return !allowed.some((one) => rel === one || rel.startsWith(`${one}/`));
|
|
241
|
+
});
|
|
242
|
+
if (dirty.length > 0) {
|
|
243
|
+
throw new NeedsUser(
|
|
244
|
+
'git-dirty',
|
|
245
|
+
`工作区还有别的未提交改动(${dirty.slice(0, 3).join('、')})。先提交/暂存,再发;或改用安装包。`,
|
|
246
|
+
undefined,
|
|
247
|
+
{ target: 'git', paths: dirty.slice(0, 3) },
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const tagOf = (version) => `${unit}@${version}`;
|
|
251
|
+
const exists = (version) =>
|
|
252
|
+
exec('git', ['-C', toplevel, 'rev-parse', '-q', '--verify', `refs/tags/${tagOf(version)}`], {
|
|
253
|
+
capture: true,
|
|
254
|
+
allowFailure: true,
|
|
255
|
+
}).status === 0;
|
|
256
|
+
return { target: 'git', kind: 'git', toplevel, branch, remote, url, packagePath, tagOf, exists };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function targetInfoOf(root, manifest, args, home, exec, target, fallbackRegistry) {
|
|
260
|
+
if (target === 'team' || target === 'public') return registryInfoOf(root, manifest, args, target, fallbackRegistry);
|
|
261
|
+
if (target === 'tarball') return tarballInfoOf(root, manifest, args, home);
|
|
262
|
+
if (target === 'git') return gitInfoOf(root, manifest, args, exec);
|
|
263
|
+
return { target: 'local', kind: 'local' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export { TARGETS };
|
package/src/release-util.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
2
|
+
import { homedir, tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { execaSync } from 'execa';
|
|
5
5
|
import semver from 'semver';
|
|
@@ -53,6 +53,29 @@ export function tailOf(text) {
|
|
|
53
53
|
.join(' | ');
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export function argValue(args, flag) {
|
|
57
|
+
const at = args.indexOf(flag);
|
|
58
|
+
return at === -1 ? undefined : args[at + 1];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hasFlag(args, flag) {
|
|
62
|
+
return args.includes(flag);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function normalizeRegistry(url) {
|
|
66
|
+
const value = String(url ?? '').trim();
|
|
67
|
+
if (value === '') return '';
|
|
68
|
+
return value.endsWith('/') ? value : `${value}/`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function dshHomeOf(home) {
|
|
72
|
+
return home ?? process.env.DSH_HOME?.trim() ?? join(homedir(), '.dsh');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function artifactsDirOf(home, unit) {
|
|
76
|
+
return join(dshHomeOf(home), 'workbench', 'artifacts', unit);
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
export function publishTagOf(version) {
|
|
57
80
|
const prerelease = semver.prerelease(String(version ?? '').trim());
|
|
58
81
|
if (prerelease === null) return undefined;
|
package/src/release.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
5
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
6
5
|
import semver from 'semver';
|
|
@@ -19,10 +18,38 @@ import {
|
|
|
19
18
|
} from './release-journal.mjs';
|
|
20
19
|
import { emit, installOf, marketInstallOf, stateOf } from './release-report.mjs';
|
|
21
20
|
import { authorChangeOf, changelogGateOf } from './release-changelog.mjs';
|
|
22
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
NeedsUser,
|
|
23
|
+
ReleaseError,
|
|
24
|
+
ResumableError,
|
|
25
|
+
argValue,
|
|
26
|
+
defaultExec,
|
|
27
|
+
dshHomeOf,
|
|
28
|
+
hasFlag,
|
|
29
|
+
tailOf,
|
|
30
|
+
} from './release-util.mjs';
|
|
31
|
+
import {
|
|
32
|
+
TARGETS,
|
|
33
|
+
checkLogin,
|
|
34
|
+
licenseOf,
|
|
35
|
+
repositoryOf,
|
|
36
|
+
targetInfoOf,
|
|
37
|
+
targetOf,
|
|
38
|
+
} from './release-target.mjs';
|
|
39
|
+
import {
|
|
40
|
+
checkGitFiles,
|
|
41
|
+
checkPublished,
|
|
42
|
+
localFiles,
|
|
43
|
+
packTarball,
|
|
44
|
+
publishRegistry,
|
|
45
|
+
registryExists,
|
|
46
|
+
verifyRegistry,
|
|
47
|
+
} from './release-publish.mjs';
|
|
23
48
|
import {
|
|
24
49
|
MarketIndexError,
|
|
50
|
+
identityOf,
|
|
25
51
|
marketVersionExists,
|
|
52
|
+
refreshMarketEntry,
|
|
26
53
|
resolveIndexPlan,
|
|
27
54
|
updateMarketIndex,
|
|
28
55
|
verifyMarketArtifact,
|
|
@@ -32,34 +59,6 @@ export { NeedsUser, ReleaseError, ResumableError };
|
|
|
32
59
|
|
|
33
60
|
const CLI = fileURLToPath(new URL('../bin/dsh-wb-module.mjs', import.meta.url));
|
|
34
61
|
const SCRIPTS = ['typecheck', 'test', 'selfcheck'];
|
|
35
|
-
const TARGETS = ['team', 'public', 'tarball', 'git', 'market', 'local'];
|
|
36
|
-
const PUBLIC_REGISTRIES = ['https://registry.npmjs.org/', 'https://registry.npmmirror.com/'];
|
|
37
|
-
const REQUIRED_FILES = ['dist/host.js', 'client.js', 'cordis.patch.yml', 'package.json', 'CHANGELOG.md'];
|
|
38
|
-
|
|
39
|
-
function argValue(args, flag) {
|
|
40
|
-
const at = args.indexOf(flag);
|
|
41
|
-
return at === -1 ? undefined : args[at + 1];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function hasFlag(args, flag) {
|
|
45
|
-
return args.includes(flag);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function parseJson(text) {
|
|
49
|
-
const raw = String(text ?? '');
|
|
50
|
-
const start = raw.search(/[[{]/);
|
|
51
|
-
if (start === -1) return undefined;
|
|
52
|
-
try {
|
|
53
|
-
return JSON.parse(raw.slice(start));
|
|
54
|
-
} catch {
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function quote(value) {
|
|
60
|
-
const text = String(value);
|
|
61
|
-
return /[\s"'$`\\]/.test(text) ? `'${text.replace(/'/g, `'\\''`)}'` : text;
|
|
62
|
-
}
|
|
63
62
|
|
|
64
63
|
function readManifest(root) {
|
|
65
64
|
const file = join(root, 'package.json');
|
|
@@ -80,115 +79,6 @@ function writeManifest(root, manifest) {
|
|
|
80
79
|
writeFileSync(join(root, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
function dshHomeOf(home) {
|
|
84
|
-
return home ?? process.env.DSH_HOME?.trim() ?? join(homedir(), '.dsh');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function artifactsDirOf(home, unit) {
|
|
88
|
-
return join(dshHomeOf(home), 'workbench', 'artifacts', unit);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function normalizeRegistry(url) {
|
|
92
|
-
const value = String(url ?? '').trim();
|
|
93
|
-
if (value === '') return '';
|
|
94
|
-
return value.endsWith('/') ? value : `${value}/`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function isPublicRegistry(registry) {
|
|
98
|
-
return PUBLIC_REGISTRIES.includes(normalizeRegistry(registry));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function findNpmrc(root) {
|
|
102
|
-
let dir = resolve(root);
|
|
103
|
-
for (;;) {
|
|
104
|
-
const file = join(dir, '.npmrc');
|
|
105
|
-
if (existsSync(file)) return file;
|
|
106
|
-
const parent = dirname(dir);
|
|
107
|
-
if (parent === dir) return undefined;
|
|
108
|
-
dir = parent;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function hasAuth(file) {
|
|
113
|
-
if (file === undefined) return false;
|
|
114
|
-
try {
|
|
115
|
-
return /(_authToken|_auth\s*=|always-auth)/i.test(readFileSync(file, 'utf8'));
|
|
116
|
-
} catch {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function registryInNpmrc(file, name) {
|
|
122
|
-
if (file === undefined) return '';
|
|
123
|
-
let general = '';
|
|
124
|
-
const scope = name.startsWith('@') ? name.slice(0, name.indexOf('/')) : '';
|
|
125
|
-
for (const raw of readFileSync(file, 'utf8').split('\n')) {
|
|
126
|
-
const line = raw.trim();
|
|
127
|
-
if (line === '' || line.startsWith('#') || line.startsWith(';')) continue;
|
|
128
|
-
const at = line.indexOf('=');
|
|
129
|
-
if (at === -1) continue;
|
|
130
|
-
const key = line.slice(0, at).trim();
|
|
131
|
-
const value = normalizeRegistry(line.slice(at + 1).trim());
|
|
132
|
-
if (value === '') continue;
|
|
133
|
-
if (scope !== '' && key === `${scope}:registry`) return value;
|
|
134
|
-
if (key === 'registry') general = value;
|
|
135
|
-
}
|
|
136
|
-
return general;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function configuredRegistry(root, manifest) {
|
|
140
|
-
const npmrc = findNpmrc(root);
|
|
141
|
-
return (
|
|
142
|
-
normalizeRegistry(process.env.DSH_WB_PUBLISH_REGISTRY) ||
|
|
143
|
-
normalizeRegistry(manifest.publishConfig?.registry) ||
|
|
144
|
-
registryInNpmrc(npmrc, String(manifest.name ?? ''))
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function targetOf(root, manifest, args, indexPlan) {
|
|
149
|
-
const asked = argValue(args, '--target');
|
|
150
|
-
if (asked !== undefined) {
|
|
151
|
-
if (!TARGETS.includes(asked)) throw new ReleaseError(`--target 只认 ${TARGETS.join(' / ')}`);
|
|
152
|
-
return asked;
|
|
153
|
-
}
|
|
154
|
-
if (hasFlag(args, '--public')) return 'public';
|
|
155
|
-
if (hasFlag(args, '--git') || argValue(args, '--git') !== undefined) return 'git';
|
|
156
|
-
if (argValue(args, '--out') !== undefined) return 'tarball';
|
|
157
|
-
const flagged = normalizeRegistry(argValue(args, '--registry'));
|
|
158
|
-
if (flagged !== '') return isPublicRegistry(flagged) ? 'public' : 'team';
|
|
159
|
-
if (indexPlan?.policy === 'artifacts-in-repo') return 'market';
|
|
160
|
-
const configured = configuredRegistry(root, manifest);
|
|
161
|
-
if (configured !== '') return isPublicRegistry(configured) ? 'public' : 'team';
|
|
162
|
-
return 'tarball';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function licenseFileOf(root) {
|
|
166
|
-
for (const name of ['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING']) {
|
|
167
|
-
if (existsSync(join(root, name))) return name;
|
|
168
|
-
}
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function licenseOf(root, manifest, target, args) {
|
|
173
|
-
const given = argValue(args, '--license') ?? manifest.license;
|
|
174
|
-
if (typeof given === 'string' && given.trim() !== '') return given.trim();
|
|
175
|
-
const file = licenseFileOf(root);
|
|
176
|
-
if (file !== undefined) return `SEE LICENSE IN ${file}`;
|
|
177
|
-
if (target === 'public') {
|
|
178
|
-
throw new NeedsUser('license', '公开发布需要一个许可证。用 MIT、Apache-2.0,还是仓库里已有的 LICENSE?', undefined, {
|
|
179
|
-
target,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
if (target === 'team') return 'UNLICENSED';
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function repositoryOf(root, exec) {
|
|
187
|
-
const result = exec('git', ['-C', root, 'remote', 'get-url', 'origin'], { capture: true, allowFailure: true });
|
|
188
|
-
const url = result.status === 0 ? result.stdout.trim() : '';
|
|
189
|
-
return url === '' ? undefined : { type: 'git', url };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
82
|
function bumpVersion(version, kind) {
|
|
193
83
|
const next = semver.inc(String(version), kind);
|
|
194
84
|
if (next === null) throw new ReleaseError(`当前版本号读不出来:${String(version)}`);
|
|
@@ -212,160 +102,6 @@ function nextVersion(current, asked, exists) {
|
|
|
212
102
|
throw new ReleaseError('版本号没法自动加 —— 手动给 --version');
|
|
213
103
|
}
|
|
214
104
|
|
|
215
|
-
function loginCommandOf(registry) {
|
|
216
|
-
return `npm login --registry ${registry}`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function npmrcArgs(npmrc) {
|
|
220
|
-
return npmrc === undefined ? [] : ['--userconfig', npmrc];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function checkLogin(exec, info) {
|
|
224
|
-
const argv = ['whoami', '--registry', info.registry, ...npmrcArgs(info.npmrc)];
|
|
225
|
-
const result = exec('npm', argv, { capture: true, allowFailure: true });
|
|
226
|
-
if (result.status === 0) return;
|
|
227
|
-
const text = `${result.stdout}\n${result.stderr}`;
|
|
228
|
-
if (/E404|404|not found/i.test(text)) return;
|
|
229
|
-
if (/ENEEDAUTH|not logged in|401|Unauthorized|E401|E403|403/i.test(text)) {
|
|
230
|
-
throw new NeedsUser(
|
|
231
|
-
'login',
|
|
232
|
-
`${info.target === 'public' ? 'npm' : '团队源'}还没登录。请在终端跑 \`${loginCommandOf(info.registry)}\`,走完登录/验证器再跟我说「好了」,我接着发。`,
|
|
233
|
-
loginCommandOf(info.registry),
|
|
234
|
-
{ target: info.target, registry: info.registry },
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
throw new ReleaseError(`连不上源(${info.registry}):${tailOf(text)}`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function registryInfoOf(root, manifest, args, target, fallbackRegistry) {
|
|
241
|
-
const name = String(manifest.name ?? '');
|
|
242
|
-
const askedNpmrc = argValue(args, '--npmrc');
|
|
243
|
-
const sourceNpmrc = askedNpmrc === undefined ? findNpmrc(root) : resolve(askedNpmrc);
|
|
244
|
-
if (askedNpmrc !== undefined && !existsSync(sourceNpmrc)) {
|
|
245
|
-
throw new ReleaseError(`--npmrc 指的文件不存在:${sourceNpmrc}`);
|
|
246
|
-
}
|
|
247
|
-
const npmrc = askedNpmrc !== undefined ? sourceNpmrc : hasAuth(sourceNpmrc) ? sourceNpmrc : undefined;
|
|
248
|
-
const flagged = normalizeRegistry(argValue(args, '--registry'));
|
|
249
|
-
let registry;
|
|
250
|
-
if (target === 'public') {
|
|
251
|
-
registry = flagged || 'https://registry.npmjs.org/';
|
|
252
|
-
if (!isPublicRegistry(registry)) {
|
|
253
|
-
throw new ReleaseError('public 目标要公网源;团队私有源用 --target team');
|
|
254
|
-
}
|
|
255
|
-
} else {
|
|
256
|
-
registry =
|
|
257
|
-
flagged ||
|
|
258
|
-
normalizeRegistry(process.env.DSH_WB_PUBLISH_REGISTRY) ||
|
|
259
|
-
normalizeRegistry(manifest.publishConfig?.registry) ||
|
|
260
|
-
normalizeRegistry(fallbackRegistry) ||
|
|
261
|
-
registryInNpmrc(sourceNpmrc, name);
|
|
262
|
-
if (registry === '') {
|
|
263
|
-
throw new NeedsUser(
|
|
264
|
-
'registry',
|
|
265
|
-
'这个工坊还没配团队源。让公司把 registry 和 token 写进 `.npmrc`;或者我先给你打一个安装包。',
|
|
266
|
-
undefined,
|
|
267
|
-
{ target },
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
if (isPublicRegistry(registry)) {
|
|
271
|
-
throw new ReleaseError('这个源是公共 npm —— 用 --target public 明确公开发布');
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
const access = argValue(args, '--access') ?? manifest.publishConfig?.access ?? (target === 'public' ? 'public' : 'restricted');
|
|
275
|
-
if (access !== 'restricted' && access !== 'public') {
|
|
276
|
-
throw new ReleaseError(`--access 只认 restricted / public(收到「${access}」)`);
|
|
277
|
-
}
|
|
278
|
-
if (target === 'public' && access !== 'public') throw new ReleaseError('public 目标只接受 --access public');
|
|
279
|
-
return { target, kind: 'registry', name, registry, access, npmrc };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function tarballInfoOf(root, manifest, args, home) {
|
|
283
|
-
const out = resolve(argValue(args, '--out') ?? artifactsDirOf(home, String(manifest.dsh?.workbench?.unit ?? '')));
|
|
284
|
-
const fileNameOf = (version) => `${String(manifest.name).replace(/^@/, '').replace(/[/\\]/g, '-')}-${version}.tgz`;
|
|
285
|
-
const fileOf = (version) => join(out, fileNameOf(version));
|
|
286
|
-
return {
|
|
287
|
-
target: 'tarball',
|
|
288
|
-
kind: 'tarball',
|
|
289
|
-
out,
|
|
290
|
-
fileNameOf,
|
|
291
|
-
fileOf,
|
|
292
|
-
exists: (version) => existsSync(fileOf(version)),
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function gitInfoOf(root, manifest, args, exec) {
|
|
297
|
-
const unit = String(manifest.dsh?.workbench?.unit ?? '');
|
|
298
|
-
const top = exec('git', ['-C', root, 'rev-parse', '--show-toplevel'], { capture: true, allowFailure: true });
|
|
299
|
-
if (top.status !== 0) {
|
|
300
|
-
throw new NeedsUser(
|
|
301
|
-
'git-remote',
|
|
302
|
-
'这个模块目录还不是 git 仓库。先给它一个仓库/配好权限,或改打一个安装包。',
|
|
303
|
-
undefined,
|
|
304
|
-
{ target: 'git' },
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
const toplevel = top.stdout.trim();
|
|
308
|
-
const branchResult = exec('git', ['-C', toplevel, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
309
|
-
capture: true,
|
|
310
|
-
allowFailure: true,
|
|
311
|
-
});
|
|
312
|
-
const branch = branchResult.status === 0 ? branchResult.stdout.trim() : '';
|
|
313
|
-
if (branch === '' || branch === 'HEAD') {
|
|
314
|
-
throw new NeedsUser('git-branch', '这个仓库现在不在一个分支上(detached HEAD)。先切回一个分支,再发。', undefined, {
|
|
315
|
-
target: 'git',
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
const remote = argValue(args, '--git') ?? 'origin';
|
|
319
|
-
const remoteResult = exec('git', ['-C', toplevel, 'remote', 'get-url', remote], { capture: true, allowFailure: true });
|
|
320
|
-
if (remoteResult.status !== 0 || remoteResult.stdout.trim() === '') {
|
|
321
|
-
throw new NeedsUser('git-remote', `这个仓库没有可推送的远端 ${remote}。先加一个远端,或改打一个安装包。`, undefined, {
|
|
322
|
-
target: 'git',
|
|
323
|
-
remote,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
const url = remoteResult.stdout.trim();
|
|
327
|
-
const packagePath = relative(toplevel, root).split(sep).join('/') || '.';
|
|
328
|
-
const status = exec('git', ['-C', toplevel, 'status', '--porcelain', '--untracked-files=all'], {
|
|
329
|
-
capture: true,
|
|
330
|
-
allowFailure: true,
|
|
331
|
-
});
|
|
332
|
-
if (status.status !== 0) throw new ReleaseError(`读不了 git 状态:${tailOf(status.stderr)}`);
|
|
333
|
-
const prefix = packagePath === '.' ? '' : `${packagePath}/`;
|
|
334
|
-
const allowed = ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'CHANGELOG.md', 'design.json'];
|
|
335
|
-
const dirty = status.stdout
|
|
336
|
-
.trim()
|
|
337
|
-
.split('\n')
|
|
338
|
-
.filter((line) => line.trim() !== '')
|
|
339
|
-
.map((line) => line.slice(3).trim())
|
|
340
|
-
.filter((path) => {
|
|
341
|
-
if (!path.startsWith(prefix)) return true;
|
|
342
|
-
const rel = path.slice(prefix.length);
|
|
343
|
-
return !allowed.some((one) => rel === one || rel.startsWith(`${one}/`));
|
|
344
|
-
});
|
|
345
|
-
if (dirty.length > 0) {
|
|
346
|
-
throw new NeedsUser(
|
|
347
|
-
'git-dirty',
|
|
348
|
-
`工作区还有别的未提交改动(${dirty.slice(0, 3).join('、')})。先提交/暂存,再发;或改用安装包。`,
|
|
349
|
-
undefined,
|
|
350
|
-
{ target: 'git', paths: dirty.slice(0, 3) },
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
const tagOf = (version) => `${unit}@${version}`;
|
|
354
|
-
const exists = (version) =>
|
|
355
|
-
exec('git', ['-C', toplevel, 'rev-parse', '-q', '--verify', `refs/tags/${tagOf(version)}`], {
|
|
356
|
-
capture: true,
|
|
357
|
-
allowFailure: true,
|
|
358
|
-
}).status === 0;
|
|
359
|
-
return { target: 'git', kind: 'git', toplevel, branch, remote, url, packagePath, tagOf, exists };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function targetInfoOf(root, manifest, args, home, exec, target, fallbackRegistry) {
|
|
363
|
-
if (target === 'team' || target === 'public') return registryInfoOf(root, manifest, args, target, fallbackRegistry);
|
|
364
|
-
if (target === 'tarball') return tarballInfoOf(root, manifest, args, home);
|
|
365
|
-
if (target === 'git') return gitInfoOf(root, manifest, args, exec);
|
|
366
|
-
return { target: 'local', kind: 'local' };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
105
|
function stage(exec, command, argv, { cwd, capture, err, label }) {
|
|
370
106
|
const result = exec(command, argv, { cwd, capture, allowFailure: true });
|
|
371
107
|
if (result.status === 0) return result;
|
|
@@ -392,167 +128,55 @@ function runGate(exec, root, options) {
|
|
|
392
128
|
for (const script of SCRIPTS) runScriptStage(exec, root, script, options);
|
|
393
129
|
}
|
|
394
130
|
|
|
395
|
-
function
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
);
|
|
401
|
-
if (result.status !== 0) throw new ReleaseError(`核不了本地包内容:${tailOf(`${result.stdout}\n${result.stderr}`)}`);
|
|
402
|
-
const parsed = parseJson(result.stdout);
|
|
403
|
-
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
404
|
-
const files = Array.isArray(first?.files) ? first.files.map((one) => String(one.path ?? one)) : [];
|
|
405
|
-
if (files.length === 0) throw new ReleaseError('npm pack 没列出包内容');
|
|
406
|
-
return files;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function checkPublished(files) {
|
|
410
|
-
const missing = REQUIRED_FILES.filter((one) => !files.includes(one));
|
|
411
|
-
if (missing.length > 0) throw new ReleaseError(`包里少了 ${missing.join(' / ')} —— 装上会起不来`);
|
|
412
|
-
const leaked = files.filter(
|
|
413
|
-
(one) => one.startsWith('src/') || one.endsWith('.npmrc') || one.startsWith('template') || one.includes('.env'),
|
|
414
|
-
);
|
|
415
|
-
if (leaked.length > 0) throw new ReleaseError(`包里混进了不该发的:${leaked.slice(0, 3).join('、')}`);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function checkGitFiles(files) {
|
|
419
|
-
const missing = REQUIRED_FILES.filter((one) => !files.includes(one));
|
|
420
|
-
if (missing.length > 0) throw new ReleaseError(`tag 里少了 ${missing.join(' / ')} —— 装上会起不来`);
|
|
421
|
-
const leaked = files.filter((one) => one.endsWith('.npmrc') || one.includes('.env'));
|
|
422
|
-
if (leaked.length > 0) throw new ReleaseError(`tag 里混进了凭据文件:${leaked.slice(0, 3).join('、')}`);
|
|
131
|
+
function gitPathsOf(info) {
|
|
132
|
+
const prefix = info.packagePath === '.' ? '' : `${info.packagePath}/`;
|
|
133
|
+
return ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'CHANGELOG.md', 'design.json']
|
|
134
|
+
.map((one) => `${prefix}${one}`)
|
|
135
|
+
.filter((one) => existsSync(join(info.toplevel, one)));
|
|
423
136
|
}
|
|
424
137
|
|
|
425
|
-
|
|
426
|
-
const argv = ['view', `${name}@${version}`, 'version', '--json', '--registry', info.registry, ...npmrcArgs(info.npmrc)];
|
|
427
|
-
const result = exec('npm', argv, { capture: true, allowFailure: true });
|
|
428
|
-
if (result.status === 0) return true;
|
|
429
|
-
const text = `${result.stdout}\n${result.stderr}`;
|
|
430
|
-
if (/E404|404|No matching version|not found|不存在/i.test(text)) return false;
|
|
431
|
-
throw new ReleaseError(`查团队源失败(${info.registry}):${tailOf(text)}`);
|
|
432
|
-
}
|
|
138
|
+
const MESSAGE_FILES = ['src/messages.ts', 'src/messages.tsx', 'src/messages.mjs', 'src/messages.js'];
|
|
433
139
|
|
|
434
|
-
function
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
'--registry',
|
|
439
|
-
info.registry,
|
|
440
|
-
'--access',
|
|
441
|
-
info.access,
|
|
442
|
-
'--ignore-scripts',
|
|
443
|
-
...(tag === undefined ? [] : ['--tag', tag]),
|
|
444
|
-
...npmrcArgs(info.npmrc),
|
|
445
|
-
];
|
|
140
|
+
function titleIn(segment, key) {
|
|
141
|
+
const quoted = `['"\`]${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"\`]`;
|
|
142
|
+
const match = new RegExp(`${quoted}\\s*:\\s*['"\`]([^'"\`]*)['"\`]`).exec(segment);
|
|
143
|
+
return match === null ? '' : match[1].trim();
|
|
446
144
|
}
|
|
447
145
|
|
|
448
|
-
function
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function publishRegistry(exec, root, info, manifest, version) {
|
|
466
|
-
const result = exec('npm', publishArgsOf(info, version), { cwd: root, capture: true, allowFailure: true });
|
|
467
|
-
if (result.status === 0) return;
|
|
468
|
-
const text = `${result.stdout}\n${result.stderr}`;
|
|
469
|
-
if (/EOTP|one-time password|OTP/i.test(text)) {
|
|
470
|
-
throw new NeedsUser(
|
|
471
|
-
'otp',
|
|
472
|
-
'源要求一次性验证码。构建和门禁都过了,请在终端跑下面这条,按提示输入验证码;完了跟我说「好了」,我来核对。',
|
|
473
|
-
otpCommandOf(root, info, version),
|
|
474
|
-
{ target: info.target, name: manifest.name, version, registry: info.registry },
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
if (info.target === 'public' && /E403|403|taken|already exists|Forbidden|cannot publish over/i.test(text)) {
|
|
478
|
-
throw new NeedsUser('name', '这个名字在 npm 上已经被占了。换一个:`@<user>/<name>` 或加一个后缀?', undefined, {
|
|
479
|
-
target: info.target,
|
|
480
|
-
name: manifest.name,
|
|
481
|
-
version,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
if (/E403|403|already exists|Forbidden/i.test(text)) {
|
|
485
|
-
throw new NeedsUser('scope', '团队源不让发这个包名(可能被占了,或没有权限)。让管理员确认 scope / 权限后跟我说。', undefined, {
|
|
486
|
-
target: info.target,
|
|
487
|
-
name: manifest.name,
|
|
488
|
-
version,
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
if (/E402|402|E404|404|scope|payment|not found/i.test(text)) {
|
|
492
|
-
throw new NeedsUser('scope', '团队源要求包名带 scope。用 `@<scope>/<name>` 吗?', undefined, {
|
|
493
|
-
target: info.target,
|
|
494
|
-
name: manifest.name,
|
|
495
|
-
version,
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
throw new ReleaseError(`npm publish 没过:${tailOf(text)}`);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function verifyRegistry(exec, info, name, version) {
|
|
502
|
-
const view = exec(
|
|
503
|
-
'npm',
|
|
504
|
-
['view', `${name}@${version}`, 'version', 'dist.tarball', '--json', '--registry', info.registry, ...npmrcArgs(info.npmrc)],
|
|
505
|
-
{ capture: true, allowFailure: true },
|
|
506
|
-
);
|
|
507
|
-
if (view.status !== 0) {
|
|
508
|
-
throw new ReleaseError(`发完了,但在源上查不到 ${name}@${version} —— 去 registry 确认一下`);
|
|
146
|
+
export function cardTitleFromMessages(root, unit) {
|
|
147
|
+
if (unit === '') return undefined;
|
|
148
|
+
const key = `${unit}.cardTitle`;
|
|
149
|
+
for (const file of MESSAGE_FILES) {
|
|
150
|
+
let text;
|
|
151
|
+
try {
|
|
152
|
+
text = readFileSync(join(root, file), 'utf8');
|
|
153
|
+
} catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const zhAt = text.search(/\bzh\s*:/);
|
|
157
|
+
const enAt = text.search(/\ben\s*:/);
|
|
158
|
+
const zh = zhAt === -1 ? '' : titleIn(text.slice(zhAt, enAt === -1 ? text.length : enAt), key);
|
|
159
|
+
const en = enAt === -1 ? '' : titleIn(text.slice(enAt), key);
|
|
160
|
+
if (zh === '' && en === '') continue;
|
|
161
|
+
return { ...(zh === '' ? {} : { zh }), ...(en === '' ? {} : { en }) };
|
|
509
162
|
}
|
|
510
|
-
|
|
511
|
-
'npm',
|
|
512
|
-
['pack', `${name}@${version}`, '--dry-run', '--json', '--ignore-scripts', '--registry', info.registry, ...npmrcArgs(info.npmrc)],
|
|
513
|
-
{ capture: true, allowFailure: true },
|
|
514
|
-
);
|
|
515
|
-
if (packed.status !== 0) throw new ReleaseError(`发完了,但核不了包:${tailOf(`${packed.stdout}\n${packed.stderr}`)}`);
|
|
516
|
-
const parsed = parseJson(packed.stdout);
|
|
517
|
-
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
518
|
-
const files = Array.isArray(first?.files) ? first.files.map((one) => String(one.path ?? one)) : [];
|
|
519
|
-
if (files.length === 0) throw new ReleaseError('发完了,但 npm pack 没列出包内容');
|
|
520
|
-
checkPublished(files);
|
|
521
|
-
return files;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function packTarball(exec, info, version) {
|
|
525
|
-
mkdirSync(info.out, { recursive: true });
|
|
526
|
-
const result = exec(
|
|
527
|
-
'npm',
|
|
528
|
-
['pack', '--json', '--ignore-scripts', '--pack-destination', info.out],
|
|
529
|
-
{ capture: true, allowFailure: true },
|
|
530
|
-
);
|
|
531
|
-
if (result.status !== 0) throw new ReleaseError(`npm pack 没过:${tailOf(`${result.stdout}\n${result.stderr}`)}`);
|
|
532
|
-
const parsed = parseJson(result.stdout);
|
|
533
|
-
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
534
|
-
const filename = typeof first?.filename === 'string' ? first.filename : info.fileNameOf(version);
|
|
535
|
-
const file = join(info.out, filename);
|
|
536
|
-
if (!existsSync(file)) throw new ReleaseError(`npm pack 说生成了 ${filename},但文件不在 ${info.out}`);
|
|
537
|
-
return file;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function gitPathsOf(info) {
|
|
541
|
-
const prefix = info.packagePath === '.' ? '' : `${info.packagePath}/`;
|
|
542
|
-
return ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'CHANGELOG.md', 'design.json']
|
|
543
|
-
.map((one) => `${prefix}${one}`)
|
|
544
|
-
.filter((one) => existsSync(join(info.toplevel, one)));
|
|
163
|
+
return undefined;
|
|
545
164
|
}
|
|
546
165
|
|
|
547
|
-
function gitCommitOf(exec, info, version, capture, progress = {}) {
|
|
166
|
+
function gitCommitOf(exec, info, version, capture, progress = {}, identity) {
|
|
548
167
|
const add = exec('git', ['-C', info.toplevel, 'add', '-f', '--', ...gitPathsOf(info)], {
|
|
549
168
|
capture,
|
|
550
169
|
allowFailure: true,
|
|
551
170
|
});
|
|
552
171
|
if (add.status !== 0) throw new ReleaseError(`git add 没过:${tailOf(`${add.stdout}\n${add.stderr}`)}`);
|
|
172
|
+
const who = [];
|
|
173
|
+
if (identity !== undefined) {
|
|
174
|
+
who.push('-c', `user.name=${identity.name}`);
|
|
175
|
+
if (identity.email !== undefined) who.push('-c', `user.email=${identity.email}`);
|
|
176
|
+
}
|
|
553
177
|
const commit = exec(
|
|
554
178
|
'git',
|
|
555
|
-
['-C', info.toplevel, 'commit', '-m', `chore(release): ${info.tagOf(version)}`],
|
|
179
|
+
['-C', info.toplevel, ...who, 'commit', '-m', `chore(release): ${info.tagOf(version)}`],
|
|
556
180
|
{ capture, allowFailure: true },
|
|
557
181
|
);
|
|
558
182
|
if (commit.status !== 0) throw new ReleaseError(`git commit 没过:${tailOf(`${commit.stdout}\n${commit.stderr}`)}`);
|
|
@@ -681,6 +305,40 @@ export async function runRelease({
|
|
|
681
305
|
throw new ReleaseError('这个索引源是 catalog-only:它只收目录、不放产物。用 --index-policy artifacts-in-repo 或换一个可写仓库');
|
|
682
306
|
}
|
|
683
307
|
}
|
|
308
|
+
if (hasFlag(args, '--refresh')) {
|
|
309
|
+
if (dryRun) throw new ReleaseError('--refresh 没有预览:它只改索引里的标题 / 作者 / 说明');
|
|
310
|
+
if (target !== 'market') throw new ReleaseError('--refresh 只对市场索引有意义(--target market)');
|
|
311
|
+
if (indexPlan === undefined) {
|
|
312
|
+
throw new ReleaseError('没有可写的 git 索引源 —— 用 --index <仓库> 指定,或先在「模块市场 → 发现」里初始化索引');
|
|
313
|
+
}
|
|
314
|
+
const cardTitle = cardTitleFromMessages(root, unit);
|
|
315
|
+
const titled = cardTitle === undefined ? manifest : { ...manifest, title: cardTitle };
|
|
316
|
+
const result = refreshMarketEntry({ plan: indexPlan, exec, home, manifest: titled });
|
|
317
|
+
if (result.updated !== true && result.reason !== undefined) {
|
|
318
|
+
throw new ReleaseError(`刷新市集元数据没走通:${result.reason}`);
|
|
319
|
+
}
|
|
320
|
+
const summary = {
|
|
321
|
+
ok: true,
|
|
322
|
+
target: 'market',
|
|
323
|
+
refresh: true,
|
|
324
|
+
name: manifest.name,
|
|
325
|
+
version: result.version ?? manifest.version,
|
|
326
|
+
refreshed: result.updated === true,
|
|
327
|
+
market: {
|
|
328
|
+
url: indexPlan.source.url,
|
|
329
|
+
...(result.branch === undefined ? {} : { branch: result.branch }),
|
|
330
|
+
...(result.commit === undefined ? {} : { commit: result.commit }),
|
|
331
|
+
},
|
|
332
|
+
changes: result.updated === true ? ['市集元数据已刷新'] : [],
|
|
333
|
+
install: '',
|
|
334
|
+
text:
|
|
335
|
+
result.updated === true
|
|
336
|
+
? `已刷新 ${manifest.name} 的市集元数据(版本 ${result.version ?? manifest.version} 不变)`
|
|
337
|
+
: `${manifest.name} 的市集元数据本来就是这样,没动`,
|
|
338
|
+
};
|
|
339
|
+
emit(out, json, summary);
|
|
340
|
+
return summary;
|
|
341
|
+
}
|
|
684
342
|
const authored = authorChangeOf(root, manifest, exec);
|
|
685
343
|
const authorChange = authored.change;
|
|
686
344
|
if (!verify && !dryRun && authored.author !== undefined) {
|
|
@@ -971,10 +629,12 @@ export async function runRelease({
|
|
|
971
629
|
if (!resumeMatchesTree) runCliStage(exec, root, ['check'], stageOptions);
|
|
972
630
|
if (tracking) advanceRun(home, unit, { phase: 'version', version });
|
|
973
631
|
const next = { ...manifest, version };
|
|
632
|
+
const cardTitle = cardTitleFromMessages(root, unit);
|
|
633
|
+
const titled = cardTitle === undefined ? next : { ...next, title: cardTitle };
|
|
974
634
|
const index = updateMarketIndex({
|
|
975
635
|
plan: indexPlan,
|
|
976
636
|
root,
|
|
977
|
-
manifest:
|
|
637
|
+
manifest: titled,
|
|
978
638
|
unit,
|
|
979
639
|
version,
|
|
980
640
|
publish: {},
|
|
@@ -1102,7 +762,7 @@ export async function runRelease({
|
|
|
1102
762
|
writeState(home, unit, stateOf(info, next, version, 'intent'));
|
|
1103
763
|
const progress = { committed: false };
|
|
1104
764
|
try {
|
|
1105
|
-
const commit = gitCommitOf(exec, info, version, capture, progress);
|
|
765
|
+
const commit = gitCommitOf(exec, info, version, capture, progress, identityOf(next));
|
|
1106
766
|
writeState(home, unit, { ...stateOf(info, next, version, 'committed'), commit });
|
|
1107
767
|
gitPushOf(exec, info, version, capture);
|
|
1108
768
|
const checked = verifyGit(exec, info, version);
|