@dsh-workbench/module-tools 1.1.7 → 1.1.8

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
@@ -32,6 +32,8 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
32
32
  `package.json` 缺 `files` 里的 `CHANGELOG.md` 一样拦下;`author` 没写时从本机 `git config` 补进 manifest,索引与产物都带上。
33
33
  模块的 `package.json` 可用 `dsh.workbench.market` 声明 `category` 与 `labels`(字符串数组);
34
34
  不写就不带分类和标签,索引里只留 `metadata.workbench: true` 这条身份标记。
35
+ 发布时带 `--label <短词>`(可重复,最多两个):它写回 `dsh.workbench.market.labels`;不传就用模块里已有的。
36
+ 每次发布都会把模块里的标签并进索引条目的 `metadata.labels` —— 标题 / 署名 / 说明仍保持索引里现有的那份不动。
35
37
  要换署名时带 `--author "姓名 <邮箱>"`(或只给姓名):它写回 `package.json`,索引、产物与提交都用它,
36
38
  `--refresh` 时也一样。**发布本身不改市集里的标题 / 署名 / 说明**:这些元数据只由 `--refresh`(或显式 `--author`)写,
37
39
  避免一台还没更新的机器发新版时把别人刚改的元数据盖回去。一个包名归第一次发布的人(条目文件里的 `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.8",
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.8"
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,44 @@ 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
+ function recordFieldOf(container, key) {
312
+ const value = container[key];
313
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) return value;
314
+ const next = {};
315
+ container[key] = next;
316
+ return next;
317
+ }
318
+
319
+ export function labelsOfManifest(manifest) {
320
+ const dsh = recordOf(recordOf(manifest).dsh);
321
+ const workbench = recordOf(dsh.workbench);
322
+ return textListOf(recordOf(workbench.market).labels);
323
+ }
324
+
325
+ export function setManifestLabels(manifest, labels) {
326
+ if (labels.length === 0) return;
327
+ const market = recordFieldOf(recordFieldOf(recordFieldOf(manifest, 'dsh'), 'workbench'), 'market');
328
+ market.labels = [...labels];
329
+ }
330
+
331
+ export function mergeEntryLabels(entry, labels) {
332
+ if (!Array.isArray(labels) || labels.length === 0) return entry;
333
+ const wanted = [...labels];
334
+ if (isDeepStrictEqual(entry.metadata?.labels ?? [], wanted)) return entry;
335
+ return { ...entry, metadata: { ...(entry.metadata ?? {}), labels: wanted } };
336
+ }
337
+
347
338
  export function entryMetaOf(manifest) {
348
339
  const author = authorOf(manifest);
349
340
  const homepage = typeof manifest.homepage === 'string' ? manifest.homepage.trim() : '';
350
341
  const keywords = textListOf(manifest.keywords);
351
- const market = manifest.dsh?.workbench?.market;
352
- const marketRecord = market !== null && typeof market === 'object' && !Array.isArray(market) ? market : {};
342
+ const marketRecord = recordOf(manifest.dsh?.workbench?.market);
353
343
  const category = typeof marketRecord.category === 'string' ? marketRecord.category.trim() : '';
354
- const labels = textListOf(marketRecord.labels);
344
+ const labels = labelsOfManifest(manifest);
355
345
  const order = manifest.dsh?.workbench?.card?.order;
356
346
  const unit = unitOf(manifest);
357
347
  const title = titleOf(manifest);
@@ -373,21 +363,22 @@ export function entryMetaOf(manifest) {
373
363
  }
374
364
 
375
365
  export function indexEntryOf(input) {
376
- const manifest = input.manifest;
377
- const dsh = manifest.engines?.dsh;
378
- const owner = authorTextOf(manifest);
366
+ const owner = authorTextOf(input.manifest);
379
367
  return {
380
- name: manifest.name,
368
+ name: input.manifest.name,
381
369
  ...(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
- ],
370
+ ...entryMetaOf(input.manifest),
371
+ versions: [versionRecordOf(input)],
372
+ };
373
+ }
374
+
375
+ export function versionRecordOf(input) {
376
+ const dsh = input.manifest.engines?.dsh;
377
+ return {
378
+ version: input.version,
379
+ publishTimestamp: Math.floor(input.at / 1000),
380
+ source: input.source,
381
+ ...(typeof dsh === 'string' && dsh.trim() !== '' ? { engines: { dsh: dsh.trim() } } : {}),
391
382
  };
392
383
  }
393
384
 
@@ -590,11 +581,11 @@ function remoteHeadOf(exec, workdir, branch) {
590
581
  return line.split(/\s+/)[0] ?? '';
591
582
  }
592
583
 
593
- function copyArtifacts(root, workdir, name, version, manifest) {
584
+ function copyArtifacts(root, workdir, name, manifest) {
594
585
  if (!SAFE_PACKAGE_NAME.test(name)) {
595
586
  throw new MarketIndexError(`包名不是合法 npm 名,没法当产物目录:${JSON.stringify(name)}`, 'index-write');
596
587
  }
597
- const path = marketArtifactPathFor(name, version);
588
+ const path = marketArtifactRoot(name);
598
589
  const dest = join(workdir, path);
599
590
  rmSync(dest, { recursive: true, force: true });
600
591
  mkdirSync(dest, { recursive: true });
@@ -637,22 +628,6 @@ export function marketClaim({ plan, exec, home, name, unit, publisher }) {
637
628
  });
638
629
  }
639
630
 
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
631
  export function updateMarketIndex(options) {
657
632
  const { plan, exec, home, root, manifest, version, publish, now, resume, onStep } = options;
658
633
  const unit = options.unit ?? unitOf(manifest);
@@ -677,11 +652,19 @@ export function updateMarketIndex(options) {
677
652
  if (plan.source.localPath === undefined) {
678
653
  const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
679
654
  if (fetched.status === 0) runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
680
- runGit(exec, workdir, ['clean', '-fd', '--', marketArtifactPathFor(manifest.name, version), dir], {
655
+ runGit(exec, workdir, ['clean', '-fd', '--', marketArtifactRoot(manifest.name), dir], {
681
656
  allowFailure: true,
682
657
  });
683
658
  }
684
- rmSync(join(workdir, dir, manifest.name, `${version}.json`), { force: true });
659
+ const file = join(workdir, dir, `${manifest.name}.json`);
660
+ if (existsSync(file)) {
661
+ const kept = JSON.parse(readFileSync(file, 'utf8'));
662
+ const versions = (Array.isArray(kept.versions) ? kept.versions : []).filter(
663
+ (one) => one?.version !== version,
664
+ );
665
+ if (versions.length === 0) rmSync(file, { force: true });
666
+ else writeFileSync(file, `${JSON.stringify({ ...kept, versions }, null, 2)}\n`);
667
+ }
685
668
  } else {
686
669
  assertClean(exec, workdir);
687
670
  }
@@ -719,7 +702,7 @@ export function updateMarketIndex(options) {
719
702
  const done = replicatePush(exec, workdir, branch, baseSha, () => {
720
703
  ensureClaimFree(workdir, dir, claimInput);
721
704
  migrateLegacyIndex(workdir, dir);
722
- artifactPath = copyArtifacts(root, workdir, manifest.name, version, manifest);
705
+ artifactPath = copyArtifacts(root, workdir, manifest.name, manifest);
723
706
  report({ phase: 'artifacts-copied' });
724
707
  artifactSha = commitOf(exec, workdir, `chore(release): ${entryName}`, identityOf(manifest));
725
708
  report({ phase: 'artifacts-committed', artifactCommit: artifactSha });
@@ -740,15 +723,32 @@ export function updateMarketIndex(options) {
740
723
  if (source === undefined) throw new MarketIndexError('这个出口没有可写进索引的 source(tarball 不进索引)', 'index-write');
741
724
  if (source.type === 'npm' && source.registry === 'https://registry.npmjs.org/') delete source.registry;
742
725
  }
743
- const entry = indexEntryOf({ manifest, version, source, at: now() });
726
+ const versionRecord = versionRecordOf({ manifest, version, source, at: now() });
744
727
  report({ phase: 'index' });
745
728
  const indexed = replicatePush(exec, workdir, branch, baseSha, () => {
746
729
  if (plan.policy !== 'artifacts-in-repo') migrateLegacyIndex(workdir, dir);
747
730
  ensureClaimFree(workdir, dir, claimInput);
731
+ ensureVersionFree(workdir, dir, claimInput);
748
732
  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]);
733
+ const owner = authorTextOf(manifest);
734
+ let next;
735
+ if (current === undefined) {
736
+ next = {
737
+ name: manifest.name,
738
+ ...(owner === '' ? {} : { owner }),
739
+ ...entryMetaOf(manifest),
740
+ versions: [versionRecord],
741
+ };
742
+ } else {
743
+ next = { ...current, versions: sortMarketVersions([versionRecord, ...current.versions]) };
744
+ if (options.metadata === true) {
745
+ const merged = { ...next, ...entryMetaOf(manifest) };
746
+ if (current.owner !== undefined) merged.owner = current.owner;
747
+ next = merged;
748
+ }
749
+ }
750
+ next = mergeEntryLabels(next, options.labels);
751
+ writeEntryFile(workdir, dir, next);
752
752
  indexSha = commitOf(exec, workdir, `chore(market): ${entryName}`, identityOf(manifest));
753
753
  report({ phase: 'index-committed', indexCommit: indexSha });
754
754
  return indexSha;
@@ -830,10 +830,18 @@ export function refreshMarketEntry(options) {
830
830
  if (isDeepStrictEqual(next, existing)) {
831
831
  return { ...base, updated: false, unchanged: true, version: latest, phase: 'done' };
832
832
  }
833
- const at = Date.now();
834
833
  const baseSha = revOf(exec, workdir, 'HEAD');
835
834
  const done = replicatePush(exec, workdir, branch, baseSha, () => {
836
- writeMetaFile(workdir, dir, next, at);
835
+ const fresh = readWorktreeIndex(workdir, dir).plugins.find((one) => one.name === manifest.name);
836
+ if (fresh === undefined) {
837
+ throw new MarketIndexError(`${manifest.name} 还不在索引里 —— 先正常发一版`, 'index-write');
838
+ }
839
+ const updated = { ...fresh };
840
+ for (const key of ['title', 'description', 'author', 'homepage', 'metadata']) {
841
+ if (Object.hasOwn(meta, key)) updated[key] = meta[key];
842
+ else delete updated[key];
843
+ }
844
+ writeEntryFile(workdir, dir, updated);
837
845
  return commitOf(exec, workdir, `chore(market): refresh ${manifest.name}`, identityOf(manifest));
838
846
  });
839
847
  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,11 +53,13 @@ import {
52
53
  authorTextOf,
53
54
  claimProblemText,
54
55
  identityOf,
56
+ labelsOfManifest,
55
57
  marketClaim,
56
58
  marketVersionExists,
57
59
  missingPublisherText,
58
60
  refreshMarketEntry,
59
61
  resolveIndexPlan,
62
+ setManifestLabels,
60
63
  updateMarketIndex,
61
64
  verifyMarketArtifact,
62
65
  } from './market-index.mjs';
@@ -262,6 +265,11 @@ export async function runRelease({
262
265
  if (!CHANGELOG_KINDS.includes(changelogKind)) {
263
266
  throw new ReleaseError(`--changelog-type 只认 ${CHANGELOG_KINDS.join(' / ')}(收到「${changelogKind}」)`);
264
267
  }
268
+ const askedLabels = [...new Set(argValues(args, '--label').map((one) => String(one).trim()))];
269
+ if (askedLabels.some((one) => one === '' || /[\r\n]/u.test(one))) {
270
+ throw new ReleaseError('--label 只认一行短词:不能是空的');
271
+ }
272
+ if (askedLabels.length > 2) throw new ReleaseError('--label 最多两个 —— 一个模块的标签是 1–2 个短词');
265
273
  const manifest = readManifest(root);
266
274
  const unit = manifest?.dsh?.workbench?.unit;
267
275
  if (typeof unit !== 'string' || unit === '') throw new ReleaseError('这不是一个 wb 模块包(没有 dsh.workbench.unit)');
@@ -313,6 +321,14 @@ export async function runRelease({
313
321
  emit(out, json, summary);
314
322
  return summary;
315
323
  }
324
+ const labels = askedLabels;
325
+ const labelsKept = labels.length === 0 || isDeepStrictEqual(labelsOfManifest(manifest), labels);
326
+ const labelsChange = verify || labelsKept ? undefined : `标签 ← ${labels.join(' · ')}`;
327
+ if (!labelsKept && !verify && !dryRun) {
328
+ setManifestLabels(manifest, labels);
329
+ writeManifest(root, manifest);
330
+ }
331
+ const labelsForIndex = labels.length > 0 ? labels : labelsOfManifest(manifest);
316
332
  const explicitApplied = explicitAuthor !== undefined && !verify && !dryRun;
317
333
  if (explicitApplied) {
318
334
  manifest.author = explicitAuthor;
@@ -343,7 +359,7 @@ export async function runRelease({
343
359
  }
344
360
  const authored = authorChangeOf(root, manifest, exec);
345
361
  if (hasFlag(args, '--refresh')) {
346
- if (dryRun) throw new ReleaseError('--refresh 没有预览:它只改索引里的标题 / 作者 / 说明');
362
+ if (dryRun) throw new ReleaseError('--refresh 没有预览:它只改索引里的标题 / 作者 / 说明 / 标签');
347
363
  if (target !== 'market') throw new ReleaseError('--refresh 只对市场索引有意义(--target market)');
348
364
  if (indexPlan === undefined) {
349
365
  throw new ReleaseError('没有可写的 git 索引源 —— 用 --index <仓库> 指定,或先在「模块市场 → 发现」里初始化索引');
@@ -369,8 +385,13 @@ export async function runRelease({
369
385
  },
370
386
  changes:
371
387
  result.updated === true
372
- ? ['市集元数据已刷新', ...(explicitAuthor === undefined ? [] : ['author ← --author'])]
388
+ ? [
389
+ '市集元数据已刷新',
390
+ ...(explicitAuthor === undefined ? [] : ['author ← --author']),
391
+ ...(labelsChange === undefined ? [] : [labelsChange]),
392
+ ]
373
393
  : [],
394
+ ...(labels.length === 0 ? {} : { labels }),
374
395
  install: '',
375
396
  text:
376
397
  result.updated === true
@@ -481,6 +502,7 @@ export async function runRelease({
481
502
  publish,
482
503
  publisher: authorTextOf(manifestNow),
483
504
  metadata: authorChange !== undefined,
505
+ ...(labelsForIndex.length === 0 ? {} : { labels: labelsForIndex }),
484
506
  exec,
485
507
  home,
486
508
  now: () => Date.now(),
@@ -489,6 +511,7 @@ export async function runRelease({
489
511
  let changelogChange;
490
512
  const changesOf = (version) => [
491
513
  ...(authorChange === undefined ? [] : [authorChange]),
514
+ ...(labelsChange === undefined ? [] : [labelsChange]),
492
515
  ...(version === manifest.version ? [] : [`version → ${version}`]),
493
516
  ...(changelogChange === undefined ? [] : [changelogChange]),
494
517
  ];
@@ -708,6 +731,7 @@ export async function runRelease({
708
731
  version,
709
732
  files,
710
733
  changes: changesOf(version),
734
+ ...(labels.length === 0 ? {} : { labels }),
711
735
  ...(changelogReport === undefined ? {} : { changelog: changelogReport }),
712
736
  install: '',
713
737
  market: indexPreviewOf(manifest.name, version),
@@ -730,6 +754,7 @@ export async function runRelease({
730
754
  publish: {},
731
755
  publisher,
732
756
  metadata: authorChange !== undefined,
757
+ ...(labelsForIndex.length === 0 ? {} : { labels: labelsForIndex }),
733
758
  exec,
734
759
  home,
735
760
  now: () => Date.now(),