@dsh-workbench/shared 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/dist/i18n/zh.js CHANGED
@@ -372,7 +372,7 @@ export const zh = {
372
372
  'panels.discover.missingIndex': '仓库里还没有索引文件 {path}',
373
373
  'panels.discover.initTitle': '把这个仓库初始化成模块市场索引?',
374
374
  'panels.discover.initCta': '初始化成市场索引',
375
- 'panels.discover.initWhat': '会在 {branch} 分支写索引目录 market/ 与 README.md,提交一次并推上去;之后从这个工作台发布的模块会把打包好的文件提交到 artifacts/<包名>/<版本>/,条目写到 market/<包名>/<版本>.json。仓库的写权限归它的所有者。',
375
+ 'panels.discover.initWhat': '会在 {branch} 分支写索引目录 market/ 与 README.md,提交一次并推上去;之后从这个工作台发布的模块会把打包好的文件提交到 artifacts/<包名>/,条目写到 market/<包名>.json。仓库的写权限归它的所有者。',
376
376
  'panels.discover.initBranch': '分支',
377
377
  'panels.discover.initBranchHint': '索引和打包文件都发到这个分支;空仓库会新建它。',
378
378
  'panels.discover.initBranchBad': '分支名只能用字母、数字、点、下划线、斜杠或短横线',
@@ -703,6 +703,8 @@ export const zh = {
703
703
  'approval.publish.version': '版本',
704
704
  'approval.publish.author': '作者',
705
705
  'approval.publish.authorMissing': '未署名',
706
+ 'approval.publish.labels': '标签',
707
+ 'approval.publish.labelsMissing': '还没有(发布到市场时补 1–2 个)',
706
708
  'approval.publish.owner': '认领人',
707
709
  'approval.publish.ownerUnowned': '原来没署名',
708
710
  'approval.publish.ownerTaken': '已有主人:{owner}',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/shared",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "工作台与模块共享的纯代码与契约类型:类型、纯逻辑、哑组件、平台解析。宿主侧的东西从 /host 进(不影响浏览器产物)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -263,6 +263,7 @@ export interface PublishPreview {
263
263
  readonly name: string;
264
264
  readonly version: string;
265
265
  readonly author?: string;
266
+ readonly labels?: readonly string[];
266
267
  readonly registry?: string;
267
268
  readonly market?: MarketPublishDestination;
268
269
  readonly claim?: MarketClaim;
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { MarketIndex } from '../../contract/protocol/market-index.js';
3
+ import { assembleMarketIndex, marketEntryFilesOf } from '../market-index.js';
4
+
5
+ describe('市场索引的迁移', () => {
6
+ it('迁移时把老产物路径里的 unit 补进元数据', () => {
7
+ const legacy: MarketIndex = {
8
+ format: 1,
9
+ plugins: [
10
+ {
11
+ name: '@scope/module-x',
12
+ versions: [
13
+ {
14
+ version: '0.1.0',
15
+ source: { type: 'git', url: 'https://git.example.com/m.git', commit: 'a'.repeat(40), path: 'packages/x' },
16
+ },
17
+ ],
18
+ },
19
+ ],
20
+ };
21
+ const files = marketEntryFilesOf(legacy, 'market');
22
+ const entry = JSON.parse(files[0]?.text ?? '{}');
23
+ expect(entry.metadata).toEqual({ unit: 'x' });
24
+ expect(assembleMarketIndex(files).ok).toBe(true);
25
+ });
26
+ });
@@ -8,9 +8,8 @@ import {
8
8
  marketEntryFilesOf,
9
9
  marketEntryPath,
10
10
  marketInstallTarget,
11
- marketMetaPath,
12
11
  marketSourceKey,
13
- parseMarketEntryDoc,
12
+ parseMarketEntryJson,
14
13
  parseMarketIndex,
15
14
  parseMarketIndexJson,
16
15
  parseSourceConfig,
@@ -353,75 +352,69 @@ describe('索引源与安装目标', () => {
353
352
  });
354
353
  });
355
354
 
356
- describe('条目文件:一个版本一个文件', () => {
357
- const versionDoc = (version: string, timestamp: number, title: string) => ({
355
+ describe('条目文件:一个包一个文件', () => {
356
+ const entryOf = () => ({
358
357
  name: '@scope/module-x',
359
- version,
360
- source: { type: 'npm' },
361
- publishTimestamp: timestamp,
362
- title: { zh: title },
358
+ title: { zh: '探针' },
359
+ versions: [{ version: '0.1.0', source: { type: 'npm' as const }, publishTimestamp: 1_760_000_000 }],
363
360
  });
364
361
 
365
- it('路径按包名分层,meta 文件带 epoch 与随机后缀', () => {
366
- expect(marketEntryPath('market', '@scope/module-x', '0.1.0')).toBe('market/@scope/module-x/0.1.0.json');
367
- expect(marketMetaPath('market', '@scope/module-x', 3, 'a1b2c3')).toBe(
368
- 'market/@scope/module-x/meta-3-a1b2c3.json',
369
- );
370
- expect(marketEntryPath('market', 'plain', '1.0.0')).toBe('market/plain/1.0.0.json');
362
+ it('路径是 market/<包名>.json', () => {
363
+ expect(marketEntryPath('market', '@scope/module-x')).toBe('market/@scope/module-x.json');
364
+ expect(marketEntryPath('market', 'plain')).toBe('market/plain.json');
371
365
  });
372
366
 
373
- it('包名/版本号不合法就不给路径(防止写出目录外)', () => {
374
- expect(() => marketEntryPath('market', '../escape', '1.0.0')).toThrow();
375
- expect(() => marketEntryPath('market', 'ok', '../1.0.0')).toThrow();
367
+ it('包名不合法就不给路径(防止写出目录外)', () => {
368
+ expect(() => marketEntryPath('market', '../escape')).toThrow();
369
+ expect(() => marketEntryPath('market', 'Bad Name')).toThrow();
376
370
  });
377
371
 
378
372
  it('单条文档能解析,坏文档整条拒', () => {
379
- const good = parseMarketEntryDoc(versionDoc('0.1.0', 1_760_000_000, '探针'));
373
+ const good = parseMarketEntryJson(JSON.stringify(entryOf()));
380
374
  expect(good.ok).toBe(true);
381
375
  if (!good.ok) return;
382
- expect(good.doc.name).toBe('@scope/module-x');
383
- const bad = parseMarketEntryDoc({ name: 'Bad Name', version: '0.1.0', source: { type: 'npm' } });
384
- expect(bad.ok).toBe(false);
385
- const noSource = parseMarketEntryDoc({ name: '@scope/x', version: '0.1.0' });
386
- expect(noSource.ok).toBe(false);
387
- });
388
-
389
- it('组装:版本全收并按 SemVer 降序,元数据取最新那份', () => {
390
- const files = [
391
- { path: 'market/@scope/module-x/0.1.0.json', text: JSON.stringify(versionDoc('0.1.0', 1_760_000_000, '旧标题')) },
392
- { path: 'market/@scope/module-x/0.2.0.json', text: JSON.stringify(versionDoc('0.2.0', 1_760_000_100, '新标题')) },
393
- { path: 'market/@scope/module-x/meta-1760000200000.json', text: JSON.stringify({
394
- name: '@scope/module-x',
395
- publishTimestamp: 1_760_000_200,
396
- title: { zh: '最新标题' },
397
- }) },
398
- ];
399
- const assembled = assembleMarketIndex(files);
376
+ expect(good.entry.name).toBe('@scope/module-x');
377
+ expect(parseMarketEntryJson(JSON.stringify({ name: 'Bad Name', versions: [] })).ok).toBe(false);
378
+ expect(parseMarketEntryJson(JSON.stringify({ name: '@scope/x' })).ok).toBe(false);
379
+ expect(parseMarketEntryJson('{oops').ok).toBe(false);
380
+ });
381
+
382
+ it('组装:版本按 SemVer 降序,一个包一条', () => {
383
+ const assembled = assembleMarketIndex([
384
+ {
385
+ path: 'market/@scope/module-x.json',
386
+ text: JSON.stringify({
387
+ ...entryOf(),
388
+ versions: [
389
+ { version: '0.2.0', source: { type: 'npm' }, publishTimestamp: 1_760_000_100 },
390
+ { version: '0.1.0', source: { type: 'npm' }, publishTimestamp: 1_760_000_000 },
391
+ ],
392
+ }),
393
+ },
394
+ ]);
400
395
  expect(assembled.ok).toBe(true);
401
396
  if (!assembled.ok) return;
402
- const entry = assembled.index.plugins[0];
403
- expect(entry?.versions.map((one) => one.version)).toEqual(['0.2.0', '0.1.0']);
404
- expect(entry?.title?.zh).toBe('最新标题');
397
+ expect(assembled.index.plugins[0]?.versions.map((one) => one.version)).toEqual(['0.2.0', '0.1.0']);
405
398
  });
406
399
 
407
- it('组装:同一个版本出现两次就整份拒(那是真重复发布)', () => {
400
+ it('组装:同一个包出现两次就整份拒', () => {
408
401
  const assembled = assembleMarketIndex([
409
- { path: 'a/0.1.0.json', text: JSON.stringify(versionDoc('0.1.0', 1_760_000_000, '甲')) },
410
- { path: 'b/0.1.0.json', text: JSON.stringify(versionDoc('0.1.0', 1_760_000_100, '乙')) },
402
+ { path: 'a.json', text: JSON.stringify(entryOf()) },
403
+ { path: 'b.json', text: JSON.stringify(entryOf()) },
411
404
  ]);
412
405
  expect(assembled.ok).toBe(false);
413
406
  if (assembled.ok) return;
414
- expect(assembled.problems.some((one) => one.kind === 'duplicate-version')).toBe(true);
407
+ expect(assembled.problems.some((one) => one.kind === 'duplicate-name')).toBe(true);
415
408
  });
416
409
 
417
410
  it('从目录读只收 .json,README 不当条目', () => {
418
411
  const dir = mkdtempSync(join(tmpdir(), 'wb-entry-'));
419
412
  try {
420
- mkdirSync(join(dir, '@scope/module-x'), { recursive: true });
413
+ mkdirSync(dir, { recursive: true });
421
414
  writeFileSync(join(dir, 'README.md'), 'index');
422
- writeFileSync(join(dir, '@scope/module-x', '0.1.0.json'), JSON.stringify(versionDoc('0.1.0', 1_760_000_000, '探针')));
415
+ writeFileSync(join(dir, '@scope-module-x.json'), JSON.stringify(entryOf()));
423
416
  const files = collectMarketEntryFiles(dir);
424
- expect(files.map((one) => one.path)).toEqual(['@scope/module-x/0.1.0.json']);
417
+ expect(files.map((one) => one.path)).toEqual(['@scope-module-x.json']);
425
418
  const assembled = assembleMarketIndex(files);
426
419
  expect(assembled.ok).toBe(true);
427
420
  if (!assembled.ok) return;
@@ -431,7 +424,7 @@ describe('条目文件:一个版本一个文件', () => {
431
424
  }
432
425
  });
433
426
 
434
- it('迁移:一份 format 1 能拆成条目文件,再组装回来一模一样', () => {
427
+ it('迁移:一份 format 1 拆成每包一个文件,再组装回来一模一样', () => {
435
428
  const legacy: MarketIndex = {
436
429
  format: 1,
437
430
  plugins: [
@@ -447,10 +440,7 @@ describe('条目文件:一个版本一个文件', () => {
447
440
  ],
448
441
  };
449
442
  const files = marketEntryFilesOf(legacy, 'market');
450
- expect(files.map((one) => one.path)).toEqual([
451
- 'market/@scope/module-x/0.2.0.json',
452
- 'market/@scope/module-x/0.1.0.json',
453
- ]);
443
+ expect(files.map((one) => one.path)).toEqual(['market/@scope/module-x.json']);
454
444
  const assembled = assembleMarketIndex(files);
455
445
  expect(assembled.ok).toBe(true);
456
446
  if (!assembled.ok) return;
@@ -58,10 +58,6 @@ export function marketArtifactRoot(name: string): string {
58
58
  return `${MARKET_ARTIFACTS_DIR}/${name}`;
59
59
  }
60
60
 
61
- export function marketArtifactPathFor(name: string, version: string): string {
62
- return `${MARKET_ARTIFACTS_DIR}/${name}/${version}`;
63
- }
64
-
65
61
  export function entryUnitOf(entry: MarketIndexEntry): string | undefined {
66
62
  const unit = entry.metadata?.unit?.trim();
67
63
  if (unit !== undefined && unit !== '') return unit;
@@ -1,45 +1,4 @@
1
- import type {
2
- MarketIndex,
3
- MarketIndexEngines,
4
- MarketIndexEntryMetadata,
5
- MarketIndexProblem,
6
- MarketIndexSource,
7
- MarketLocalizedText,
8
- } from '../contract/protocol/market-index.js';
9
-
10
- export interface MarketVersionDoc {
11
- readonly name: string;
12
- readonly version: string;
13
- readonly source: MarketIndexSource;
14
- readonly publishTimestamp?: number;
15
- readonly engines?: MarketIndexEngines;
16
- readonly title?: MarketLocalizedText;
17
- readonly description?: MarketLocalizedText;
18
- readonly author?: string;
19
- readonly owner?: string;
20
- readonly homepage?: string;
21
- readonly icon?: string;
22
- readonly metadata?: MarketIndexEntryMetadata;
23
- }
24
-
25
- export interface MarketMetaDoc {
26
- readonly name: string;
27
- readonly epoch?: number;
28
- readonly publishTimestamp: number;
29
- readonly title?: MarketLocalizedText;
30
- readonly description?: MarketLocalizedText;
31
- readonly author?: string;
32
- readonly owner?: string;
33
- readonly homepage?: string;
34
- readonly icon?: string;
35
- readonly metadata?: MarketIndexEntryMetadata;
36
- }
37
-
38
- export type MarketEntryDoc = MarketVersionDoc | MarketMetaDoc;
39
-
40
- export type MarketEntryDocParse =
41
- | { readonly ok: true; readonly doc: MarketEntryDoc }
42
- | { readonly ok: false; readonly problems: readonly MarketIndexProblem[] };
1
+ import type { MarketIndex, MarketIndexProblem } from '../contract/protocol/market-index.js';
43
2
 
44
3
  export interface MarketEntryFile {
45
4
  readonly path: string;
@@ -49,14 +8,3 @@ export interface MarketEntryFile {
49
8
  export type MarketIndexAssemble =
50
9
  | { readonly ok: true; readonly index: MarketIndex }
51
10
  | { readonly ok: false; readonly problems: readonly MarketIndexProblem[] };
52
-
53
- export interface MarketDocFields {
54
- readonly name: string;
55
- readonly title?: MarketLocalizedText;
56
- readonly description?: MarketLocalizedText;
57
- readonly author?: string;
58
- readonly owner?: string;
59
- readonly homepage?: string;
60
- readonly icon?: string;
61
- readonly metadata?: MarketIndexEntryMetadata;
62
- }
@@ -13,7 +13,6 @@ import type {
13
13
  } from '../contract/protocol/market-index.js';
14
14
  import { MARKET_INDEX_DIR, MARKET_INDEX_FORMAT } from '../contract/protocol/market-index.js';
15
15
  import { marketText } from '../lib/market-index.js';
16
- import type { MarketDocFields, MarketEntryDocParse } from './market-index-doc.js';
17
16
  import { compareVersions, isValidVersion } from './version.js';
18
17
 
19
18
  const NPM_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
@@ -388,95 +387,32 @@ export function parseSourceConfig(raw: unknown): SourceConfigParse {
388
387
  }
389
388
 
390
389
 
391
- function docFieldsOf(record: Readonly<Record<string, unknown>>, at: string): MarketDocFields | MarketIndexProblem {
392
- const name = asText(record['name']);
393
- if (name === '' || name.length > 214 || !NPM_NAME.test(name)) {
394
- return problem('name', `不是合法 npm 包名:${JSON.stringify(name)}`, at);
395
- }
396
- const icon = asText(record['icon']);
397
- if (icon !== '') {
398
- const badIcon = iconProblem(icon);
399
- if (badIcon !== undefined) return badIcon;
400
- }
401
- const owner = asText(record['owner']);
402
- if (owner !== '') {
403
- const badOwner = ownerProblem(owner);
404
- if (badOwner !== undefined) return badOwner;
405
- }
406
- const metadata = metadataOf(record['metadata']);
407
- if (metadata?.unit !== undefined && !UNIT_ID.test(metadata.unit)) {
408
- return problem('unit', `metadata.unit 只能是小写字母开头的 a-z0-9-(现在是 ${JSON.stringify(metadata.unit)})`, at);
409
- }
410
- const title = asLocalized(record['title']);
411
- const description = asLocalized(record['description']);
412
- const author = asText(record['author']);
413
- const homepage = asText(record['homepage']);
414
- return {
415
- name,
416
- ...(title === undefined ? {} : { title }),
417
- ...(description === undefined ? {} : { description }),
418
- ...(author === '' ? {} : { author }),
419
- ...(owner === '' ? {} : { owner }),
420
- ...(homepage === '' ? {} : { homepage }),
421
- ...(icon === '' ? {} : { icon }),
422
- ...(metadata === undefined ? {} : { metadata }),
423
- };
424
- }
425
-
426
- export function parseMarketEntryDoc(raw: unknown): MarketEntryDocParse {
427
- const record = asRecord(raw);
428
- if (record === undefined) return { ok: false, problems: [problem('entry', '条目文件必须是一个 JSON 对象')] };
429
- const fields = docFieldsOf(record, 'entry');
430
- if ('kind' in fields) return { ok: false, problems: [fields] };
431
- const version = asText(record['version']);
432
- const timestamp = record['publishTimestamp'];
433
- if (timestamp !== undefined && (!Number.isInteger(timestamp) || (timestamp as number) < 0)) {
434
- return { ok: false, problems: [problem('timestamp', 'publishTimestamp 必须是 Unix 秒整数', 'entry')] };
435
- }
436
- if (version === '') {
437
- if (timestamp === undefined) {
438
- return { ok: false, problems: [problem('timestamp', '元数据文件要有 publishTimestamp', 'entry')] };
439
- }
440
- const epoch = record['epoch'];
441
- if (epoch !== undefined && (!Number.isInteger(epoch) || (epoch as number) < 0)) {
442
- return { ok: false, problems: [problem('timestamp', 'epoch 必须是非负整数', 'entry')] };
443
- }
444
- return {
445
- ok: true,
446
- doc: { ...fields, publishTimestamp: timestamp as number, ...(epoch === undefined ? {} : { epoch: epoch as number }) },
447
- };
448
- }
449
- if (!isValidVersion(version)) {
450
- return { ok: false, problems: [problem('version', `版本号不是合法 SemVer:${JSON.stringify(version)}`, 'entry')] };
451
- }
452
- const source = sourceOf(record['source'], 'entry.source');
453
- if ('kind' in source) return { ok: false, problems: [source] };
454
- const engines = asRecord(record['engines']);
455
- const dsh = engines === undefined ? '' : asText(engines['dsh']);
456
- return {
457
- ok: true,
458
- doc: {
459
- ...fields,
460
- version,
461
- source,
462
- ...(timestamp === undefined ? {} : { publishTimestamp: timestamp as number }),
463
- ...(dsh === '' ? {} : { engines: { dsh } }),
464
- },
465
- };
466
- }
467
390
  function indexPathBase(dir: string): string {
468
391
  return dir.replace(/^\/+|\/+$/g, '') || MARKET_INDEX_DIR;
469
392
  }
470
393
 
471
- export function marketEntryPath(dir: string, name: string, version: string): string {
394
+ export function marketEntryPath(dir: string, name: string): string {
472
395
  if (!NPM_NAME.test(name)) throw new Error(`不是合法包名:${JSON.stringify(name)}`);
473
- if (!isValidVersion(version)) throw new Error(`不是合法版本号:${JSON.stringify(version)}`);
474
- return `${indexPathBase(dir)}/${name}/${version}.json`;
396
+ return `${indexPathBase(dir)}/${name}.json`;
475
397
  }
476
398
 
477
- export function marketMetaPath(dir: string, name: string, epoch: number, suffix: string): string {
478
- if (!NPM_NAME.test(name)) throw new Error(`不是合法包名:${JSON.stringify(name)}`);
479
- if (!Number.isInteger(epoch) || epoch < 0) throw new Error(`epoch 不合法:${String(epoch)}`);
480
- if (!/^[a-z0-9]{4,16}$/u.test(suffix)) throw new Error(`后缀不合法:${suffix}`);
481
- return `${indexPathBase(dir)}/${name}/meta-${String(epoch)}-${suffix}.json`;
399
+ export type MarketEntryParse =
400
+ | { readonly ok: true; readonly entry: MarketIndexEntry }
401
+ | { readonly ok: false; readonly problems: readonly MarketIndexProblem[] };
402
+
403
+ export function parseMarketEntryJson(text: string): MarketEntryParse {
404
+ let raw: unknown;
405
+ try {
406
+ raw = JSON.parse(text);
407
+ } catch (error) {
408
+ return {
409
+ ok: false,
410
+ problems: [problem('not-json', `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`, 'entry')],
411
+ };
412
+ }
413
+ const parsed = entryOf(raw, 'entry');
414
+ if ('kind' in parsed) return { ok: false, problems: [parsed] };
415
+ return { ok: true, entry: parsed };
482
416
  }
417
+
418
+
@@ -1,40 +1,26 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { join, relative, sep } from 'node:path';
3
- import type {
4
- MarketIndex,
5
- MarketIndexEntry,
6
- MarketIndexProblem,
7
- MarketIndexVersion,
8
- } from '../contract/protocol/market-index.js';
3
+ import type { MarketIndex, MarketIndexEntry, MarketIndexProblem } from '../contract/protocol/market-index.js';
9
4
  import { MARKET_INDEX_DIR, MARKET_INDEX_FORMAT } from '../contract/protocol/market-index.js';
10
-
11
- import type {
12
- MarketDocFields,
13
- MarketEntryDoc,
14
- MarketEntryFile,
15
- MarketIndexAssemble,
16
- MarketMetaDoc,
17
- MarketVersionDoc,
18
- } from './market-index-doc.js';
19
- import {
20
- marketEntryPath,
21
- parseMarketEntryDoc,
22
- parseMarketIndexJson,
23
- sortMarketVersions,
24
- } from './market-index-parse.js';
5
+ import type { MarketEntryFile, MarketIndexAssemble } from './market-index-doc.js';
6
+ import { entryUnitOf } from './market-index-claim.js';
7
+ import { marketEntryPath, parseMarketEntryJson, parseMarketIndexJson, sortMarketVersions } from './market-index-parse.js';
25
8
 
26
9
  export const MARKET_ENTRIES_README_LINES: readonly string[] = [
27
10
  '# 索引目录',
28
11
  '',
29
- '一个包一个目录,一个版本一个文件:`market/<包名>/<版本>.json`;产物在 `artifacts/<包名>/<版本>/`。',
12
+ '一个包一个文件:`market/<包名>.json`,里面是这一个包的元数据与所有版本;',
13
+ '',
14
+ '产物在 `artifacts/<包名>/`(只放最新一版,老版本按条目里的 commit 取历史)。',
30
15
  '',
31
- '`meta-<epoch>-<随机后缀>.json` 是只改标题 / 署名 / 说明时追加的元数据快照,读取时按 epoch 取最新的一份。',
16
+ '发布时只改自己包的文件;包与包之间不共享文件。',
32
17
  ];
33
18
 
34
19
  export const MARKET_INDEX_README_LINES: readonly string[] = [
35
20
  '# 模块市场索引',
36
21
  '',
37
- '这个仓库给 DeepSeek Harness 工作台当模块市场索引:条目在 `market/<包名>/<版本>.json`,产物在 `artifacts/<包名>/<版本>/`。',
22
+ '这个仓库给 DeepSeek Harness 工作台当模块市场索引:一个包一个文件 `market/<包名>.json`,',
23
+ '产物在 `artifacts/<包名>/`;老仓库的 `market.json` 会在第一次发布时自动拆开。',
38
24
  '',
39
25
  '## 发布模块',
40
26
  '',
@@ -44,8 +30,7 @@ export const MARKET_INDEX_README_LINES: readonly string[] = [
44
30
  'dsh-wb-module release --index <本仓库地址> --index-policy artifacts-in-repo',
45
31
  '```',
46
32
  '',
47
- '产物会提交到 `artifacts/<包名>/<版本>/`,条目提交到 `market/<包名>/<版本>.json`。',
48
- '发布只新增文件、不改已有文件,两台机器同时发也不会冲突;老条目的 `packages/<包名>/` 路径继续可用。',
33
+ '发布只会改自己包的那一个文件;包与包之间不会碰同一个文件。',
49
34
  '',
50
35
  '一个包名归第一次发布的人(条目文件里的 `owner`)。换一个署名发同名会被拒;要换人,先把这一条的 `owner` 改成新的人。',
51
36
  '同一个市场里,一个模块 id(`metadata.unit`)只能被一个条目使用;另一个包名想用同一个 id / 产物目录也会被拒。',
@@ -56,14 +41,7 @@ export const MARKET_INDEX_README_LINES: readonly string[] = [
56
41
  '',
57
42
  ];
58
43
  export { marketBranchOk, marketInstallTarget, marketText } from '../lib/market-index.js';
59
- export type {
60
- MarketEntryDoc,
61
- MarketEntryDocParse,
62
- MarketEntryFile,
63
- MarketIndexAssemble,
64
- MarketMetaDoc,
65
- MarketVersionDoc,
66
- } from './market-index-doc.js';
44
+ export type { MarketEntryFile, MarketIndexAssemble } from './market-index-doc.js';
67
45
  export {
68
46
  compareSemver,
69
47
  filterMarketIndex,
@@ -71,9 +49,8 @@ export {
71
49
  httpUrlProblem,
72
50
  marketEntryLabels,
73
51
  marketEntryPath,
74
- marketMetaPath,
75
52
  marketSourceKey,
76
- parseMarketEntryDoc,
53
+ parseMarketEntryJson,
77
54
  parseMarketIndex,
78
55
  parseMarketIndexJson,
79
56
  parseSourceConfig,
@@ -82,116 +59,45 @@ export {
82
59
  } from './market-index-parse.js';
83
60
  export type { SourceConfigParse } from './market-index-parse.js';
84
61
 
85
-
86
62
  function problem(kind: MarketIndexProblem['kind'], detail: string, at?: string): MarketIndexProblem {
87
63
  return { kind, detail, ...(at === undefined ? {} : { at }) };
88
64
  }
89
65
 
90
- function docFieldsFromEntry(entry: MarketIndexEntry): MarketDocFields {
91
- return {
92
- name: entry.name,
93
- ...(entry.title === undefined ? {} : { title: entry.title }),
94
- ...(entry.description === undefined ? {} : { description: entry.description }),
95
- ...(entry.author === undefined ? {} : { author: entry.author }),
96
- ...(entry.owner === undefined ? {} : { owner: entry.owner }),
97
- ...(entry.homepage === undefined ? {} : { homepage: entry.homepage }),
98
- ...(entry.icon === undefined ? {} : { icon: entry.icon }),
99
- ...(entry.metadata === undefined ? {} : { metadata: entry.metadata }),
100
- };
101
- }
102
-
103
- export function marketVersionDocOf(entry: MarketIndexEntry, version: MarketIndexVersion): MarketVersionDoc {
104
- return { ...docFieldsFromEntry(entry), ...version };
105
- }
106
-
107
- export function marketMetaDocOf(entry: MarketIndexEntry, epoch: number, at: number): MarketMetaDoc {
108
- return { ...docFieldsFromEntry(entry), epoch, publishTimestamp: at };
109
- }
110
-
111
66
  export function assembleMarketIndex(files: readonly MarketEntryFile[]): MarketIndexAssemble {
112
67
  const problems: MarketIndexProblem[] = [];
113
- interface Group {
114
- versions: Map<string, MarketIndexVersion>;
115
- meta: { epoch: number; at: number; path: string; doc: MarketEntryDoc } | undefined;
116
- fallback: { at: number; path: string; doc: MarketEntryDoc } | undefined;
117
- }
118
- const groups = new Map<string, Group>();
68
+ const entries = new Map<string, MarketIndexEntry>();
119
69
  const ordered = [...files].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
120
70
  for (const file of ordered) {
121
- let raw: unknown;
122
- try {
123
- raw = JSON.parse(file.text) as unknown;
124
- } catch (error) {
125
- problems.push(
126
- problem('not-json', `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`, file.path),
127
- );
128
- continue;
129
- }
130
- const parsed = parseMarketEntryDoc(raw);
71
+ if (!file.path.endsWith('.json')) continue;
72
+ const parsed = parseMarketEntryJson(file.text);
131
73
  if (!parsed.ok) {
132
74
  for (const one of parsed.problems) problems.push({ ...one, at: file.path });
133
75
  continue;
134
76
  }
135
- const doc = parsed.doc;
136
- const group =
137
- groups.get(doc.name) ?? { versions: new Map<string, MarketIndexVersion>(), meta: undefined, fallback: undefined };
138
- const at = doc.publishTimestamp ?? 0;
139
- if ('version' in doc) {
140
- if (group.versions.has(doc.version)) {
141
- problems.push(problem('duplicate-version', `版本重复:${doc.version}`, file.path));
142
- }
143
- group.versions.set(doc.version, {
144
- version: doc.version,
145
- source: doc.source,
146
- ...(doc.publishTimestamp === undefined ? {} : { publishTimestamp: doc.publishTimestamp }),
147
- ...(doc.engines === undefined ? {} : { engines: doc.engines }),
148
- });
149
- const newerFallback =
150
- group.fallback === undefined ||
151
- at > group.fallback.at ||
152
- (at === group.fallback.at && file.path > group.fallback.path);
153
- if (newerFallback) group.fallback = { at, path: file.path, doc };
154
- } else {
155
- const epoch = doc.epoch ?? 0;
156
- const newer =
157
- group.meta === undefined ||
158
- epoch > group.meta.epoch ||
159
- (epoch === group.meta.epoch && at > group.meta.at) ||
160
- (epoch === group.meta.epoch && at === group.meta.at && file.path > group.meta.path);
161
- if (newer) group.meta = { epoch, at, path: file.path, doc };
77
+ const entry = parsed.entry;
78
+ if (entries.has(entry.name)) {
79
+ problems.push(problem('duplicate-name', `插件名重复:${entry.name}`, file.path));
80
+ continue;
162
81
  }
163
- groups.set(doc.name, group);
82
+ entries.set(entry.name, { ...entry, versions: sortMarketVersions(entry.versions) });
164
83
  }
165
84
  if (problems.length > 0) return { ok: false, problems };
166
- const plugins: MarketIndexEntry[] = [...groups.entries()].map(([name, group]) => {
167
- const meta = group.meta?.doc ?? group.fallback?.doc;
168
- return {
169
- name,
170
- versions: sortMarketVersions([...group.versions.values()]),
171
- ...(meta?.title === undefined ? {} : { title: meta.title }),
172
- ...(meta?.description === undefined ? {} : { description: meta.description }),
173
- ...(meta?.author === undefined ? {} : { author: meta.author }),
174
- ...(meta?.owner === undefined ? {} : { owner: meta.owner }),
175
- ...(meta?.homepage === undefined ? {} : { homepage: meta.homepage }),
176
- ...(meta?.icon === undefined ? {} : { icon: meta.icon }),
177
- ...(meta?.metadata === undefined ? {} : { metadata: meta.metadata }),
178
- };
179
- });
180
- plugins.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
85
+ const plugins = [...entries.values()].sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
181
86
  return { ok: true, index: { format: MARKET_INDEX_FORMAT, plugins } };
182
87
  }
183
88
 
89
+ export function marketEntryFileOf(entry: MarketIndexEntry, dir: string = MARKET_INDEX_DIR): MarketEntryFile {
90
+ return { path: marketEntryPath(dir, entry.name), text: `${JSON.stringify(entry, null, 2)}\n` };
91
+ }
92
+
184
93
  export function marketEntryFilesOf(index: MarketIndex, dir: string = MARKET_INDEX_DIR): readonly MarketEntryFile[] {
185
- const files: MarketEntryFile[] = [];
186
- for (const entry of index.plugins) {
187
- for (const version of entry.versions) {
188
- files.push({
189
- path: marketEntryPath(dir, entry.name, version.version),
190
- text: `${JSON.stringify(marketVersionDocOf(entry, version), null, 2)}\n`,
191
- });
192
- }
193
- }
194
- return files;
94
+ return index.plugins.map((entry) => marketEntryFileOf(healedUnit(entry), dir));
95
+ }
96
+
97
+ function healedUnit(entry: MarketIndexEntry): MarketIndexEntry {
98
+ if (entry.metadata?.unit !== undefined) return entry;
99
+ const unit = entryUnitOf(entry);
100
+ return unit === undefined ? entry : { ...entry, metadata: { ...entry.metadata, unit } };
195
101
  }
196
102
 
197
103
  export function collectMarketEntryFiles(dir: string): readonly MarketEntryFile[] {
package/src/i18n/en.ts CHANGED
@@ -383,7 +383,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
383
383
  'panels.discover.missingIndex': 'This repository has no {path} yet.',
384
384
  'panels.discover.initTitle': 'Initialize this repository as a module market index?',
385
385
  'panels.discover.initCta': 'Initialize market index',
386
- 'panels.discover.initWhat': 'It will write the index directory market/ and README.md on {branch}, commit once, and push. Modules you publish from this workbench will commit their build files under artifacts/<package-name>/<version>/ and their entry under market/<package-name>/<version>.json. The repository stays owned by its owner.',
386
+ 'panels.discover.initWhat': 'It will write the index directory market/ and README.md on {branch}, commit once, and push. Modules you publish from this workbench will commit their build files under artifacts/<package-name>/ and their entry in market/<package-name>.json. The repository stays owned by its owner.',
387
387
  'panels.discover.initBranch': 'Branch',
388
388
  'panels.discover.initBranchHint': 'The index and build files go to this branch; an empty repository gets it created.',
389
389
  'panels.discover.initBranchBad': 'Use only letters, digits, dots, underscores, slashes, or dashes in a branch name.',
@@ -722,6 +722,8 @@ export const en: Record<keyof WorkbenchMessages, string> = {
722
722
  'approval.publish.version': 'Version',
723
723
  'approval.publish.author': 'Author',
724
724
  'approval.publish.authorMissing': 'unsigned',
725
+ 'approval.publish.labels': 'Labels',
726
+ 'approval.publish.labelsMissing': 'none yet (1–2 picked on publish)',
725
727
  'approval.publish.owner': 'Owner',
726
728
  'approval.publish.ownerUnowned': 'originally unattributed',
727
729
  'approval.publish.ownerTaken': 'Owned by {owner}',