@dsh-workbench/shared 1.1.0 → 1.1.2
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 +29 -1
- package/dist/contract/unit-declaration.js +2 -6
- package/dist/contract/unit.d.ts +4 -3
- package/dist/host/changelog.d.ts +7 -0
- package/dist/host/changelog.js +12 -0
- package/dist/host/market-index-claim.d.ts +18 -0
- package/dist/host/market-index-claim.js +83 -0
- package/dist/host/market-index.js +34 -2
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/i18n/en.js +29 -5
- package/dist/i18n/index.d.ts +30 -6
- package/dist/i18n/zh.d.ts +29 -5
- package/dist/i18n/zh.js +29 -5
- package/dist/testing/needs.js +5 -2
- package/package.json +1 -1
- package/src/contract/__tests__/unit-declaration.test.ts +2 -1
- package/src/contract/__tests__/unit.test.ts +1 -1
- package/src/contract/protocol/market-index.ts +19 -2
- package/src/contract/unit-declaration.ts +2 -7
- package/src/contract/unit.ts +4 -3
- package/src/host/__tests__/changelog.test.ts +33 -0
- package/src/host/__tests__/market-index.test.ts +166 -0
- package/src/host/changelog.ts +19 -0
- package/src/host/market-index-claim.ts +91 -0
- package/src/host/market-index.ts +33 -2
- package/src/host.ts +1 -0
- package/src/i18n/en.ts +29 -5
- package/src/i18n/zh.ts +29 -5
- package/src/testing/__tests__/declaration.test.ts +2 -1
- package/src/testing/needs.ts +5 -2
package/dist/i18n/zh.d.ts
CHANGED
|
@@ -297,12 +297,19 @@ export declare const zh: {
|
|
|
297
297
|
readonly 'panels.discover.addingSource': "正在添加…";
|
|
298
298
|
readonly 'panels.discover.addFirstSource': "添加索引源";
|
|
299
299
|
readonly 'panels.discover.removeSource': "移除";
|
|
300
|
-
readonly 'panels.discover.
|
|
301
|
-
readonly 'panels.discover.
|
|
302
|
-
readonly 'panels.discover.
|
|
303
|
-
readonly 'panels.discover.
|
|
300
|
+
readonly 'panels.discover.removeTitle': "移除这个索引源?";
|
|
301
|
+
readonly 'panels.discover.removeBody': "会把「{label}」从这份列表里移除;仓库本身不会被删,之后还能再添加。";
|
|
302
|
+
readonly 'panels.discover.sourceActions': "「{label}」的操作";
|
|
303
|
+
readonly 'panels.discover.sourceStateWritable': "可发布";
|
|
304
|
+
readonly 'panels.discover.sourceStateReadOnly': "仅读取模块列表";
|
|
305
|
+
readonly 'panels.discover.sourceEnable': "设为可发布";
|
|
306
|
+
readonly 'panels.discover.sourceDisable': "设为只读";
|
|
307
|
+
readonly 'panels.discover.sourceWorking': "设置中…";
|
|
308
|
+
readonly 'panels.discover.sourceEnabled': "已把「{label}」设为可发布";
|
|
309
|
+
readonly 'panels.discover.sourceReadOnly': "已把「{label}」设为只读";
|
|
304
310
|
readonly 'panels.discover.refresh': "刷新";
|
|
305
311
|
readonly 'panels.discover.search': "搜索模块";
|
|
312
|
+
readonly 'panels.discover.clearSearch': "清除搜索";
|
|
306
313
|
readonly 'panels.discover.all': "全部";
|
|
307
314
|
readonly 'panels.discover.sourceLabel': "名字";
|
|
308
315
|
readonly 'panels.discover.sourceKind': "类型";
|
|
@@ -326,7 +333,7 @@ export declare const zh: {
|
|
|
326
333
|
readonly 'panels.discover.missingIndex': "仓库里还没有索引文件 {path}";
|
|
327
334
|
readonly 'panels.discover.initTitle': "把这个仓库初始化成模块市场索引?";
|
|
328
335
|
readonly 'panels.discover.initCta': "初始化成市场索引";
|
|
329
|
-
readonly 'panels.discover.initWhat': "会在 {branch} 分支写 market.json 与 README.md
|
|
336
|
+
readonly 'panels.discover.initWhat': "会在 {branch} 分支写 market.json 与 README.md,提交一次并推上去;之后从这个工作台发布的模块会把打包好的文件提交到按包名分的 packages/<包名>/。仓库的写权限归它的所有者。";
|
|
330
337
|
readonly 'panels.discover.initBranch': "分支";
|
|
331
338
|
readonly 'panels.discover.initBranchHint': "索引和打包文件都发到这个分支;空仓库会新建它。";
|
|
332
339
|
readonly 'panels.discover.initBranchBad': "分支名只能用字母、数字、点、下划线、斜杠或短横线";
|
|
@@ -636,6 +643,13 @@ export declare const zh: {
|
|
|
636
643
|
readonly 'approval.publish.version': "版本";
|
|
637
644
|
readonly 'approval.publish.author': "作者";
|
|
638
645
|
readonly 'approval.publish.authorMissing': "未署名";
|
|
646
|
+
readonly 'approval.publish.owner': "认领人";
|
|
647
|
+
readonly 'approval.publish.ownerUnowned': "原来没署名";
|
|
648
|
+
readonly 'approval.publish.ownerTaken': "已有主人:{owner}";
|
|
649
|
+
readonly 'approval.publish.unit': "模块 id";
|
|
650
|
+
readonly 'approval.publish.unitTaken': "已被 {entry} 占用";
|
|
651
|
+
readonly 'approval.publish.path': "产物目录";
|
|
652
|
+
readonly 'approval.publish.pathTaken': "已被 {entry} 占用";
|
|
639
653
|
readonly 'approval.publish.registry': "源";
|
|
640
654
|
readonly 'approval.publish.repo': "仓库";
|
|
641
655
|
readonly 'approval.publish.branch': "分支";
|
|
@@ -646,6 +660,7 @@ export declare const zh: {
|
|
|
646
660
|
readonly 'approval.publish.twoCommits': "打包文件 + market.json 两次提交,一次推送";
|
|
647
661
|
readonly 'approval.publish.reading': "读取中…";
|
|
648
662
|
readonly 'approval.publish.changelog': "{version} 的更新日志";
|
|
663
|
+
readonly 'approval.publish.changelogPending': "{version} 这一版会写进更新日志,随包发布";
|
|
649
664
|
readonly 'approval.publish.changelogMissing': "这一版还没有更新日志({version}),发布会被拦下";
|
|
650
665
|
readonly 'approval.publish.targetAuto': "按配置默认";
|
|
651
666
|
readonly 'approval.publish.targetTeam': "团队源";
|
|
@@ -654,6 +669,15 @@ export declare const zh: {
|
|
|
654
669
|
readonly 'approval.publish.targetGit': "git 仓库";
|
|
655
670
|
readonly 'approval.publish.targetMarket': "模块市场索引";
|
|
656
671
|
readonly 'approval.publish.targetLocal': "留在本机";
|
|
672
|
+
readonly 'approval.market.source': "索引源";
|
|
673
|
+
readonly 'approval.market.url': "地址";
|
|
674
|
+
readonly 'approval.market.action': "这次操作";
|
|
675
|
+
readonly 'approval.market.policy': "当前状态";
|
|
676
|
+
readonly 'approval.market.enable': "设为可发布";
|
|
677
|
+
readonly 'approval.market.disable': "改回只读";
|
|
678
|
+
readonly 'approval.market.writable': "可发布";
|
|
679
|
+
readonly 'approval.market.readOnly': "仅读取模块列表";
|
|
680
|
+
readonly 'approval.market.reading': "正在读取…";
|
|
657
681
|
readonly 'markdown.copy': "复制";
|
|
658
682
|
readonly 'markdown.copied': "已复制";
|
|
659
683
|
readonly 'markdown.footnotes': "脚注";
|
package/dist/i18n/zh.js
CHANGED
|
@@ -289,12 +289,19 @@ export const zh = {
|
|
|
289
289
|
'panels.discover.addingSource': '正在添加…',
|
|
290
290
|
'panels.discover.addFirstSource': '添加索引源',
|
|
291
291
|
'panels.discover.removeSource': '移除',
|
|
292
|
-
'panels.discover.
|
|
293
|
-
'panels.discover.
|
|
294
|
-
'panels.discover.
|
|
295
|
-
'panels.discover.
|
|
292
|
+
'panels.discover.removeTitle': '移除这个索引源?',
|
|
293
|
+
'panels.discover.removeBody': '会把「{label}」从这份列表里移除;仓库本身不会被删,之后还能再添加。',
|
|
294
|
+
'panels.discover.sourceActions': '「{label}」的操作',
|
|
295
|
+
'panels.discover.sourceStateWritable': '可发布',
|
|
296
|
+
'panels.discover.sourceStateReadOnly': '仅读取模块列表',
|
|
297
|
+
'panels.discover.sourceEnable': '设为可发布',
|
|
298
|
+
'panels.discover.sourceDisable': '设为只读',
|
|
299
|
+
'panels.discover.sourceWorking': '设置中…',
|
|
300
|
+
'panels.discover.sourceEnabled': '已把「{label}」设为可发布',
|
|
301
|
+
'panels.discover.sourceReadOnly': '已把「{label}」设为只读',
|
|
296
302
|
'panels.discover.refresh': '刷新',
|
|
297
303
|
'panels.discover.search': '搜索模块',
|
|
304
|
+
'panels.discover.clearSearch': '清除搜索',
|
|
298
305
|
'panels.discover.all': '全部',
|
|
299
306
|
'panels.discover.sourceLabel': '名字',
|
|
300
307
|
'panels.discover.sourceKind': '类型',
|
|
@@ -318,7 +325,7 @@ export const zh = {
|
|
|
318
325
|
'panels.discover.missingIndex': '仓库里还没有索引文件 {path}',
|
|
319
326
|
'panels.discover.initTitle': '把这个仓库初始化成模块市场索引?',
|
|
320
327
|
'panels.discover.initCta': '初始化成市场索引',
|
|
321
|
-
'panels.discover.initWhat': '会在 {branch} 分支写 market.json 与 README.md
|
|
328
|
+
'panels.discover.initWhat': '会在 {branch} 分支写 market.json 与 README.md,提交一次并推上去;之后从这个工作台发布的模块会把打包好的文件提交到按包名分的 packages/<包名>/。仓库的写权限归它的所有者。',
|
|
322
329
|
'panels.discover.initBranch': '分支',
|
|
323
330
|
'panels.discover.initBranchHint': '索引和打包文件都发到这个分支;空仓库会新建它。',
|
|
324
331
|
'panels.discover.initBranchBad': '分支名只能用字母、数字、点、下划线、斜杠或短横线',
|
|
@@ -628,6 +635,13 @@ export const zh = {
|
|
|
628
635
|
'approval.publish.version': '版本',
|
|
629
636
|
'approval.publish.author': '作者',
|
|
630
637
|
'approval.publish.authorMissing': '未署名',
|
|
638
|
+
'approval.publish.owner': '认领人',
|
|
639
|
+
'approval.publish.ownerUnowned': '原来没署名',
|
|
640
|
+
'approval.publish.ownerTaken': '已有主人:{owner}',
|
|
641
|
+
'approval.publish.unit': '模块 id',
|
|
642
|
+
'approval.publish.unitTaken': '已被 {entry} 占用',
|
|
643
|
+
'approval.publish.path': '产物目录',
|
|
644
|
+
'approval.publish.pathTaken': '已被 {entry} 占用',
|
|
631
645
|
'approval.publish.registry': '源',
|
|
632
646
|
'approval.publish.repo': '仓库',
|
|
633
647
|
'approval.publish.branch': '分支',
|
|
@@ -638,6 +652,7 @@ export const zh = {
|
|
|
638
652
|
'approval.publish.twoCommits': '打包文件 + market.json 两次提交,一次推送',
|
|
639
653
|
'approval.publish.reading': '读取中…',
|
|
640
654
|
'approval.publish.changelog': '{version} 的更新日志',
|
|
655
|
+
'approval.publish.changelogPending': '{version} 这一版会写进更新日志,随包发布',
|
|
641
656
|
'approval.publish.changelogMissing': '这一版还没有更新日志({version}),发布会被拦下',
|
|
642
657
|
'approval.publish.targetAuto': '按配置默认',
|
|
643
658
|
'approval.publish.targetTeam': '团队源',
|
|
@@ -646,6 +661,15 @@ export const zh = {
|
|
|
646
661
|
'approval.publish.targetGit': 'git 仓库',
|
|
647
662
|
'approval.publish.targetMarket': '模块市场索引',
|
|
648
663
|
'approval.publish.targetLocal': '留在本机',
|
|
664
|
+
'approval.market.source': '索引源',
|
|
665
|
+
'approval.market.url': '地址',
|
|
666
|
+
'approval.market.action': '这次操作',
|
|
667
|
+
'approval.market.policy': '当前状态',
|
|
668
|
+
'approval.market.enable': '设为可发布',
|
|
669
|
+
'approval.market.disable': '改回只读',
|
|
670
|
+
'approval.market.writable': '可发布',
|
|
671
|
+
'approval.market.readOnly': '仅读取模块列表',
|
|
672
|
+
'approval.market.reading': '正在读取…',
|
|
649
673
|
'markdown.copy': '复制',
|
|
650
674
|
'markdown.copied': '已复制',
|
|
651
675
|
'markdown.footnotes': '脚注',
|
package/dist/testing/needs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unitTitleKey } from '../contract/unit.js';
|
|
1
2
|
export const fakeNpmSource = {
|
|
2
3
|
kind: 'npm',
|
|
3
4
|
registryEnv: 'WB_TEST_REGISTRY',
|
|
@@ -23,9 +24,11 @@ export function envNeedOf(name, extra = {}) {
|
|
|
23
24
|
return { kind: 'env', name, purpose: '测试', ...extra };
|
|
24
25
|
}
|
|
25
26
|
export function manifestOf(options = {}) {
|
|
27
|
+
const id = options.id ?? 'fixture';
|
|
26
28
|
return {
|
|
27
|
-
id
|
|
28
|
-
|
|
29
|
+
id,
|
|
30
|
+
titleKey: unitTitleKey(id),
|
|
31
|
+
card: options.card === undefined ? { order: 10 } : options.card,
|
|
29
32
|
tools: [],
|
|
30
33
|
host: { inject: [] },
|
|
31
34
|
...(options.needs === undefined ? {} : { needs: options.needs }),
|
package/package.json
CHANGED
|
@@ -79,6 +79,7 @@ describe('清单里的名字(v3 起名字在顶层,是字面量)', () => {
|
|
|
79
79
|
|
|
80
80
|
expect(read.problems).toEqual([]);
|
|
81
81
|
expect(read.manifest?.title).toEqual({ zh: '演示', en: 'Demo' });
|
|
82
|
-
expect(read.manifest?.
|
|
82
|
+
expect(read.manifest?.titleKey).toBe(unitTitleKey('demo'));
|
|
83
|
+
expect(read.manifest?.card).toEqual({ order: 7 });
|
|
83
84
|
});
|
|
84
85
|
});
|
|
@@ -5,7 +5,7 @@ import { needsOf, provisionableNeedsOf, type UnitManifest } from '../unit.js';
|
|
|
5
5
|
function manifest(needs?: readonly Need[]): UnitManifest {
|
|
6
6
|
return {
|
|
7
7
|
id: 'issues',
|
|
8
|
-
|
|
8
|
+
titleKey: 'issues.mine', card: { order: 10 },
|
|
9
9
|
tools: [],
|
|
10
10
|
host: { inject: [] },
|
|
11
11
|
...(needs === undefined ? {} : { needs }),
|
|
@@ -56,6 +56,8 @@ export interface MarketIndexVersion {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export interface MarketIndexEntryMetadata {
|
|
59
|
+
readonly workbench?: boolean;
|
|
60
|
+
readonly unit?: string;
|
|
59
61
|
readonly category?: string;
|
|
60
62
|
readonly labels?: readonly string[];
|
|
61
63
|
readonly keywords?: readonly string[];
|
|
@@ -67,12 +69,22 @@ export interface MarketIndexEntry {
|
|
|
67
69
|
readonly title?: MarketLocalizedText;
|
|
68
70
|
readonly description?: MarketLocalizedText;
|
|
69
71
|
readonly author?: string;
|
|
72
|
+
readonly owner?: string;
|
|
70
73
|
readonly homepage?: string;
|
|
71
74
|
readonly icon?: string;
|
|
72
75
|
readonly metadata?: MarketIndexEntryMetadata;
|
|
73
76
|
readonly versions: readonly MarketIndexVersion[];
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
export type MarketClaim =
|
|
80
|
+
| { readonly state: 'new' }
|
|
81
|
+
| { readonly state: 'owned'; readonly owner: string }
|
|
82
|
+
| { readonly state: 'unowned' }
|
|
83
|
+
| { readonly state: 'conflict'; readonly owner: string }
|
|
84
|
+
| { readonly state: 'unit-taken'; readonly entry: string }
|
|
85
|
+
| { readonly state: 'path-taken'; readonly entry: string }
|
|
86
|
+
| { readonly state: 'unknown' };
|
|
87
|
+
|
|
76
88
|
export interface MarketIndexCategory {
|
|
77
89
|
readonly title?: MarketLocalizedText;
|
|
78
90
|
}
|
|
@@ -114,7 +126,9 @@ export type MarketIndexProblemKind =
|
|
|
114
126
|
| 'source'
|
|
115
127
|
| 'git-commit'
|
|
116
128
|
| 'timestamp'
|
|
117
|
-
| 'icon'
|
|
129
|
+
| 'icon'
|
|
130
|
+
| 'owner'
|
|
131
|
+
| 'unit';
|
|
118
132
|
|
|
119
133
|
export interface MarketIndexProblem {
|
|
120
134
|
readonly kind: MarketIndexProblemKind;
|
|
@@ -213,6 +227,7 @@ export interface PublishChangelog {
|
|
|
213
227
|
readonly version: string;
|
|
214
228
|
readonly text?: string;
|
|
215
229
|
readonly missing?: boolean;
|
|
230
|
+
readonly pending?: boolean;
|
|
216
231
|
}
|
|
217
232
|
|
|
218
233
|
export interface PublishPreview {
|
|
@@ -221,6 +236,7 @@ export interface PublishPreview {
|
|
|
221
236
|
readonly author?: string;
|
|
222
237
|
readonly registry?: string;
|
|
223
238
|
readonly market?: MarketPublishDestination;
|
|
239
|
+
readonly claim?: MarketClaim;
|
|
224
240
|
readonly changelog?: PublishChangelog;
|
|
225
241
|
}
|
|
226
242
|
|
|
@@ -245,4 +261,5 @@ export type MarketSourcesAction =
|
|
|
245
261
|
| { readonly action: 'save'; readonly source: MarketSourceConfig; readonly select?: boolean }
|
|
246
262
|
| { readonly action: 'remove'; readonly id: string }
|
|
247
263
|
| { readonly action: 'select'; readonly id: string | null }
|
|
248
|
-
| { readonly action: 'init'; readonly id: string; readonly ref?: string }
|
|
264
|
+
| { readonly action: 'init'; readonly id: string; readonly ref?: string; readonly stampOnly?: boolean }
|
|
265
|
+
| { readonly action: 'read-only'; readonly id: string };
|
|
@@ -298,13 +298,8 @@ export function readDeclaration(subject: string, raw: unknown): DeclarationRead
|
|
|
298
298
|
manifest: {
|
|
299
299
|
id,
|
|
300
300
|
title: { zh: text(declaration.title.zh), en: text(declaration.title.en) },
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
? null
|
|
304
|
-
: {
|
|
305
|
-
titleKey: unitTitleKey(id),
|
|
306
|
-
order: card.order,
|
|
307
|
-
},
|
|
301
|
+
titleKey: unitTitleKey(id),
|
|
302
|
+
card: card === null ? null : { order: card.order },
|
|
308
303
|
tools: declaration.tools.map((tool) => ({
|
|
309
304
|
name: text(tool.name),
|
|
310
305
|
writes: tool.writes,
|
package/src/contract/unit.ts
CHANGED
|
@@ -18,10 +18,9 @@ export function unitTitleKey(unit: string): string {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* 一张卡片在清单里的静态声明:只剩排序位;显不显示由运行期面板配置定。
|
|
22
22
|
*/
|
|
23
23
|
export interface UnitCard {
|
|
24
|
-
readonly titleKey: string;
|
|
25
24
|
readonly order: number;
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -50,11 +49,13 @@ export interface PluginRelease {
|
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
/**
|
|
53
|
-
* 装配期用的模块清单:单元 id
|
|
52
|
+
* 装配期用的模块清单:单元 id、名字、名字键、卡片、工具、宿主服务与环境需要,比声明少一层协议包装。
|
|
53
|
+
* titleKey 恒等于 unitTitleKey(id):有没有卡片都指向同一个词表键,展示方照它取名字。
|
|
54
54
|
*/
|
|
55
55
|
export interface UnitManifest {
|
|
56
56
|
readonly id: string;
|
|
57
57
|
readonly title?: UnitTitle;
|
|
58
|
+
readonly titleKey: string;
|
|
58
59
|
readonly card: UnitCard | null;
|
|
59
60
|
readonly tools: readonly UnitTool[];
|
|
60
61
|
readonly host: UnitHostNeeds;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
changelogSectionOf,
|
|
7
7
|
changelogSkeleton,
|
|
8
8
|
parseChangelog,
|
|
9
|
+
planChangelog,
|
|
9
10
|
} from '../changelog.js';
|
|
10
11
|
|
|
11
12
|
const SAMPLE = [
|
|
@@ -100,3 +101,35 @@ describe('changelogSkeleton', () => {
|
|
|
100
101
|
expect(changelogSkeleton()).toBe('# 更新日志\n');
|
|
101
102
|
});
|
|
102
103
|
});
|
|
104
|
+
|
|
105
|
+
describe('planChangelog', () => {
|
|
106
|
+
it('这一版已有内容就 kept,不追加也不改原文', () => {
|
|
107
|
+
const plan = planChangelog(SAMPLE, { version: '1.2.0', kind: 'fixed', note: '又修了一条' });
|
|
108
|
+
expect(plan?.action).toBe('kept');
|
|
109
|
+
expect(plan?.text).toBe(SAMPLE);
|
|
110
|
+
expect(plan?.section).toContain('模块详情能看说明与更新日志');
|
|
111
|
+
expect(plan?.section).not.toContain('又修了一条');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('缺这一版且有 note 就 written:空文件从骨架起,段里就是这条', () => {
|
|
115
|
+
const plan = planChangelog('', { version: '0.1.0', kind: 'added', note: '第一次发布', date: '2026-10-27' });
|
|
116
|
+
expect(plan?.action).toBe('written');
|
|
117
|
+
expect(plan?.version).toBe('0.1.0');
|
|
118
|
+
expect(plan?.text.startsWith('# 更新日志')).toBe(true);
|
|
119
|
+
expect(plan?.section).toContain('- 第一次发布');
|
|
120
|
+
expect(changelogHasEntry(plan?.text ?? '', '0.1.0')).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('缺这一版又没有 note 就 undefined(发布前先要一句话)', () => {
|
|
124
|
+
expect(planChangelog(SAMPLE, { version: '9.9.9', note: ' ' })).toBeUndefined();
|
|
125
|
+
expect(planChangelog('', { version: '9.9.9', note: '' })).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('只有版本标题没有条目时算缺,补进去而不是再开一段', () => {
|
|
129
|
+
const empty = '# 更新日志\n\n## v0.2.0\n\n`2026-10-26`\n';
|
|
130
|
+
const plan = planChangelog(empty, { version: '0.2.0', kind: 'fixed', note: '修一条' });
|
|
131
|
+
expect(plan?.action).toBe('written');
|
|
132
|
+
expect(parseChangelog(plan?.text ?? '').filter((one) => one.version === '0.2.0')).toHaveLength(1);
|
|
133
|
+
expect(plan?.section).toContain('- 修一条');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { MarketIndex, MarketIndexEntry } from '../../contract/protocol/market-index.js';
|
|
3
|
+
import { claimOf, claimOfEntry, entryOwnerOf, entryUnitOf, marketArtifactPath, samePublisher } from '../market-index-claim.js';
|
|
3
4
|
import {
|
|
4
5
|
filterMarketIndex,
|
|
5
6
|
marketInstallTarget,
|
|
@@ -39,6 +40,13 @@ describe('parseMarketIndex', () => {
|
|
|
39
40
|
expect(parsed.index.title).toEqual({ zh: '工作台模块' });
|
|
40
41
|
});
|
|
41
42
|
|
|
43
|
+
it('★ metadata.workbench 是身份标记,labels 可以为空(不再靠 workbench 标签)', () => {
|
|
44
|
+
const parsed = parseMarketIndex(indexOf([entry({ metadata: { workbench: true } })]));
|
|
45
|
+
expect(parsed.ok).toBe(true);
|
|
46
|
+
if (!parsed.ok) return;
|
|
47
|
+
expect(parsed.index.plugins[0]?.metadata).toEqual({ workbench: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
42
50
|
it('format 不是 1 就整份拒', () => {
|
|
43
51
|
const parsed = parseMarketIndex({ format: 2, plugins: [] });
|
|
44
52
|
expect(parsed.ok).toBe(false);
|
|
@@ -78,6 +86,31 @@ describe('parseMarketIndex', () => {
|
|
|
78
86
|
if (parsed.ok) return;
|
|
79
87
|
expect(parsed.problems[0]?.kind).toBe('not-json');
|
|
80
88
|
});
|
|
89
|
+
|
|
90
|
+
it('收下 owner,原样写回', () => {
|
|
91
|
+
const parsed = parseMarketIndex(indexOf([entry({ owner: '陈康 <kang@example.com>' })]));
|
|
92
|
+
expect(parsed.ok).toBe(true);
|
|
93
|
+
if (!parsed.ok) return;
|
|
94
|
+
expect(parsed.index.plugins[0]?.owner).toBe('陈康 <kang@example.com>');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('owner 只收一行:带换行整份拒', () => {
|
|
98
|
+
const parsed = parseMarketIndex(indexOf([entry({ owner: '陈康\n<b>' })]));
|
|
99
|
+
expect(parsed.ok).toBe(false);
|
|
100
|
+
if (parsed.ok) return;
|
|
101
|
+
expect(parsed.problems[0]?.kind).toBe('owner');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('收下 metadata.unit;不是合法模块 id 整份拒', () => {
|
|
105
|
+
const parsed = parseMarketIndex(indexOf([entry({ metadata: { unit: 'inventory' } })]));
|
|
106
|
+
expect(parsed.ok).toBe(true);
|
|
107
|
+
if (!parsed.ok) return;
|
|
108
|
+
expect(parsed.index.plugins[0]?.metadata?.unit).toBe('inventory');
|
|
109
|
+
const bad = parseMarketIndex(indexOf([entry({ metadata: { unit: 'Not A Unit' } })]));
|
|
110
|
+
expect(bad.ok).toBe(false);
|
|
111
|
+
if (bad.ok) return;
|
|
112
|
+
expect(bad.problems[0]?.kind).toBe('unit');
|
|
113
|
+
});
|
|
81
114
|
});
|
|
82
115
|
|
|
83
116
|
describe('upsertMarketEntry', () => {
|
|
@@ -101,6 +134,139 @@ describe('upsertMarketEntry', () => {
|
|
|
101
134
|
expect(next.plugins[0]?.versions).toHaveLength(1);
|
|
102
135
|
expect(next.plugins[0]?.versions[0]?.source.type).toBe('git');
|
|
103
136
|
});
|
|
137
|
+
|
|
138
|
+
it('已有 owner 就翻不过来:后来的条目只加版本', () => {
|
|
139
|
+
const owned = upsertMarketEntry(
|
|
140
|
+
{ format: 1, plugins: [entry({ owner: '甲 <a@example.com>' })] },
|
|
141
|
+
entry({ owner: '乙 <b@example.com>', versions: [{ version: '0.2.0', source: { type: 'npm' } }] }),
|
|
142
|
+
);
|
|
143
|
+
expect(owned.plugins[0]?.owner).toBe('甲 <a@example.com>');
|
|
144
|
+
expect(owned.plugins[0]?.versions[0]?.version).toBe('0.2.0');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('认领', () => {
|
|
149
|
+
const owned = entry({ owner: '陈康 <kang@example.com>' });
|
|
150
|
+
|
|
151
|
+
it('没有条目是 new,条目没 owner 也没 author 是 unowned', () => {
|
|
152
|
+
expect(claimOfEntry(undefined, '陈康')).toEqual({ state: 'new' });
|
|
153
|
+
expect(claimOfEntry(entry(), '陈康')).toEqual({ state: 'unowned' });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('邮箱相同就算同一人,大小写不影响', () => {
|
|
157
|
+
expect(samePublisher('陈康 <KANG@example.com>', '陈康 <kang@example.com>')).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('两边都有邮箱时邮箱说了算:姓名不同也算同一人', () => {
|
|
161
|
+
expect(samePublisher('陈康 <kang@example.com>', 'Kang Chen <kang@example.com>')).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('只有一边有邮箱就比姓名:格式漂了也认', () => {
|
|
165
|
+
expect(samePublisher('陈康', '陈康 <kang@example.com>')).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('姓名和邮箱都不同就是冲突', () => {
|
|
169
|
+
expect(claimOfEntry(owned, '乙 <yi@example.com>')).toEqual({
|
|
170
|
+
state: 'conflict',
|
|
171
|
+
owner: '陈康 <kang@example.com>',
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('原主自己发是 owned;没给身份时是 unknown', () => {
|
|
176
|
+
expect(claimOfEntry(owned, '陈康 <kang@example.com>')).toEqual({
|
|
177
|
+
state: 'owned',
|
|
178
|
+
owner: '陈康 <kang@example.com>',
|
|
179
|
+
});
|
|
180
|
+
expect(claimOfEntry(owned, '')).toEqual({ state: 'unknown' });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('老条目只有 author:把它当 owner 认', () => {
|
|
184
|
+
const legacy = entry({ author: '陈康 <kang@example.com>' });
|
|
185
|
+
expect(entryOwnerOf(legacy)).toBe('陈康 <kang@example.com>');
|
|
186
|
+
expect(claimOf({ format: 1, plugins: [legacy] }, { name: '@scope/module-x', publisher: '陈康' })).toEqual({
|
|
187
|
+
state: 'owned',
|
|
188
|
+
owner: '陈康 <kang@example.com>',
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('别的条目用了同一个 unit:unit-taken;老条目从产物目录认出 unit', () => {
|
|
193
|
+
const other = entry({ name: '@scope/other', owner: '乙', metadata: { unit: 'inventory' } });
|
|
194
|
+
expect(claimOf({ format: 1, plugins: [other] }, { name: '@scope/module-x', publisher: '甲', unit: 'inventory' })).toEqual({
|
|
195
|
+
state: 'unit-taken',
|
|
196
|
+
entry: '@scope/other',
|
|
197
|
+
});
|
|
198
|
+
const legacy = entry({
|
|
199
|
+
name: '@scope/legacy',
|
|
200
|
+
versions: [
|
|
201
|
+
{
|
|
202
|
+
version: '0.1.0',
|
|
203
|
+
source: {
|
|
204
|
+
type: 'git',
|
|
205
|
+
url: 'https://git.company.com/wb/market.git',
|
|
206
|
+
commit: 'a'.repeat(40),
|
|
207
|
+
path: 'packages/inventory',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
expect(entryUnitOf(legacy)).toBe('inventory');
|
|
213
|
+
expect(claimOf({ format: 1, plugins: [legacy] }, { name: '@scope/module-x', publisher: '甲', unit: 'inventory' }).state).toBe(
|
|
214
|
+
'unit-taken',
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('同一个条目自己再用同 unit / 同产物目录,不算撞', () => {
|
|
219
|
+
const mine = entry({
|
|
220
|
+
owner: '甲',
|
|
221
|
+
metadata: { unit: 'inventory' },
|
|
222
|
+
versions: [
|
|
223
|
+
{
|
|
224
|
+
version: '0.1.0',
|
|
225
|
+
source: {
|
|
226
|
+
type: 'git',
|
|
227
|
+
url: 'https://git.company.com/wb/market.git',
|
|
228
|
+
commit: 'a'.repeat(40),
|
|
229
|
+
path: marketArtifactPath('@scope/module-x'),
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
expect(
|
|
235
|
+
claimOf(
|
|
236
|
+
{ format: 1, plugins: [mine] },
|
|
237
|
+
{
|
|
238
|
+
name: '@scope/module-x',
|
|
239
|
+
publisher: '甲',
|
|
240
|
+
unit: 'inventory',
|
|
241
|
+
artifactPath: marketArtifactPath('@scope/module-x'),
|
|
242
|
+
},
|
|
243
|
+
).state,
|
|
244
|
+
).toBe('owned');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('别的条目占了同一个产物目录:path-taken', () => {
|
|
248
|
+
const other = entry({
|
|
249
|
+
name: '@scope/other',
|
|
250
|
+
owner: '乙',
|
|
251
|
+
versions: [
|
|
252
|
+
{
|
|
253
|
+
version: '0.1.0',
|
|
254
|
+
source: {
|
|
255
|
+
type: 'git',
|
|
256
|
+
url: 'https://git.company.com/wb/market.git',
|
|
257
|
+
commit: 'a'.repeat(40),
|
|
258
|
+
path: marketArtifactPath('@scope/module-x'),
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
expect(
|
|
264
|
+
claimOf(
|
|
265
|
+
{ format: 1, plugins: [other] },
|
|
266
|
+
{ name: '@scope/module-x', publisher: '甲', unit: 'x', artifactPath: marketArtifactPath('@scope/module-x') },
|
|
267
|
+
),
|
|
268
|
+
).toEqual({ state: 'path-taken', entry: '@scope/other' });
|
|
269
|
+
});
|
|
104
270
|
});
|
|
105
271
|
|
|
106
272
|
describe('filterMarketIndex', () => {
|
package/src/host/changelog.ts
CHANGED
|
@@ -202,6 +202,25 @@ export function appendChangelogEntry(text: string, options: ChangelogEntryOption
|
|
|
202
202
|
return `${normalizeWhitespace(out)}\n`;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
export interface ChangelogPlan {
|
|
206
|
+
readonly action: 'kept' | 'written';
|
|
207
|
+
readonly version: string;
|
|
208
|
+
readonly text: string;
|
|
209
|
+
readonly section: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function planChangelog(text: string, options: ChangelogEntryOptions): ChangelogPlan | undefined {
|
|
213
|
+
const source = normalizeChangelogText(text);
|
|
214
|
+
const version = changelogVersion(options.version);
|
|
215
|
+
const section = changelogSectionOf(source, version);
|
|
216
|
+
if (section !== undefined && changelogHasEntry(source, version)) {
|
|
217
|
+
return { action: 'kept', version, text: source, section };
|
|
218
|
+
}
|
|
219
|
+
if (bulletsOf(options.note).length === 0) return undefined;
|
|
220
|
+
const next = appendChangelogEntry(source, options);
|
|
221
|
+
return { action: 'written', version, text: next, section: changelogSectionOf(next, version) ?? '' };
|
|
222
|
+
}
|
|
223
|
+
|
|
205
224
|
function normalizeWhitespace(rows: readonly string[]): string {
|
|
206
225
|
const out: string[] = [];
|
|
207
226
|
for (const row of rows) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { MarketClaim, MarketIndex, MarketIndexEntry } from '../contract/protocol/market-index.js';
|
|
2
|
+
|
|
3
|
+
export interface PublisherIdentity {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly email?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function foldedName(name: string): string {
|
|
9
|
+
return name.replace(/\s+/gu, ' ').trim().toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function publisherIdentityOf(text: string | undefined): PublisherIdentity | undefined {
|
|
13
|
+
const raw = (text ?? '').trim();
|
|
14
|
+
if (raw === '') return undefined;
|
|
15
|
+
const match = /^(.*?)\s*<([^<>]*)>\s*$/u.exec(raw);
|
|
16
|
+
const name = (match === null ? raw : match[1]).trim();
|
|
17
|
+
const email = match === null ? '' : match[2].trim().toLowerCase();
|
|
18
|
+
if (name === '') return undefined;
|
|
19
|
+
return email === '' ? { name } : { name, email };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function samePublisher(left: string, right: string): boolean {
|
|
23
|
+
const a = publisherIdentityOf(left);
|
|
24
|
+
const b = publisherIdentityOf(right);
|
|
25
|
+
if (a === undefined || b === undefined) return false;
|
|
26
|
+
if (a.email !== undefined && b.email !== undefined) return a.email === b.email;
|
|
27
|
+
return foldedName(a.name) === foldedName(b.name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function entryOwnerOf(entry: MarketIndexEntry): string | undefined {
|
|
31
|
+
const owner = (entry.owner ?? '').trim();
|
|
32
|
+
if (owner !== '') return owner;
|
|
33
|
+
const author = (entry.author ?? '').trim();
|
|
34
|
+
return author === '' ? undefined : author;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function claimOfEntry(entry: MarketIndexEntry | undefined, publisher: string | undefined): MarketClaim {
|
|
38
|
+
if (entry === undefined) return { state: 'new' };
|
|
39
|
+
const owner = entryOwnerOf(entry);
|
|
40
|
+
if (owner === undefined) return { state: 'unowned' };
|
|
41
|
+
if (publisher === undefined || publisher.trim() === '') return { state: 'unknown' };
|
|
42
|
+
return samePublisher(owner, publisher) ? { state: 'owned', owner } : { state: 'conflict', owner };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MarketClaimInput {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly publisher: string | undefined;
|
|
48
|
+
readonly unit?: string | undefined;
|
|
49
|
+
readonly artifactPath?: string | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function marketArtifactPath(name: string): string {
|
|
53
|
+
return `packages/${name}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function entryUnitOf(entry: MarketIndexEntry): string | undefined {
|
|
57
|
+
const unit = entry.metadata?.unit?.trim();
|
|
58
|
+
if (unit !== undefined && unit !== '') return unit;
|
|
59
|
+
for (const version of entry.versions) {
|
|
60
|
+
if (version.source.type !== 'git') continue;
|
|
61
|
+
const match = /^packages\/([^/]+)$/u.exec(version.source.path ?? '');
|
|
62
|
+
const legacy = match?.[1];
|
|
63
|
+
if (legacy !== undefined) return legacy;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function claimOf(index: MarketIndex, input: MarketClaimInput): MarketClaim {
|
|
69
|
+
const base = claimOfEntry(
|
|
70
|
+
index.plugins.find((one) => one.name === input.name),
|
|
71
|
+
input.publisher,
|
|
72
|
+
);
|
|
73
|
+
if (base.state === 'conflict' || base.state === 'unknown') return base;
|
|
74
|
+
const unit = (input.unit ?? '').trim();
|
|
75
|
+
if (unit !== '') {
|
|
76
|
+
for (const other of index.plugins) {
|
|
77
|
+
if (other.name === input.name) continue;
|
|
78
|
+
if (entryUnitOf(other) !== unit) continue;
|
|
79
|
+
return { state: 'unit-taken', entry: other.name };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const path = (input.artifactPath ?? '').trim();
|
|
83
|
+
if (path !== '') {
|
|
84
|
+
for (const other of index.plugins) {
|
|
85
|
+
if (other.name === input.name) continue;
|
|
86
|
+
if (!other.versions.some((one) => one.source.type === 'git' && (one.source.path ?? '') === path)) continue;
|
|
87
|
+
return { state: 'path-taken', entry: other.name };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return base;
|
|
91
|
+
}
|