@dsh-workbench/module-tools 1.1.7 → 1.1.9

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 CHANGED
@@ -30,8 +30,11 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
30
30
  没带 note 又没记录时 `release` 在跑任何门禁之前就拦下。
31
31
  日志写在确认之后,发失败不回滚:这一版已经记下了,下一次发布直接用。
32
32
  `package.json` 缺 `files` 里的 `CHANGELOG.md` 一样拦下;`author` 没写时从本机 `git config` 补进 manifest,索引与产物都带上。
33
- 模块的 `package.json` 可用 `dsh.workbench.market` 声明 `category` 与 `labels`(字符串数组);
34
- 不写就不带分类和标签,索引里只留 `metadata.workbench: true` 这条身份标记。
33
+ 标签只住在市集条目上(`metadata.labels`),**不写进 `package.json`**:清单的键是协议定死的
34
+ (`apiVersion` / `unit` / `title` / `card` / `tools` / `host` / `needs`),往里塞私货 check 会判红。
35
+ 发布时带 `--label <短词>`(可重复,最多两个):它并进索引条目的 `metadata.labels`;
36
+ 不传就沿用老清单里已经写过的(`dsh.workbench.market.labels`,只在读那一侧认)。
37
+ 标题 / 署名 / 说明仍保持索引里现有的那份不动。
35
38
  要换署名时带 `--author "姓名 <邮箱>"`(或只给姓名):它写回 `package.json`,索引、产物与提交都用它,
36
39
  `--refresh` 时也一样。**发布本身不改市集里的标题 / 署名 / 说明**:这些元数据只由 `--refresh`(或显式 `--author`)写,
37
40
  避免一台还没更新的机器发新版时把别人刚改的元数据盖回去。一个包名归第一次发布的人(条目文件里的 `owner`):换署名发同名会被拒,
