@dsh-workbench/module-tools 1.0.0-beta.0 → 1.0.0
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 +6 -2
- package/src/changelog.mjs +3 -4
- package/src/checks/contracts.mjs +20 -2
- package/src/checks/style.mjs +24 -0
- package/src/market-index.mjs +128 -9
- package/src/publish.mjs +5 -1
- package/src/release-publish.mjs +169 -0
- package/src/release-target.mjs +266 -0
- package/src/release-util.mjs +31 -1
- package/src/release.mjs +108 -449
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/module-tools",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,9 +30,10 @@
|
|
|
30
30
|
"eslint-plugin-react": "^7.37.5",
|
|
31
31
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
32
32
|
"execa": "^10.2.0",
|
|
33
|
+
"semver": "^7.8.5",
|
|
33
34
|
"typescript": "5.9.3",
|
|
34
35
|
"typescript-eslint": "^8.71.0",
|
|
35
|
-
"@dsh-workbench/shared": "1.0.0
|
|
36
|
+
"@dsh-workbench/shared": "1.0.0"
|
|
36
37
|
},
|
|
37
38
|
"repository": {
|
|
38
39
|
"type": "git",
|
|
@@ -43,6 +44,9 @@
|
|
|
43
44
|
"bugs": {
|
|
44
45
|
"url": "https://github.com/novaeth/dsh-workbench/issues"
|
|
45
46
|
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/semver": "^7.8.0"
|
|
49
|
+
},
|
|
46
50
|
"scripts": {
|
|
47
51
|
"lint": "eslint src bin scripts",
|
|
48
52
|
"arch": "arch-guard",
|
package/src/changelog.mjs
CHANGED
|
@@ -9,10 +9,9 @@ import {
|
|
|
9
9
|
changelogHasEntry,
|
|
10
10
|
changelogSkeleton,
|
|
11
11
|
changelogVersion,
|
|
12
|
+
isValidVersion,
|
|
12
13
|
} from '@dsh-workbench/shared/host';
|
|
13
14
|
|
|
14
|
-
const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
15
|
-
|
|
16
15
|
const USAGE = `用法:
|
|
17
16
|
changelog init 建一份 ${CHANGELOG_FILE}(已有就不动)
|
|
18
17
|
changelog add <版本> [--type ${CHANGELOG_KINDS.join('|')}] [--note <文字>] [--date <日期>]
|
|
@@ -89,7 +88,7 @@ export function addChangelog(root, { version, kind = 'changed', note, date }) {
|
|
|
89
88
|
throw new ChangelogError(`--type 只认 ${CHANGELOG_KINDS.join(' / ')}(收到「${kind}」)`);
|
|
90
89
|
}
|
|
91
90
|
const wanted = changelogVersion(version === undefined || version === '' ? manifestVersionOf(root) : version);
|
|
92
|
-
if (!
|
|
91
|
+
if (!isValidVersion(wanted)) {
|
|
93
92
|
throw new ChangelogError(`版本要写成 x.y.z(收到「${String(version)}」)—— 也可以省略,用 package.json 里的`);
|
|
94
93
|
}
|
|
95
94
|
const file = changelogFileOf(root);
|
|
@@ -134,7 +133,7 @@ function runAdd(root, args) {
|
|
|
134
133
|
|
|
135
134
|
function runCheck(root, args) {
|
|
136
135
|
const version = changelogVersion(positional(args, 1) ?? manifestVersionOf(root));
|
|
137
|
-
if (!
|
|
136
|
+
if (!isValidVersion(version)) {
|
|
138
137
|
console.error('changelog check 要知道查哪一版:给一个 x.y.z,或先让 package.json 有 version');
|
|
139
138
|
process.exit(2);
|
|
140
139
|
}
|
package/src/checks/contracts.mjs
CHANGED
|
@@ -253,6 +253,17 @@ function mountOf(face, created, manifest, tools) {
|
|
|
253
253
|
},
|
|
254
254
|
manifests: () => [],
|
|
255
255
|
opencliCommand: 'opencli',
|
|
256
|
+
env: {},
|
|
257
|
+
git: {
|
|
258
|
+
origin: async () => ({ kind: 'no-origin' }),
|
|
259
|
+
branch: async () => ({ kind: 'no-branch' }),
|
|
260
|
+
head: async () => ({ kind: 'no-head' }),
|
|
261
|
+
},
|
|
262
|
+
libs: {
|
|
263
|
+
load: async () => {
|
|
264
|
+
throw new Error('gate: libs 未装');
|
|
265
|
+
},
|
|
266
|
+
},
|
|
256
267
|
store: fakeStore(),
|
|
257
268
|
longTasks: fakeLongTasks(created),
|
|
258
269
|
});
|
|
@@ -603,9 +614,16 @@ export async function run() {
|
|
|
603
614
|
}
|
|
604
615
|
}
|
|
605
616
|
|
|
606
|
-
|
|
617
|
+
const usesLibPort = sourceFilesOf(pkg.dir).some((file) =>
|
|
618
|
+
/(?:\blibs\.load\b|\bworkflowFromLibs\b|\bLibReader\b)/.test(readFileSync(file, 'utf8')),
|
|
619
|
+
);
|
|
620
|
+
if (
|
|
621
|
+
needs.some((need) => need?.kind === 'lib') &&
|
|
622
|
+
!products.includes('libInstallCommand') &&
|
|
623
|
+
!usesLibPort
|
|
624
|
+
) {
|
|
607
625
|
problems.push(
|
|
608
|
-
`${name}: 声明了 lib 类 need
|
|
626
|
+
`${name}: 声明了 lib 类 need 却既没用 libInstallCommand,也没走 core 的 LibReader(libs.load)—— ` +
|
|
609
627
|
`lib 没装好时用户只看到"没装",没有能照做的那条安装命令`,
|
|
610
628
|
);
|
|
611
629
|
}
|
package/src/checks/style.mjs
CHANGED
|
@@ -125,6 +125,30 @@ const ANCHOR_EXEMPT = new Map([
|
|
|
125
125
|
'@dsh-workbench/module-demo-anchor-exempt:src/components/IssueRow.tsx',
|
|
126
126
|
'自检探针:整行链形态的裸 `<a>`(只在这个临时探针包里出现)',
|
|
127
127
|
],
|
|
128
|
+
[
|
|
129
|
+
'@pagoda-dsh-workbench/module-argocd:src/components/views/EnvRows.tsx',
|
|
130
|
+
'图标钮 / 按钮皮 / 整行链:外观与交互沿用既有视图,文字链已走 kit 的 Link',
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
'@pagoda-dsh-workbench/module-argocd:src/components/views/IdentityFields.tsx',
|
|
134
|
+
'图标钮 / 按钮皮:登录入口沿用既有视图,文字链已走 kit 的 Link',
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
'@pagoda-dsh-workbench/module-coding-analysis:src/components/views/AnalysisBody.tsx',
|
|
138
|
+
'图标钮:卡片角上的外链按钮,不是正文文字链',
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
'@pagoda-dsh-workbench/module-coding-analysis:src/components/views/AnalysisStates.tsx',
|
|
142
|
+
'按钮皮的链:空态行动区,沿用既有视图',
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
'@pagoda-dsh-workbench/module-coding-analysis:src/components/views/EnvSettings.tsx',
|
|
146
|
+
'按钮皮的链:设置区行动入口,沿用既有视图',
|
|
147
|
+
],
|
|
148
|
+
[
|
|
149
|
+
'@pagoda-dsh-workbench/module-coding-issues:src/components/IssueRow.tsx',
|
|
150
|
+
'整行链:缺陷行整行可点,不是正文文字链',
|
|
151
|
+
],
|
|
128
152
|
]);
|
|
129
153
|
|
|
130
154
|
function bareControlsIn(text) {
|
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
|
}
|
package/src/publish.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { execaSync } from 'execa';
|
|
6
6
|
|
|
7
7
|
import { DESIGN_FILE } from './design.mjs';
|
|
8
|
+
import { publishTagOf } from './release-util.mjs';
|
|
8
9
|
|
|
9
10
|
const CLI = fileURLToPath(new URL('../bin/dsh-wb-module.mjs', import.meta.url));
|
|
10
11
|
|
|
@@ -111,6 +112,9 @@ export async function publish({ root }) {
|
|
|
111
112
|
return;
|
|
112
113
|
}
|
|
113
114
|
console.log('✔ 清单干净。真要发就自己跑(凭据在你手上):');
|
|
114
|
-
|
|
115
|
+
const tag = publishTagOf(manifest.version);
|
|
116
|
+
console.log(
|
|
117
|
+
` npm publish${manifest.publishConfig?.registry === undefined ? '' : ` --registry ${manifest.publishConfig.registry}`}${tag === undefined ? '' : ` --tag ${tag}`}`,
|
|
118
|
+
);
|
|
115
119
|
console.log(` 包目录:${relative(process.cwd(), root) === '' ? '.' : relative(process.cwd(), root)}`);
|
|
116
120
|
}
|
|
@@ -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
|
+
}
|