@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
|
@@ -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,7 +1,8 @@
|
|
|
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
|
+
import semver from 'semver';
|
|
5
6
|
|
|
6
7
|
export class ReleaseError extends Error {
|
|
7
8
|
constructor(message, code = 1) {
|
|
@@ -52,6 +53,35 @@ export function tailOf(text) {
|
|
|
52
53
|
.join(' | ');
|
|
53
54
|
}
|
|
54
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
|
+
|
|
79
|
+
export function publishTagOf(version) {
|
|
80
|
+
const prerelease = semver.prerelease(String(version ?? '').trim());
|
|
81
|
+
if (prerelease === null) return undefined;
|
|
82
|
+
return typeof prerelease[0] === 'string' ? prerelease[0] : 'next';
|
|
83
|
+
}
|
|
84
|
+
|
|
55
85
|
export function defaultExec(command, argv, { cwd, capture = false, allowFailure = false } = {}) {
|
|
56
86
|
const cache = join(tmpdir(), 'dsh-wb-npm-cache');
|
|
57
87
|
const logs = join(tmpdir(), 'dsh-wb-npm-logs');
|