@dsh-workbench/module-tools 1.1.5 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/bin/dsh-wb-module.mjs +2 -2
- package/package.json +2 -2
- package/src/market-index.mjs +290 -143
- package/src/release-report.mjs +1 -1
- package/src/release.mjs +2 -0
package/README.md
CHANGED
|
@@ -17,10 +17,10 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
|
|
|
17
17
|
维护者确认要真发时跑 `pnpm exec dsh-wb-module release`:全门禁(build / check / design / typecheck / test / selfcheck)、
|
|
18
18
|
验收必须全勾、预检(登录 / license / git 远端)、按 `--target` 发到团队源 / 公网 npm / 安装包 / git,
|
|
19
19
|
最后从出口侧核包(`--dry-run` 只预览,`--verify` 只核已发布的包)。配了可写 git 索引源时,
|
|
20
|
-
|
|
20
|
+
发完还会写索引条目(`--index auto|none|<目录|git 地址>`,`artifacts-in-repo` 连产物一起入库,
|
|
21
21
|
`catalog-only` 只更索引);索引没更上不影响包已发布这件事,结果里会如实写。
|
|
22
22
|
发布可选 `--index-source <id>` 指定发到哪个索引源;不指定时用唯一可发布的源,有多个可发布源会要求指定。
|
|
23
|
-
空仓库先跑 `dsh-wb-module market-init <git
|
|
23
|
+
空仓库先跑 `dsh-wb-module market-init <git 地址>`:生成索引目录 `market/` 与 README 并推首个 commit
|
|
24
24
|
(用仓库声明的默认分支,读不到时用 `--ref` 指定)。在工作台里配了可写索引源之后,
|
|
25
25
|
`release` 默认出口就是 `market`:产物与索引一起进那个仓库。
|
|
26
26
|
发布过程中每一步都写进 `~/.dsh/workbench/releases/<unit>.json`;中断后带 `--resume` 接着发
|
|
@@ -33,10 +33,11 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
|
|
|
33
33
|
模块的 `package.json` 可用 `dsh.workbench.market` 声明 `category` 与 `labels`(字符串数组);
|
|
34
34
|
不写就不带分类和标签,索引里只留 `metadata.workbench: true` 这条身份标记。
|
|
35
35
|
要换署名时带 `--author "姓名 <邮箱>"`(或只给姓名):它写回 `package.json`,索引、产物与提交都用它,
|
|
36
|
-
`--refresh`
|
|
36
|
+
`--refresh` 时也一样。**发布本身不改市集里的标题 / 署名 / 说明**:这些元数据只由 `--refresh`(或显式 `--author`)写,
|
|
37
|
+
避免一台还没更新的机器发新版时把别人刚改的元数据盖回去。一个包名归第一次发布的人(条目文件里的 `owner`):换署名发同名会被拒,
|
|
37
38
|
`--refresh` 也一样 —— 真要换人,先改那一条的 `owner`。
|
|
38
39
|
同一个市场里,一个模块 id(`dsh.workbench.unit`)只能被一个条目使用;产物目录按包名走
|
|
39
|
-
(`
|
|
40
|
+
(`artifacts/<包名>/`):换一个包名沿用旧 unit、或两个条目撞同一个 id / 目录,都会被拒。
|
|
40
41
|
|
|
41
42
|
骨架自带脚本,日常不用记长命令:`build` · `typecheck` · `test` · `selfcheck` · `check` · `lint` · `design`。
|
|
42
43
|
|
|
@@ -45,7 +46,7 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
|
|
|
45
46
|
| 命令 | 作用 |
|
|
46
47
|
|---|---|
|
|
47
48
|
| `dsh-wb-module init <unit> [--dir <目录>] [--template minimal\|diy] [--description "一句话"]` | 建模块骨架 |
|
|
48
|
-
| `dsh-wb-module market-init <git 地址\|目录> [--ref <分支>] [--path <索引路径>]` | 初始化市场索引仓库:写 `market
|
|
49
|
+
| `dsh-wb-module market-init <git 地址\|目录> [--ref <分支>] [--path <索引路径>]` | 初始化市场索引仓库:写 `market/` + README,推首个 commit |
|
|
49
50
|
| `dsh-wb-module build` | 出 `dist/host.js`(单文件 ESM)与 `client.js`(套 loader 壳) |
|
|
50
51
|
| `dsh-wb-module check [--only <判据>]` | 十三条判据;`--only` 单跑,也当查询口用;`--list [--json\|--markdown]` 只列判据元数据 |
|
|
51
52
|
| `dsh-wb-module lint [--fix]` | 用模块自己那份 `eslint.config.mjs`;`--fix` 写回文件 |
|
package/bin/dsh-wb-module.mjs
CHANGED
|
@@ -73,7 +73,7 @@ const MARKET_INIT_HELP = `
|
|
|
73
73
|
market-init <git 地址|目录> [--ref <分支>] [--path <索引路径>] [--no-readme] [--json]
|
|
74
74
|
|
|
75
75
|
空仓库:用仓库自己声明的默认分支,读不到时用 --ref 指定(github.com 兜底 main)。
|
|
76
|
-
已有 commit
|
|
76
|
+
已有 commit 的仓库:只补索引目录 / README.md,不改已有文件。`;
|
|
77
77
|
|
|
78
78
|
const CHANGELOG_HELP = `
|
|
79
79
|
用法:
|
|
@@ -86,7 +86,7 @@ const COMMANDS = {
|
|
|
86
86
|
build: '出 dist/host.js(单文件 ESM)与 client.js(套 loader 壳)',
|
|
87
87
|
check: '十三条判据;--only 单跑,也当查询口用;--list 只列判据元数据',
|
|
88
88
|
init: '建模块骨架(--dir / --template minimal|diy / --description)',
|
|
89
|
-
'market-init': '初始化市场索引仓库(写 market
|
|
89
|
+
'market-init': '初始化市场索引仓库(写 market/ 与 README,推首个 commit)',
|
|
90
90
|
design: '设计底稿:init 填 / check 查空格 / render 生成 README / accept 打勾',
|
|
91
91
|
publish: '发货前清单:门禁 + files + npm pack(只 dry run)',
|
|
92
92
|
release: '真发布:全门禁 + 验收闸 + 预检接管 + 发到出口 + 发后核包 + 更新市场索引(中断可 --resume)',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/module-tools",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
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.1.
|
|
36
|
+
"@dsh-workbench/shared": "1.1.6"
|
|
37
37
|
},
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
package/src/market-index.mjs
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
3
|
import { dirname, join, resolve } from 'node:path';
|
|
3
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
5
|
|
|
5
6
|
import {
|
|
7
|
+
MARKET_ENTRIES_README_LINES,
|
|
6
8
|
MARKET_INDEX_README_LINES,
|
|
7
|
-
|
|
9
|
+
assembleMarketIndex,
|
|
8
10
|
claimOf,
|
|
11
|
+
collectMarketEntryFiles,
|
|
9
12
|
gitUrlOk,
|
|
10
13
|
marketArtifactPath,
|
|
14
|
+
marketArtifactPathFor,
|
|
15
|
+
marketArtifactRoot,
|
|
16
|
+
marketEntryFilesOfLegacy,
|
|
17
|
+
marketEntryPath,
|
|
18
|
+
marketMetaDocOf,
|
|
19
|
+
marketMetaPath,
|
|
11
20
|
marketSourceKey,
|
|
21
|
+
marketVersionDocOf,
|
|
12
22
|
parseMarketIndexJson,
|
|
13
|
-
upsertMarketEntry,
|
|
14
23
|
} from '@dsh-workbench/shared/host';
|
|
15
24
|
|
|
16
|
-
const
|
|
25
|
+
const MARKET_DIR = 'market';
|
|
26
|
+
const LEGACY_FILE = 'market.json';
|
|
17
27
|
const SAFE_PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
18
28
|
const WORKSPACE_PREFIX = 'workspace:';
|
|
19
29
|
const ARTIFACT_DIRS = ['dist'];
|
|
@@ -108,7 +118,7 @@ export function claimProblemText(name, claim, unit) {
|
|
|
108
118
|
}
|
|
109
119
|
if (claim.state === 'path-taken') {
|
|
110
120
|
return (
|
|
111
|
-
`市场仓库里的 ${
|
|
121
|
+
`市场仓库里的 ${marketArtifactRoot(name)} 已经被 ${claim.entry} 占了 —— 一个产物目录只能放一个包。` +
|
|
112
122
|
'换一个包名再发;那个条目如果已经废弃,让索引维护者先处理掉。'
|
|
113
123
|
);
|
|
114
124
|
}
|
|
@@ -128,17 +138,180 @@ function unitOf(manifest) {
|
|
|
128
138
|
return typeof unit === 'string' ? unit.trim() : '';
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
function
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
function indexPathOf(plan) {
|
|
142
|
+
const asked = (plan.source.path ?? '').trim();
|
|
143
|
+
const base = asked === '' ? MARKET_DIR : asked.replace(/^\/+/, '');
|
|
144
|
+
return base.replace(/\/+$/, '') || MARKET_DIR;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readWorktreeIndex(workdir, dir) {
|
|
148
|
+
const root = join(workdir, dir);
|
|
149
|
+
if (existsSync(root)) {
|
|
150
|
+
const assembled = assembleMarketIndex(collectMarketEntryFiles(root));
|
|
151
|
+
if (!assembled.ok) {
|
|
152
|
+
throw new MarketIndexError(
|
|
153
|
+
`${dir}/ 里有不合规的条目:${assembled.problems.map((one) => one.detail).join(';')}`,
|
|
154
|
+
'index-write',
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return assembled.index;
|
|
158
|
+
}
|
|
159
|
+
const legacy = join(workdir, LEGACY_FILE);
|
|
160
|
+
if (!existsSync(legacy)) return { format: 1, plugins: [] };
|
|
161
|
+
const parsed = parseMarketIndexJson(readFileSync(legacy, 'utf8'));
|
|
135
162
|
if (!parsed.ok) {
|
|
136
163
|
throw new MarketIndexError(
|
|
137
|
-
`${
|
|
164
|
+
`${LEGACY_FILE} 不合 format 1:${parsed.problems.map((one) => one.detail).join(';')}`,
|
|
138
165
|
'index-write',
|
|
139
166
|
);
|
|
140
167
|
}
|
|
141
|
-
return
|
|
168
|
+
return parsed.index;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function ensureClaimFree(workdir, dir, input) {
|
|
172
|
+
const claim = claimOf(readWorktreeIndex(workdir, dir), input);
|
|
173
|
+
if (claim.state === 'conflict' || claim.state === 'unit-taken' || claim.state === 'path-taken') {
|
|
174
|
+
throw new MarketIndexError(claimProblemText(input.name, claim, input.unit), 'market-owner');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function ensureVersionFree(workdir, dir, input) {
|
|
179
|
+
const entry = readWorktreeIndex(workdir, dir).plugins.find((one) => one.name === input.name);
|
|
180
|
+
if (entry?.versions.some((one) => one.version === input.version) === true) {
|
|
181
|
+
throw new MarketIndexError(`索引里已经有 ${input.name}@${input.version} —— 换个版本号再发`, 'market-owner');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function writeEntryFile(workdir, dir, entry, version) {
|
|
186
|
+
const doc = marketVersionDocOf(entry, version);
|
|
187
|
+
const rel = marketEntryPath(dir, entry.name, version.version);
|
|
188
|
+
const file = join(workdir, ...rel.split('/'));
|
|
189
|
+
if (existsSync(file)) {
|
|
190
|
+
const current = JSON.parse(readFileSync(file, 'utf8'));
|
|
191
|
+
if (isDeepStrictEqual(current, doc)) return rel;
|
|
192
|
+
throw new MarketIndexError(
|
|
193
|
+
`索引里已经有 ${entry.name}@${version.version}(${rel})—— 换个版本号再发`,
|
|
194
|
+
'market-owner',
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
198
|
+
writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
|
|
199
|
+
return rel;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function metaEpochOf(workdir, dir, name) {
|
|
203
|
+
const root = join(workdir, dir, name);
|
|
204
|
+
if (!existsSync(root)) return 0;
|
|
205
|
+
let epoch = 0;
|
|
206
|
+
for (const item of readdirSync(root)) {
|
|
207
|
+
if (!item.startsWith('meta-') || !item.endsWith('.json')) continue;
|
|
208
|
+
try {
|
|
209
|
+
const doc = JSON.parse(readFileSync(join(root, item), 'utf8'));
|
|
210
|
+
if (typeof doc.epoch === 'number' && doc.epoch > epoch) epoch = doc.epoch;
|
|
211
|
+
} catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return epoch;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function writeMetaFile(workdir, dir, entry, at) {
|
|
219
|
+
const epoch = metaEpochOf(workdir, dir, entry.name) + 1;
|
|
220
|
+
const suffix = randomBytes(3).toString('hex');
|
|
221
|
+
const doc = marketMetaDocOf(entry, epoch, Math.floor(at / 1000));
|
|
222
|
+
const rel = marketMetaPath(dir, entry.name, epoch, suffix);
|
|
223
|
+
const file = join(workdir, ...rel.split('/'));
|
|
224
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
225
|
+
writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
|
|
226
|
+
return rel;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function migrateLegacyIndex(workdir, dir) {
|
|
230
|
+
const legacy = join(workdir, LEGACY_FILE);
|
|
231
|
+
if (!existsSync(legacy)) return [];
|
|
232
|
+
const files = marketEntryFilesOfLegacy(readFileSync(legacy, 'utf8'), dir);
|
|
233
|
+
if (files === undefined) {
|
|
234
|
+
const parsed = parseMarketIndexJson(readFileSync(legacy, 'utf8'));
|
|
235
|
+
throw new MarketIndexError(
|
|
236
|
+
`${LEGACY_FILE} 不合 format 1:${parsed.ok ? '' : parsed.problems.map((one) => one.detail).join(';')}`,
|
|
237
|
+
'index-write',
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const written = [];
|
|
241
|
+
for (const file of files) {
|
|
242
|
+
const target = join(workdir, ...file.path.split('/'));
|
|
243
|
+
if (existsSync(target)) continue;
|
|
244
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
245
|
+
writeFileSync(target, file.text);
|
|
246
|
+
written.push(file.path);
|
|
247
|
+
}
|
|
248
|
+
const marker = join(workdir, dir, 'README.md');
|
|
249
|
+
if (!existsSync(marker)) {
|
|
250
|
+
mkdirSync(join(workdir, dir), { recursive: true });
|
|
251
|
+
writeFileSync(marker, MARKET_ENTRIES_README_LINES.join('\n'));
|
|
252
|
+
written.push(`${dir}/README.md`);
|
|
253
|
+
}
|
|
254
|
+
rmSync(legacy);
|
|
255
|
+
written.push(LEGACY_FILE);
|
|
256
|
+
return written;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function revOf(exec, workdir, ref) {
|
|
260
|
+
const result = runGit(exec, workdir, ['rev-parse', ref], { allowFailure: true });
|
|
261
|
+
return result.status === 0 ? result.stdout.trim() : '';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function syncWorktree(exec, workdir, branch) {
|
|
265
|
+
const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
|
|
266
|
+
if (fetched.status !== 0) {
|
|
267
|
+
throw new MarketIndexError(
|
|
268
|
+
`拉不到索引仓库的远端 ${branch}:${tailOf(`${fetched.stdout}\n${fetched.stderr}`)}`,
|
|
269
|
+
'git-auth',
|
|
270
|
+
`git -C ${workdir} fetch origin ${branch}`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const head = revOf(exec, workdir, 'HEAD');
|
|
274
|
+
const remote = revOf(exec, workdir, `origin/${branch}`);
|
|
275
|
+
if (head === '' || remote === '' || head === remote) return;
|
|
276
|
+
const behind = runGit(exec, workdir, ['merge-base', '--is-ancestor', head, `origin/${branch}`], {
|
|
277
|
+
allowFailure: true,
|
|
278
|
+
});
|
|
279
|
+
if (behind.status !== 0) return;
|
|
280
|
+
runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function replicatePush(exec, workdir, branch, base, redo) {
|
|
284
|
+
let last = '';
|
|
285
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
286
|
+
const sha = redo();
|
|
287
|
+
const pushed = pushOf(exec, workdir, branch);
|
|
288
|
+
if (pushed.status === 0) return { ok: true, sha };
|
|
289
|
+
last = tailOf(`${pushed.stdout}\n${pushed.stderr}`);
|
|
290
|
+
const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
|
|
291
|
+
if (fetched.status !== 0) return { ok: false, code: 'git-auth', reason: last };
|
|
292
|
+
const remote = revOf(exec, workdir, `origin/${branch}`);
|
|
293
|
+
const ancestor =
|
|
294
|
+
base === ''
|
|
295
|
+
? true
|
|
296
|
+
: runGit(exec, workdir, ['merge-base', '--is-ancestor', base, `origin/${branch}`], {
|
|
297
|
+
allowFailure: true,
|
|
298
|
+
}).status === 0;
|
|
299
|
+
if (!ancestor) {
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
code: 'git-market-rewritten',
|
|
303
|
+
reason: `远端 ${branch} 的历史被改写过(本地基点不在它的祖先里)—— 不自动合并:${last}`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (remote === '' || remote === base) {
|
|
307
|
+
return { ok: false, code: 'git-market-protected', reason: last };
|
|
308
|
+
}
|
|
309
|
+
const reset = runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
|
|
310
|
+
if (reset.status !== 0) {
|
|
311
|
+
return { ok: false, code: 'index-write', reason: `同步远端失败:${tailOf(reset.stderr)}` };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { ok: false, code: 'index-write', reason: `远端一直在变,重试 3 次没挤进去(${last})—— 过一会儿再发` };
|
|
142
315
|
}
|
|
143
316
|
|
|
144
317
|
export function identityOf(manifest) {
|
|
@@ -218,16 +391,6 @@ export function indexEntryOf(input) {
|
|
|
218
391
|
};
|
|
219
392
|
}
|
|
220
393
|
|
|
221
|
-
export function mergeIndexText(text, entry) {
|
|
222
|
-
const raw = String(text ?? '').trim();
|
|
223
|
-
const parsed = raw === '' ? { ok: true, index: { format: 1, plugins: [] } } : parseMarketIndexJson(raw);
|
|
224
|
-
if (!parsed.ok) return { ok: false, problems: parsed.problems };
|
|
225
|
-
const merged = upsertMarketEntry(parsed.index, entry);
|
|
226
|
-
const checked = parseMarketIndexJson(JSON.stringify(merged));
|
|
227
|
-
if (!checked.ok) return { ok: false, problems: checked.problems };
|
|
228
|
-
return { ok: true, text: `${JSON.stringify(checked.index, null, 2)}\n` };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
394
|
function readSources(file) {
|
|
232
395
|
try {
|
|
233
396
|
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
@@ -275,7 +438,7 @@ export function resolveIndexPlan({ args = [], home, exec }) {
|
|
|
275
438
|
if (plan === undefined) {
|
|
276
439
|
if (asked === 'auto' || asked === '') return undefined;
|
|
277
440
|
throw new MarketIndexError(
|
|
278
|
-
`这个工坊还没配可写的 git 索引源。给我一个能推的 git 仓库(或它在本机的 clone 路径),我配成索引源并建一份 ${
|
|
441
|
+
`这个工坊还没配可写的 git 索引源。给我一个能推的 git 仓库(或它在本机的 clone 路径),我配成索引源并建一份 ${MARKET_DIR}/。`,
|
|
279
442
|
'index-write',
|
|
280
443
|
);
|
|
281
444
|
}
|
|
@@ -427,11 +590,11 @@ function remoteHeadOf(exec, workdir, branch) {
|
|
|
427
590
|
return line.split(/\s+/)[0] ?? '';
|
|
428
591
|
}
|
|
429
592
|
|
|
430
|
-
function copyArtifacts(root, workdir, name, manifest) {
|
|
593
|
+
function copyArtifacts(root, workdir, name, version, manifest) {
|
|
431
594
|
if (!SAFE_PACKAGE_NAME.test(name)) {
|
|
432
595
|
throw new MarketIndexError(`包名不是合法 npm 名,没法当产物目录:${JSON.stringify(name)}`, 'index-write');
|
|
433
596
|
}
|
|
434
|
-
const path =
|
|
597
|
+
const path = marketArtifactPathFor(name, version);
|
|
435
598
|
const dest = join(workdir, path);
|
|
436
599
|
rmSync(dest, { recursive: true, force: true });
|
|
437
600
|
mkdirSync(dest, { recursive: true });
|
|
@@ -447,37 +610,6 @@ function copyArtifacts(root, workdir, name, manifest) {
|
|
|
447
610
|
return path;
|
|
448
611
|
}
|
|
449
612
|
|
|
450
|
-
function writeEntry(workdir, indexPath, entry, publisher, unit) {
|
|
451
|
-
const file = join(workdir, indexPath);
|
|
452
|
-
const current = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
453
|
-
if (publisher === '') throw new MarketIndexError(missingPublisherText(), 'market-owner');
|
|
454
|
-
const claim = claimOfIndexFile(workdir, indexPath, {
|
|
455
|
-
name: entry.name,
|
|
456
|
-
publisher,
|
|
457
|
-
unit,
|
|
458
|
-
artifactPath: entry.versions[0]?.source?.type === 'git' ? entry.versions[0].source.path : undefined,
|
|
459
|
-
});
|
|
460
|
-
if (claim.state === 'conflict' || claim.state === 'unit-taken' || claim.state === 'path-taken') {
|
|
461
|
-
throw new MarketIndexError(claimProblemText(entry.name, claim, unit), 'market-owner');
|
|
462
|
-
}
|
|
463
|
-
const merged = mergeIndexText(current, entry);
|
|
464
|
-
if (!merged.ok) {
|
|
465
|
-
throw new MarketIndexError(
|
|
466
|
-
`${indexPath} 不合 format 1:${merged.problems.map((one) => one.detail).join(';')}`,
|
|
467
|
-
'index-write',
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
471
|
-
writeFileSync(file, merged.text);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function resetToRemote(exec, workdir, branch) {
|
|
475
|
-
const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
|
|
476
|
-
if (fetched.status !== 0) return false;
|
|
477
|
-
const reset = runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
|
|
478
|
-
return reset.status === 0;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
613
|
function publishSourceOf(publish) {
|
|
482
614
|
if (publish.git !== undefined) {
|
|
483
615
|
return {
|
|
@@ -497,14 +629,30 @@ function publishSourceOf(publish) {
|
|
|
497
629
|
|
|
498
630
|
export function marketClaim({ plan, exec, home, name, unit, publisher }) {
|
|
499
631
|
const workdir = worktreeOf(plan, exec, home);
|
|
500
|
-
return
|
|
632
|
+
return claimOf(readWorktreeIndex(workdir, indexPathOf(plan)), {
|
|
501
633
|
name,
|
|
502
634
|
publisher,
|
|
503
635
|
unit,
|
|
504
|
-
artifactPath:
|
|
636
|
+
artifactPath: marketArtifactRoot(name),
|
|
505
637
|
});
|
|
506
638
|
}
|
|
507
639
|
|
|
640
|
+
const META_KEYS = ['title', 'description', 'author', 'owner', 'homepage', 'icon', 'metadata'];
|
|
641
|
+
|
|
642
|
+
function withCurrentMeta(entry, current, update) {
|
|
643
|
+
if (current === undefined) return entry;
|
|
644
|
+
const next = { ...entry };
|
|
645
|
+
if (!update) {
|
|
646
|
+
for (const key of META_KEYS) {
|
|
647
|
+
if (key === 'owner') continue;
|
|
648
|
+
if (current[key] === undefined) delete next[key];
|
|
649
|
+
else next[key] = current[key];
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (current.owner !== undefined) next.owner = current.owner;
|
|
653
|
+
return next;
|
|
654
|
+
}
|
|
655
|
+
|
|
508
656
|
export function updateMarketIndex(options) {
|
|
509
657
|
const { plan, exec, home, root, manifest, version, publish, now, resume, onStep } = options;
|
|
510
658
|
const unit = options.unit ?? unitOf(manifest);
|
|
@@ -522,17 +670,21 @@ export function updateMarketIndex(options) {
|
|
|
522
670
|
};
|
|
523
671
|
try {
|
|
524
672
|
const workdir = worktreeOf(plan, exec, home);
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
673
|
+
const dir = indexPathOf(plan);
|
|
674
|
+
const branch = plan.source.ref ?? resume?.branch ?? currentBranch(exec, workdir);
|
|
675
|
+
base = { ...base, branch };
|
|
676
|
+
if (resume !== undefined) {
|
|
677
|
+
if (plan.source.localPath === undefined) {
|
|
678
|
+
const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
|
|
679
|
+
if (fetched.status === 0) runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
|
|
680
|
+
runGit(exec, workdir, ['clean', '-fd', '--', marketArtifactPathFor(manifest.name, version), dir], {
|
|
681
|
+
allowFailure: true,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
rmSync(join(workdir, dir, manifest.name, `${version}.json`), { force: true });
|
|
531
685
|
} else {
|
|
532
686
|
assertClean(exec, workdir);
|
|
533
687
|
}
|
|
534
|
-
const branch = plan.source.ref ?? resume?.branch ?? currentBranch(exec, workdir);
|
|
535
|
-
base = { ...base, branch };
|
|
536
688
|
if (typeof resume?.indexCommit === 'string' && resume.indexCommit !== '' && resume.indexPushed === true) {
|
|
537
689
|
const remote = remoteHeadOf(exec, workdir, branch);
|
|
538
690
|
if (remote === resume.indexCommit) {
|
|
@@ -547,34 +699,42 @@ export function updateMarketIndex(options) {
|
|
|
547
699
|
}
|
|
548
700
|
}
|
|
549
701
|
if (publisher === '') throw new MarketIndexError(missingPublisherText(), 'market-owner');
|
|
550
|
-
|
|
702
|
+
syncWorktree(exec, workdir, branch);
|
|
703
|
+
const baseSha = revOf(exec, workdir, 'HEAD');
|
|
704
|
+
const claimInput = {
|
|
551
705
|
name: manifest.name,
|
|
552
706
|
publisher,
|
|
553
707
|
unit,
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
708
|
+
version,
|
|
709
|
+
artifactPath: marketArtifactRoot(manifest.name),
|
|
710
|
+
};
|
|
711
|
+
ensureClaimFree(workdir, dir, claimInput);
|
|
712
|
+
ensureVersionFree(workdir, dir, claimInput);
|
|
559
713
|
let source;
|
|
560
714
|
let artifactPath;
|
|
715
|
+
let artifactSha = '';
|
|
716
|
+
let indexSha = '';
|
|
561
717
|
if (plan.policy === 'artifacts-in-repo') {
|
|
562
718
|
report({ phase: 'artifacts' });
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
719
|
+
const done = replicatePush(exec, workdir, branch, baseSha, () => {
|
|
720
|
+
ensureClaimFree(workdir, dir, claimInput);
|
|
721
|
+
migrateLegacyIndex(workdir, dir);
|
|
722
|
+
artifactPath = copyArtifacts(root, workdir, manifest.name, version, manifest);
|
|
723
|
+
report({ phase: 'artifacts-copied' });
|
|
724
|
+
artifactSha = commitOf(exec, workdir, `chore(release): ${entryName}`, identityOf(manifest));
|
|
725
|
+
report({ phase: 'artifacts-committed', artifactCommit: artifactSha });
|
|
726
|
+
return artifactSha;
|
|
727
|
+
});
|
|
728
|
+
if (!done.ok) {
|
|
569
729
|
phase = 'artifacts';
|
|
570
730
|
throw new MarketIndexError(
|
|
571
|
-
`产物提交推不上去(分支保护或权限):本地 commit ${
|
|
572
|
-
|
|
731
|
+
`产物提交推不上去(分支保护或权限):本地 commit ${artifactSha};去合并/给权限后跟我说,我补索引。${done.reason}`,
|
|
732
|
+
done.code,
|
|
573
733
|
`git -C ${workdir} push origin HEAD:refs/heads/${branch}`,
|
|
574
734
|
);
|
|
575
735
|
}
|
|
576
|
-
report({ phase: 'artifacts-pushed', artifactCommit:
|
|
577
|
-
source = { type: 'git', url: plan.source.url, commit:
|
|
736
|
+
report({ phase: 'artifacts-pushed', artifactCommit: artifactSha, artifactPushed: true });
|
|
737
|
+
source = { type: 'git', url: plan.source.url, commit: artifactSha, path: artifactPath };
|
|
578
738
|
} else {
|
|
579
739
|
source = publishSourceOf(publish ?? {});
|
|
580
740
|
if (source === undefined) throw new MarketIndexError('这个出口没有可写进索引的 source(tarball 不进索引)', 'index-write');
|
|
@@ -582,30 +742,28 @@ export function updateMarketIndex(options) {
|
|
|
582
742
|
}
|
|
583
743
|
const entry = indexEntryOf({ manifest, version, source, at: now() });
|
|
584
744
|
report({ phase: 'index' });
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
phase = 'index';
|
|
593
|
-
throw new MarketIndexError(
|
|
594
|
-
`模块已进仓库、索引没更新(本地 commit ${indexSha}):${tailOf(`${pushed.stdout}\n${pushed.stderr}`)}`,
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
writeEntry(workdir, indexPath, entry, publisher, unit);
|
|
745
|
+
const indexed = replicatePush(exec, workdir, branch, baseSha, () => {
|
|
746
|
+
if (plan.policy !== 'artifacts-in-repo') migrateLegacyIndex(workdir, dir);
|
|
747
|
+
ensureClaimFree(workdir, dir, claimInput);
|
|
748
|
+
const current = readWorktreeIndex(workdir, dir).plugins.find((one) => one.name === manifest.name);
|
|
749
|
+
const written = withCurrentMeta(entry, current, options.metadata === true);
|
|
750
|
+
if (current === undefined || options.metadata === true) writeMetaFile(workdir, dir, written, now());
|
|
751
|
+
writeEntryFile(workdir, dir, written, written.versions[0]);
|
|
598
752
|
indexSha = commitOf(exec, workdir, `chore(market): ${entryName}`, identityOf(manifest));
|
|
599
753
|
report({ phase: 'index-committed', indexCommit: indexSha });
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
754
|
+
return indexSha;
|
|
755
|
+
});
|
|
756
|
+
if (!indexed.ok) {
|
|
757
|
+
phase = 'index';
|
|
758
|
+
throw new MarketIndexError(
|
|
759
|
+
indexed.code === 'git-market-protected' || indexed.code === 'git-auth'
|
|
760
|
+
? `模块已进仓库、索引没更新(本地 commit ${indexSha}):${indexed.reason}`
|
|
761
|
+
: indexed.reason,
|
|
762
|
+
indexed.code,
|
|
763
|
+
);
|
|
605
764
|
}
|
|
606
765
|
report({ phase: 'index-pushed', indexCommit: indexSha, indexPushed: true });
|
|
607
|
-
const
|
|
608
|
-
const headSha = local.status === 0 ? local.stdout.trim() : '';
|
|
766
|
+
const headSha = revOf(exec, workdir, 'HEAD');
|
|
609
767
|
const remoteHead = remoteHeadOf(exec, workdir, branch);
|
|
610
768
|
if (headSha === '' || remoteHead !== headSha) {
|
|
611
769
|
phase = 'remote-check';
|
|
@@ -642,24 +800,26 @@ export function refreshMarketEntry(options) {
|
|
|
642
800
|
let phase = 'start';
|
|
643
801
|
try {
|
|
644
802
|
const workdir = worktreeOf(plan, exec, home);
|
|
645
|
-
const
|
|
803
|
+
const dir = indexPathOf(plan);
|
|
646
804
|
if (publisher === '') throw new MarketIndexError(missingPublisherText(), 'market-owner');
|
|
647
805
|
assertClean(exec, workdir);
|
|
648
806
|
const branch = plan.source.ref ?? currentBranch(exec, workdir);
|
|
649
807
|
base = { ...base, branch };
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
if (!parsed.ok) {
|
|
654
|
-
throw new MarketIndexError(
|
|
655
|
-
`${indexPath} 不是 format 1:${parsed.problems.map((one) => one.detail).join(';')}`,
|
|
656
|
-
'index-write',
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
const existing = parsed.index.plugins.find((one) => one.name === manifest.name);
|
|
808
|
+
syncWorktree(exec, workdir, branch);
|
|
809
|
+
const index = readWorktreeIndex(workdir, dir);
|
|
810
|
+
const existing = index.plugins.find((one) => one.name === manifest.name);
|
|
660
811
|
if (existing === undefined) {
|
|
661
812
|
throw new MarketIndexError(`${manifest.name} 还不在索引里 —— 先正常发一版`, 'index-write');
|
|
662
813
|
}
|
|
814
|
+
const claim = claimOf(index, {
|
|
815
|
+
name: manifest.name,
|
|
816
|
+
publisher,
|
|
817
|
+
unit,
|
|
818
|
+
artifactPath: marketArtifactRoot(manifest.name),
|
|
819
|
+
});
|
|
820
|
+
if (claim.state === 'conflict' || claim.state === 'unit-taken' || claim.state === 'path-taken') {
|
|
821
|
+
throw new MarketIndexError(claimProblemText(manifest.name, claim, unit), 'market-owner');
|
|
822
|
+
}
|
|
663
823
|
const latest = existing.versions[0]?.version ?? '';
|
|
664
824
|
const meta = entryMetaOf(manifest);
|
|
665
825
|
const next = { ...existing };
|
|
@@ -670,23 +830,14 @@ export function refreshMarketEntry(options) {
|
|
|
670
830
|
if (isDeepStrictEqual(next, existing)) {
|
|
671
831
|
return { ...base, updated: false, unchanged: true, version: latest, phase: 'done' };
|
|
672
832
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
writeEntry(workdir, indexPath, next, publisher, unit);
|
|
682
|
-
indexSha = commitOf(exec, workdir, `chore(market): refresh ${manifest.name}`, identityOf(manifest));
|
|
683
|
-
const retried = pushOf(exec, workdir, branch);
|
|
684
|
-
if (retried.status !== 0) {
|
|
685
|
-
throw new MarketIndexError(`元数据没推上去:${tailOf(retried.stderr)}`, 'index-write');
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
const head = runGit(exec, workdir, ['rev-parse', 'HEAD']);
|
|
689
|
-
const headSha = head.status === 0 ? head.stdout.trim() : '';
|
|
833
|
+
const at = Date.now();
|
|
834
|
+
const baseSha = revOf(exec, workdir, 'HEAD');
|
|
835
|
+
const done = replicatePush(exec, workdir, branch, baseSha, () => {
|
|
836
|
+
writeMetaFile(workdir, dir, next, at);
|
|
837
|
+
return commitOf(exec, workdir, `chore(market): refresh ${manifest.name}`, identityOf(manifest));
|
|
838
|
+
});
|
|
839
|
+
if (!done.ok) throw new MarketIndexError(done.reason, done.code);
|
|
840
|
+
const headSha = revOf(exec, workdir, 'HEAD');
|
|
690
841
|
const remoteHead = remoteHeadOf(exec, workdir, branch);
|
|
691
842
|
if (headSha === '' || remoteHead !== headSha) {
|
|
692
843
|
phase = 'remote-check';
|
|
@@ -695,7 +846,7 @@ export function refreshMarketEntry(options) {
|
|
|
695
846
|
'git-market-pushed',
|
|
696
847
|
);
|
|
697
848
|
}
|
|
698
|
-
return { ...base, updated: true, version: latest, commit:
|
|
849
|
+
return { ...base, updated: true, version: latest, commit: done.sha, phase: 'done' };
|
|
699
850
|
} catch (error) {
|
|
700
851
|
return {
|
|
701
852
|
...base,
|
|
@@ -733,7 +884,7 @@ function defaultBranchOf(exec, workdir, url, ref) {
|
|
|
733
884
|
}
|
|
734
885
|
|
|
735
886
|
export function initializeMarketRepo(options) {
|
|
736
|
-
const { exec, home, url, dir, ref, indexPath =
|
|
887
|
+
const { exec, home, url, dir, ref, indexPath = MARKET_DIR, readme = true } = options;
|
|
737
888
|
let workdir;
|
|
738
889
|
let remote = String(url ?? '').trim();
|
|
739
890
|
if (dir !== undefined) {
|
|
@@ -757,12 +908,13 @@ export function initializeMarketRepo(options) {
|
|
|
757
908
|
const switched = runGit(exec, workdir, ['checkout', '-B', branch], { allowFailure: true });
|
|
758
909
|
if (switched.status !== 0) throw new MarketIndexError(`建分支 ${branch} 没过:${tailOf(switched.stderr)}`, 'index-init');
|
|
759
910
|
}
|
|
760
|
-
const created =
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
911
|
+
const created = migrateLegacyIndex(workdir, indexPath);
|
|
912
|
+
const indexDir = join(workdir, indexPath);
|
|
913
|
+
const marker = join(indexDir, 'README.md');
|
|
914
|
+
if (!existsSync(marker)) {
|
|
915
|
+
mkdirSync(indexDir, { recursive: true });
|
|
916
|
+
writeFileSync(marker, MARKET_ENTRIES_README_LINES.join('\n'));
|
|
917
|
+
created.push(`${indexPath}/README.md`);
|
|
766
918
|
}
|
|
767
919
|
const readmeFile = join(workdir, 'README.md');
|
|
768
920
|
if (readme && !existsSync(readmeFile)) {
|
|
@@ -792,11 +944,7 @@ export function initializeMarketRepo(options) {
|
|
|
792
944
|
export function marketVersionExists({ plan, exec, home, name, version }) {
|
|
793
945
|
try {
|
|
794
946
|
const workdir = worktreeOf(plan, exec, home);
|
|
795
|
-
const
|
|
796
|
-
if (!existsSync(file)) return false;
|
|
797
|
-
const parsed = parseMarketIndexJson(readFileSync(file, 'utf8'));
|
|
798
|
-
if (!parsed.ok) return false;
|
|
799
|
-
const entry = parsed.index.plugins.find((one) => one.name === name);
|
|
947
|
+
const entry = readWorktreeIndex(workdir, indexPathOf(plan)).plugins.find((one) => one.name === name);
|
|
800
948
|
return entry?.versions.some((one) => one.version === version) === true;
|
|
801
949
|
} catch {
|
|
802
950
|
return false;
|
|
@@ -823,4 +971,3 @@ export function verifyMarketArtifact({ plan, exec, home, name, path, commit }) {
|
|
|
823
971
|
return { path: artifactPath, files };
|
|
824
972
|
}
|
|
825
973
|
|
|
826
|
-
export { MARKET_FILE };
|
package/src/release-report.mjs
CHANGED
|
@@ -71,7 +71,7 @@ export function human(out, summary) {
|
|
|
71
71
|
`(${market.branch === undefined ? '默认分支' : market.branch})`,
|
|
72
72
|
);
|
|
73
73
|
out(` 产物:${String(market.path ?? '').replace(/^\/*/, '')} · commit ${shortSha(market.commit)}`);
|
|
74
|
-
out('
|
|
74
|
+
out(' 提交:产物与索引条目两个 commit 都已推上去');
|
|
75
75
|
if (summary.changes.length > 0) out(` 改了:${summary.changes.join(';')}`);
|
|
76
76
|
if (summary.files !== undefined) out(` 包内容:${summary.files.join(' · ')}`);
|
|
77
77
|
out(` 安装:${summary.install}`);
|
package/src/release.mjs
CHANGED
|
@@ -480,6 +480,7 @@ export async function runRelease({
|
|
|
480
480
|
version,
|
|
481
481
|
publish,
|
|
482
482
|
publisher: authorTextOf(manifestNow),
|
|
483
|
+
metadata: authorChange !== undefined,
|
|
483
484
|
exec,
|
|
484
485
|
home,
|
|
485
486
|
now: () => Date.now(),
|
|
@@ -728,6 +729,7 @@ export async function runRelease({
|
|
|
728
729
|
version,
|
|
729
730
|
publish: {},
|
|
730
731
|
publisher,
|
|
732
|
+
metadata: authorChange !== undefined,
|
|
731
733
|
exec,
|
|
732
734
|
home,
|
|
733
735
|
now: () => Date.now(),
|