@@ -61,6 +61,7 @@ const RELEASE_HELP = `
61
61
  --registry <url> --access restricted|public --npmrc <文件>
62
62
  --license <id> --out <目录> --git <remote>
63
63
  --author <署名> 换署名(姓名,或 姓名 <邮箱>);写回 package.json
64
+ --label <短词> 给市集条目打标签(1–2 个,可重复);写回 package.json
64
65
  --changelog-note <文字> --changelog-type added|changed|fixed|removed|security|deprecated
65
66
  这一版还没有更新日志时写进 CHANGELOG.md(文件不在先建)
66
67
  --index auto|none|<目录|git 地址> 发布后要更新的索引源
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/module-tools",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
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.7"
36
+ "@dsh-workbench/shared": "1.1.9"
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",
@@ -1,5 +1,4 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
2
  import { dirname, join, resolve } from 'node:path';
4
3
  import { isDeepStrictEqual } from 'node:util';
5
4
 
@@ -11,15 +10,12 @@ import {
11
10
  collectMarketEntryFiles,
12
11
  gitUrlOk,
13
12
  marketArtifactPath,
14
- marketArtifactPathFor,
15
13
  marketArtifactRoot,
14
+ marketEntryFileOf,
16
15
  marketEntryFilesOfLegacy,
17
- marketEntryPath,
18
- marketMetaDocOf,
19
- marketMetaPath,
20
16
  marketSourceKey,
21
- marketVersionDocOf,
22
17
  parseMarketIndexJson,
18
+ sortMarketVersions,
23
19
  } from '@dsh-workbench/shared/host';
24
20
 
25
21
  const MARKET_DIR = 'market';
@@ -182,48 +178,12 @@ function ensureVersionFree(workdir, dir, input) {
182
178
  }
183
179
  }
184
180
 
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;
181
+ function writeEntryFile(workdir, dir, entry) {
182
+ const file = marketEntryFileOf(entry, dir);
183
+ const target = join(workdir, ...file.path.split('/'));
184
+ mkdirSync(dirname(target), { recursive: true });
185
+ writeFileSync(target, file.text);
186
+ return file.path;
227
187
  }
228
188
 
229
189
  function migrateLegacyIndex(workdir, dir) {
@@ -344,14 +304,30 @@ function textListOf(value) {
344
304
  : [];
345
305
  }
346
306
 
307
+ function recordOf(value) {
308
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
309
+ }
310
+
311
+ export function labelsOfManifest(manifest) {
312
+ const dsh = recordOf(recordOf(manifest).dsh);
313
+ const workbench = recordOf(dsh.workbench);
314
+ return textListOf(recordOf(workbench.market).labels);
315
+ }
316
+
317
+ export function mergeEntryLabels(entry, labels) {
318
+ if (!Array.isArray(labels) || labels.length === 0) return entry;
319
+ const wanted = [...labels];
320
+ if (isDeepStrictEqual(entry.metadata?.labels ?? [], wanted)) return entry;
321
+ return { ...entry, metadata: { ...(entry.metadata ?? {}), labels: wanted } };
322
+ }
323
+
347
324
  export function entryMetaOf(manifest) {
348
325
  const author = authorOf(manifest);
349
326
  const homepage = typeof manifest.homepage === 'string' ? manifest.homepage.trim() : '';
350
327
  const keywords = textListOf(manifest.keywords);
351
- const market = manifest.dsh?.workbench?.market;
352
- const marketRecord = market !== null && typeof market === 'object' && !Array.isArray(market) ? market : {};
328
+ const marketRecord = recordOf(manifest.dsh?.workbench?.market);
353
329
  const category = typeof marketRecord.category === 'string' ? marketRecord.category.trim() : '';
354
- const labels = textListOf(marketRecord.labels);
330
+ const labels = labelsOfManifest(manifest);
355
331
  const order = manifest.dsh?.workbench?.card?.order;
356
332
  const unit = unitOf(manifest);
357
333
  const title = titleOf(manifest);
@@ -373,21 +349,22 @@ export function entryMetaOf(manifest) {
373
349
  }
374
350
 
375
351
  export function indexEntryOf(input) {
376
- const manifest = input.manifest;
377
- const dsh = manifest.engines?.dsh;
378
- const owner = authorTextOf(manifest);
352
+ const owner = authorTextOf(input.manifest);
379
353
  return {
380
- name: manifest.name,
354
+ name: input.manifest.name,
381
355
  ...(owner === '' ? {} : { owner }),
382
- ...entryMetaOf(manifest),
383
- versions: [
384
- {
385
- version: input.version,
386
- publishTimestamp: Math.floor(input.at / 1000),
387
- source: input.source,
388
- ...(typeof dsh === 'string' && dsh.trim() !== '' ? { engines: { dsh: dsh.trim() } } : {}),
389
- },
390
- ],
356
+ ...entryMetaOf(input.manifest),
357
+ versions: [versionRecordOf(input)],
358
+ };
359
+ }
360
+
361
+ export function versionRecordOf(input) {
362
+ const dsh = input.manifest.engines?.dsh;
363
+ return {
364
+ version: input.version,
365
+ publishTimestamp: Math.floor(input.at / 1000),
366
+ source: input.source,
367
+ ...(typeof dsh === 'string' && dsh.trim() !== '' ? { engines: { dsh: dsh.trim() } } : {}),
391
368
  };
392
369
  }
393
370
 
@@ -590,11 +567,11 @@ function remoteHeadOf(exec, workdir, branch) {
590
567
  return line.split(/\s+/)[0] ?? '';
591
568
  }
592
569
 
593
- function copyArtifacts(root, workdir, name, version, manifest) {
570
+ function copyArtifacts(root, workdir, name, manifest) {
594
571
  if (!SAFE_PACKAGE_NAME.test(name)) {
595
572
  throw new MarketIndexError(`包名不是合法 npm 名,没法当产物目录:${JSON.stringify(name)}`, 'index-write');
596
573
  }
597
- const path = marketArtifactPathFor(name, version);
574
+ const path = marketArtifactRoot(name);
598
575
  const dest = join(workdir, path);
599
576
  rmSync(dest, { recursive: true, force: true });
600
577
  mkdirSync(dest, { recursive: true });
@@ -637,22 +614,6 @@ export function marketClaim({ plan, exec, home, name, unit, publisher }) {
637
614
  });
638
615
  }
639
616
 
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
-
656
617
  export function updateMarketIndex(options) {
657
618
  const { plan, exec, home, root, manifest, version, publish, now, resume, onStep } = options;
658
619
  const unit = options.unit ?? unitOf(manifest);
@@ -677,11 +638,19 @@ export function updateMarketIndex(options) {
677
638
  if (plan.source.localPath === undefined) {
678
639
  const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
679
640
  if (fetched.status === 0) runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
680
- runGit(exec, workdir, ['clean', '-fd', '--', marketArtifactPathFor(manifest.name, version), dir], {
641
+ runGit(exec, workdir, ['clean', '-fd', '--', marketArtifactRoot(manifest.name), dir], {
681
642
  allowFailure: true,
682
643
  });
683
644
  }
684
- rmSync(join(workdir, dir, manifest.name, `${version}.json`), { force: true });
645
+ const file = join(workdir, dir, `${manifest.name}.json`);
646
+ if (existsSync(file)) {
647
+ const kept = JSON.parse(readFileSync(file, 'utf8'));
648
+ const versions = (Array.isArray(kept.versions) ? kept.versions : []).filter(
649
+ (one) => one?.version !== version,
650
+ );
651
+ if (versions.length === 0) rmSync(file, { force: true });
652
+ else writeFileSync(file, `${JSON.stringify({ ...kept, versions }, null, 2)}\n`);
653
+ }
685
654
  } else {
686
655
  assertClean(exec, workdir);
687
656
  }
@@ -719,7 +688,7 @@ export function updateMarketIndex(options) {
719
688
  const done = replicatePush(exec, workdir, branch, baseSha, () => {
720
689
  ensureClaimFree(workdir, dir, claimInput);
721
690
  migrateLegacyIndex(workdir, dir);
722
- artifactPath = copyArtifacts(root, workdir, manifest.name, version, manifest);
691
+ artifactPath = copyArtifacts(root, workdir, manifest.name, manifest);
723
692
  report({ phase: 'artifacts-copied' });
724
693
  artifactSha = commitOf(exec, workdir, `chore(release): ${entryName}`, identityOf(manifest));
725
694
  report({ phase: 'artifacts-committed', artifactCommit: artifactSha });
@@ -740,15 +709,32 @@ export function updateMarketIndex(options) {
740
709
  if (source === undefined) throw new MarketIndexError('这个出口没有可写进索引的 source(tarball 不进索引)', 'index-write');
741
710
  if (source.type === 'npm' && source.registry === 'https://registry.npmjs.org/') delete source.registry;
742
711
  }
743
- const entry = indexEntryOf({ manifest, version, source, at: now() });
712
+ const versionRecord = versionRecordOf({ manifest, version, source, at: now() });
744
713
  report({ phase: 'index' });
745
714
  const indexed = replicatePush(exec, workdir, branch, baseSha, () => {
746
715
  if (plan.policy !== 'artifacts-in-repo') migrateLegacyIndex(workdir, dir);
747
716
  ensureClaimFree(workdir, dir, claimInput);
717
+ ensureVersionFree(workdir, dir, claimInput);
748
718
  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]);
719
+ const owner = authorTextOf(manifest);
720
+ let next;
721
+ if (current === undefined) {
722
+ next = {
723
+ name: manifest.name,
724
+ ...(owner === '' ? {} : { owner }),
725
+ ...entryMetaOf(manifest),
726
+ versions: [versionRecord],
727
+ };
728
+ } else {
729
+ next = { ...current, versions: sortMarketVersions([versionRecord, ...current.versions]) };
730
+ if (options.metadata === true) {
731
+ const merged = { ...next, ...entryMetaOf(manifest) };
732
+ if (current.owner !== undefined) merged.owner = current.owner;
733
+ next = merged;
734
+ }
735
+ }
736
+ next = mergeEntryLabels(next, options.labels);
737
+ writeEntryFile(workdir, dir, next);
752
738
  indexSha = commitOf(exec, workdir, `chore(market): ${entryName}`, identityOf(manifest));
753
739
  report({ phase: 'index-committed', indexCommit: indexSha });
754
740
  return indexSha;
@@ -830,10 +816,18 @@ export function refreshMarketEntry(options) {
830
816
  if (isDeepStrictEqual(next, existing)) {
831
817
  return { ...base, updated: false, unchanged: true, version: latest, phase: 'done' };
832
818
  }
833
- const at = Date.now();
834
819
  const baseSha = revOf(exec, workdir, 'HEAD');
835
820
  const done = replicatePush(exec, workdir, branch, baseSha, () => {
836
- writeMetaFile(workdir, dir, next, at);
821
+ const fresh = readWorktreeIndex(workdir, dir).plugins.find((one) => one.name === manifest.name);
822
+ if (fresh === undefined) {
823
+ throw new MarketIndexError(`${manifest.name} 还不在索引里 —— 先正常发一版`, 'index-write');
824
+ }
825
+ const updated = { ...fresh };
826
+ for (const key of ['title', 'description', 'author', 'homepage', 'metadata']) {
827
+ if (Object.hasOwn(meta, key)) updated[key] = meta[key];
828
+ else delete updated[key];
829
+ }
830
+ writeEntryFile(workdir, dir, updated);
837
831
  return commitOf(exec, workdir, `chore(market): refresh ${manifest.name}`, identityOf(manifest));
838
832
  });
839
833
  if (!done.ok) throw new MarketIndexError(done.reason, done.code);
@@ -58,6 +58,14 @@ export function argValue(args, flag) {
58
58
  return at === -1 ? undefined : args[at + 1];
59
59
  }
60
60
 
61
+ export function argValues(args, flag) {
62
+ const values = [];
63
+ for (let at = 0; at < args.length; at += 1) {
64
+ if (args[at] === flag) values.push(args[at + 1] ?? '');
65
+ }
66
+ return values;
67
+ }
68
+
61
69
  export function hasFlag(args, flag) {
62
70
  return args.includes(flag);
63
71
  }
package/src/release.mjs CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  ReleaseError,
26
26
  ResumableError,
27
27
  argValue,
28
+ argValues,
28
29
  defaultExec,
29
30
  dshHomeOf,
30
31
  hasFlag,
@@ -52,6 +53,7 @@ import {
52
53
  authorTextOf,
53
54
  claimProblemText,
54
55
  identityOf,
56
+ labelsOfManifest,
55
57
  marketClaim,
56
58
  marketVersionExists,
57
59
  missingPublisherText,
@@ -262,6 +264,11 @@ export async function runRelease({
262
264
  if (!CHANGELOG_KINDS.includes(changelogKind)) {
263
265
  throw new ReleaseError(`--changelog-type 只认 ${CHANGELOG_KINDS.join(' / ')}(收到「${changelogKind}」)`);
264
266
  }
267
+ const askedLabels = [...new Set(argValues(args, '--label').map((one) => String(one).trim()))];
268
+ if (askedLabels.some((one) => one === '' || /[\r\n]/u.test(one))) {
269
+ throw new ReleaseError('--label 只认一行短词:不能是空的');
270
+ }
271
+ if (askedLabels.length > 2) throw new ReleaseError('--label 最多两个 —— 一个模块的标签是 1–2 个短词');
265
272
  const manifest = readManifest(root);
266
273
  const unit = manifest?.dsh?.workbench?.unit;
267
274
  if (typeof unit !== 'string' || unit === '') throw new ReleaseError('这不是一个 wb 模块包(没有 dsh.workbench.unit)');
@@ -313,6 +320,9 @@ export async function runRelease({
313
320
  emit(out, json, summary);
314
321
  return summary;
315
322
  }
323
+ const labels = askedLabels;
324
+ const labelsForIndex = labels.length > 0 ? labels : labelsOfManifest(manifest);
325
+ const labelsChange = verify || labels.length === 0 ? undefined : `标签 ← ${labels.join(' · ')}`;
316
326
  const explicitApplied = explicitAuthor !== undefined && !verify && !dryRun;
317
327
  if (explicitApplied) {
318
328
  manifest.author = explicitAuthor;
@@ -343,7 +353,7 @@ export async function runRelease({
343
353
  }
344
354
  const authored = authorChangeOf(root, manifest, exec);
345
355
  if (hasFlag(args, '--refresh')) {
346
- if (dryRun) throw new ReleaseError('--refresh 没有预览:它只改索引里的标题 / 作者 / 说明');
356
+ if (dryRun) throw new ReleaseError('--refresh 没有预览:它只改索引里的标题 / 作者 / 说明 / 标签');
347
357
  if (target !== 'market') throw new ReleaseError('--refresh 只对市场索引有意义(--target market)');
348
358
  if (indexPlan === undefined) {
349
359
  throw new ReleaseError('没有可写的 git 索引源 —— 用 --index <仓库> 指定,或先在「模块市场 → 发现」里初始化索引');
@@ -369,8 +379,13 @@ export async function runRelease({
369
379
  },
370
380
  changes:
371
381
  result.updated === true
372
- ? ['市集元数据已刷新', ...(explicitAuthor === undefined ? [] : ['author ← --author'])]
382
+ ? [
383
+ '市集元数据已刷新',
384
+ ...(explicitAuthor === undefined ? [] : ['author ← --author']),
385
+ ...(labelsChange === undefined ? [] : [labelsChange]),
386
+ ]
373
387
  : [],
388
+ ...(labels.length === 0 ? {} : { labels }),
374
389
  install: '',
375
390
  text:
376
391
  result.updated === true
@@ -481,6 +496,7 @@ export async function runRelease({
481
496
  publish,
482
497
  publisher: authorTextOf(manifestNow),
483
498
  metadata: authorChange !== undefined,
499
+ ...(labelsForIndex.length === 0 ? {} : { labels: labelsForIndex }),
484
500
  exec,
485
501
  home,
486
502
  now: () => Date.now(),
@@ -489,6 +505,7 @@ export async function runRelease({
489
505
  let changelogChange;
490
506
  const changesOf = (version) => [
491
507
  ...(authorChange === undefined ? [] : [authorChange]),
508
+ ...(labelsChange === undefined ? [] : [labelsChange]),
492
509
  ...(version === manifest.version ? [] : [`version → ${version}`]),
493
510
  ...(changelogChange === undefined ? [] : [changelogChange]),
494
511
  ];
@@ -708,6 +725,7 @@ export async function runRelease({
708
725
  version,
709
726
  files,
710
727
  changes: changesOf(version),
728
+ ...(labels.length === 0 ? {} : { labels }),
711
729
  ...(changelogReport === undefined ? {} : { changelog: changelogReport }),
712
730
  install: '',
713
731
  market: indexPreviewOf(manifest.name, version),
@@ -730,6 +748,7 @@ export async function runRelease({
730
748
  publish: {},
731
749
  publisher,
732
750
  metadata: authorChange !== undefined,
751
+ ...(labelsForIndex.length === 0 ? {} : { labels: labelsForIndex }),
733
752
  exec,
734
753
  home,
735
754
  now: () => Date.now(),