@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/contract/protocol/market-index.d.ts +1 -0
- package/dist/host/market-index-claim.d.ts +0 -1
- package/dist/host/market-index-claim.js +0 -3
- package/dist/host/market-index-doc.d.ts +1 -45
- package/dist/host/market-index-parse.d.ts +9 -4
- package/dist/host/market-index-parse.js +17 -92
- package/dist/host/market-index.d.ts +5 -6
- package/dist/host/market-index.js +29 -87
- package/dist/i18n/en.js +3 -1
- package/dist/i18n/index.d.ts +4 -2
- package/dist/i18n/zh.d.ts +3 -1
- package/dist/i18n/zh.js +3 -1
- package/package.json +1 -1
- package/src/contract/protocol/market-index.ts +1 -0
- package/src/host/__tests__/market-index-assemble.test.ts +26 -0
- package/src/host/__tests__/market-index.test.ts +41 -51
- package/src/host/market-index-claim.ts +0 -4
- package/src/host/market-index-doc.ts +1 -53
- package/src/host/market-index-parse.ts +21 -85
- package/src/host/market-index.ts +33 -127
- package/src/i18n/en.ts +3 -1
- package/src/i18n/zh.ts +3 -1
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
|
|
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
|
@@ -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
|
-
|
|
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
|
|
355
|
+
describe('条目文件:一个包一个文件', () => {
|
|
356
|
+
const entryOf = () => ({
|
|
358
357
|
name: '@scope/module-x',
|
|
359
|
-
|
|
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('
|
|
366
|
-
expect(marketEntryPath('market', '@scope/module-x'
|
|
367
|
-
expect(
|
|
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'
|
|
375
|
-
expect(() => marketEntryPath('market', '
|
|
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 =
|
|
373
|
+
const good = parseMarketEntryJson(JSON.stringify(entryOf()));
|
|
380
374
|
expect(good.ok).toBe(true);
|
|
381
375
|
if (!good.ok) return;
|
|
382
|
-
expect(good.
|
|
383
|
-
|
|
384
|
-
expect(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
|
410
|
-
{ path: 'b
|
|
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-
|
|
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(
|
|
413
|
+
mkdirSync(dir, { recursive: true });
|
|
421
414
|
writeFileSync(join(dir, 'README.md'), 'index');
|
|
422
|
-
writeFileSync(join(dir, '@scope
|
|
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
|
|
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
|
|
394
|
+
export function marketEntryPath(dir: string, name: string): string {
|
|
472
395
|
if (!NPM_NAME.test(name)) throw new Error(`不是合法包名:${JSON.stringify(name)}`);
|
|
473
|
-
|
|
474
|
-
return `${indexPathBase(dir)}/${name}/${version}.json`;
|
|
396
|
+
return `${indexPathBase(dir)}/${name}.json`;
|
|
475
397
|
}
|
|
476
398
|
|
|
477
|
-
export
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
|
package/src/host/market-index.ts
CHANGED
|
@@ -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
|
|
12
|
-
|
|
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
|
-
'
|
|
12
|
+
'一个包一个文件:`market/<包名>.json`,里面是这一个包的元数据与所有版本;',
|
|
13
|
+
'',
|
|
14
|
+
'产物在 `artifacts/<包名>/`(只放最新一版,老版本按条目里的 commit 取历史)。',
|
|
30
15
|
'',
|
|
31
|
-
'
|
|
16
|
+
'发布时只改自己包的文件;包与包之间不共享文件。',
|
|
32
17
|
];
|
|
33
18
|
|
|
34
19
|
export const MARKET_INDEX_README_LINES: readonly string[] = [
|
|
35
20
|
'# 模块市场索引',
|
|
36
21
|
'',
|
|
37
|
-
'这个仓库给 DeepSeek Harness
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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}',
|