@dsh-workbench/shared 1.1.5 → 1.1.6

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.
@@ -1,4 +1,6 @@
1
1
  export declare const MARKET_INDEX_FORMAT = 1;
2
+ export declare const MARKET_INDEX_DIR = "market";
3
+ export declare const MARKET_ARTIFACTS_DIR = "artifacts";
2
4
  export declare const MARKET_INDEX_PATH = "market.json";
3
5
  export declare const MARKET_DISCOVER_ROUTE = "panels/market-discover";
4
6
  export declare const MARKET_DOC_ROUTE = "panels/market-doc";
@@ -111,7 +113,7 @@ export interface MarketIndex {
111
113
  readonly metadata?: MarketIndexMetadata;
112
114
  readonly plugins: readonly MarketIndexEntry[];
113
115
  }
114
- export type MarketIndexProblemKind = 'not-json' | 'not-object' | 'format' | 'plugins' | 'plugin' | 'name' | 'duplicate-name' | 'versions' | 'version' | 'version-order' | 'source' | 'git-commit' | 'timestamp' | 'icon' | 'owner' | 'unit';
116
+ export type MarketIndexProblemKind = 'not-json' | 'not-object' | 'format' | 'plugins' | 'plugin' | 'entry' | 'name' | 'duplicate-name' | 'versions' | 'version' | 'version-order' | 'duplicate-version' | 'source' | 'git-commit' | 'timestamp' | 'icon' | 'owner' | 'unit';
115
117
  export interface MarketIndexProblem {
116
118
  readonly kind: MarketIndexProblemKind;
117
119
  readonly detail: string;
@@ -1,4 +1,6 @@
1
1
  export const MARKET_INDEX_FORMAT = 1;
2
+ export const MARKET_INDEX_DIR = 'market';
3
+ export const MARKET_ARTIFACTS_DIR = 'artifacts';
2
4
  export const MARKET_INDEX_PATH = 'market.json';
3
5
  export const MARKET_DISCOVER_ROUTE = 'panels/market-discover';
4
6
  export const MARKET_DOC_ROUTE = 'panels/market-doc';
@@ -14,5 +14,7 @@ export interface MarketClaimInput {
14
14
  readonly artifactPath?: string | undefined;
15
15
  }
16
16
  export declare function marketArtifactPath(name: string): string;
17
+ export declare function marketArtifactRoot(name: string): string;
18
+ export declare function marketArtifactPathFor(name: string, version: string): string;
17
19
  export declare function entryUnitOf(entry: MarketIndexEntry): string | undefined;
18
20
  export declare function claimOf(index: MarketIndex, input: MarketClaimInput): MarketClaim;
@@ -1,3 +1,4 @@
1
+ import { MARKET_ARTIFACTS_DIR } from '../contract/protocol/market-index.js';
1
2
  function foldedName(name) {
2
3
  return name.replace(/\s+/gu, ' ').trim().toLowerCase();
3
4
  }
@@ -41,6 +42,12 @@ export function claimOfEntry(entry, publisher) {
41
42
  export function marketArtifactPath(name) {
42
43
  return `packages/${name}`;
43
44
  }
45
+ export function marketArtifactRoot(name) {
46
+ return `${MARKET_ARTIFACTS_DIR}/${name}`;
47
+ }
48
+ export function marketArtifactPathFor(name, version) {
49
+ return `${MARKET_ARTIFACTS_DIR}/${name}/${version}`;
50
+ }
44
51
  export function entryUnitOf(entry) {
45
52
  const unit = entry.metadata?.unit?.trim();
46
53
  if (unit !== undefined && unit !== '')
@@ -0,0 +1,56 @@
1
+ import type { MarketIndex, MarketIndexEngines, MarketIndexEntryMetadata, MarketIndexProblem, MarketIndexSource, MarketLocalizedText } from '../contract/protocol/market-index.js';
2
+ export interface MarketVersionDoc {
3
+ readonly name: string;
4
+ readonly version: string;
5
+ readonly source: MarketIndexSource;
6
+ readonly publishTimestamp?: number;
7
+ readonly engines?: MarketIndexEngines;
8
+ readonly title?: MarketLocalizedText;
9
+ readonly description?: MarketLocalizedText;
10
+ readonly author?: string;
11
+ readonly owner?: string;
12
+ readonly homepage?: string;
13
+ readonly icon?: string;
14
+ readonly metadata?: MarketIndexEntryMetadata;
15
+ }
16
+ export interface MarketMetaDoc {
17
+ readonly name: string;
18
+ readonly epoch?: number;
19
+ readonly publishTimestamp: number;
20
+ readonly title?: MarketLocalizedText;
21
+ readonly description?: MarketLocalizedText;
22
+ readonly author?: string;
23
+ readonly owner?: string;
24
+ readonly homepage?: string;
25
+ readonly icon?: string;
26
+ readonly metadata?: MarketIndexEntryMetadata;
27
+ }
28
+ export type MarketEntryDoc = MarketVersionDoc | MarketMetaDoc;
29
+ export type MarketEntryDocParse = {
30
+ readonly ok: true;
31
+ readonly doc: MarketEntryDoc;
32
+ } | {
33
+ readonly ok: false;
34
+ readonly problems: readonly MarketIndexProblem[];
35
+ };
36
+ export interface MarketEntryFile {
37
+ readonly path: string;
38
+ readonly text: string;
39
+ }
40
+ export type MarketIndexAssemble = {
41
+ readonly ok: true;
42
+ readonly index: MarketIndex;
43
+ } | {
44
+ readonly ok: false;
45
+ readonly problems: readonly MarketIndexProblem[];
46
+ };
47
+ export interface MarketDocFields {
48
+ readonly name: string;
49
+ readonly title?: MarketLocalizedText;
50
+ readonly description?: MarketLocalizedText;
51
+ readonly author?: string;
52
+ readonly owner?: string;
53
+ readonly homepage?: string;
54
+ readonly icon?: string;
55
+ readonly metadata?: MarketIndexEntryMetadata;
56
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import type { DiscoverFilter, IndexSourceSpec, MarketIndex, MarketIndexEntry, MarketIndexParse, MarketIndexProblem, MarketIndexVersion, MarketSourceConfig } from '../contract/protocol/market-index.js';
2
+ import type { MarketEntryDocParse } from './market-index-doc.js';
3
+ export declare function compareSemver(left: string, right: string): number | undefined;
4
+ export declare function httpUrlProblem(url: string): string | undefined;
5
+ export declare function gitUrlOk(url: string): boolean;
6
+ export declare function parseMarketIndex(raw: unknown): MarketIndexParse;
7
+ export declare function parseMarketIndexJson(text: string): MarketIndexParse;
8
+ export declare function sortMarketVersions(versions: readonly MarketIndexVersion[]): MarketIndexVersion[];
9
+ export declare function upsertMarketEntry(index: MarketIndex, entry: MarketIndexEntry): MarketIndex;
10
+ export declare function marketEntryLabels(entry: MarketIndexEntry): readonly string[];
11
+ export declare function filterMarketIndex(index: MarketIndex, filter: DiscoverFilter): readonly MarketIndexEntry[];
12
+ export declare function marketSourceKey(source: IndexSourceSpec): string;
13
+ export type SourceConfigParse = {
14
+ readonly ok: true;
15
+ readonly source: MarketSourceConfig;
16
+ } | {
17
+ readonly ok: false;
18
+ readonly problem: MarketIndexProblem;
19
+ };
20
+ export declare function parseSourceConfig(raw: unknown): SourceConfigParse;
21
+ export declare function parseMarketEntryDoc(raw: unknown): MarketEntryDocParse;
22
+ export declare function marketEntryPath(dir: string, name: string, version: string): string;
23
+ export declare function marketMetaPath(dir: string, name: string, epoch: number, suffix: string): string;
@@ -0,0 +1,485 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { MARKET_INDEX_DIR, MARKET_INDEX_FORMAT } from '../contract/protocol/market-index.js';
3
+ import { marketText } from '../lib/market-index.js';
4
+ import { compareVersions, isValidVersion } from './version.js';
5
+ const NPM_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
6
+ const GIT_COMMIT = /^[0-9a-f]{40}$/;
7
+ const MAX_ICON_CHARS = 32 * 1024;
8
+ export function compareSemver(left, right) {
9
+ return compareVersions(left, right);
10
+ }
11
+ export function httpUrlProblem(url) {
12
+ let parsed;
13
+ try {
14
+ parsed = new URL(url);
15
+ }
16
+ catch {
17
+ return '不是合法 URL';
18
+ }
19
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
20
+ return '只收 http / https';
21
+ if (parsed.username !== '' || parsed.password !== '')
22
+ return 'URL 不能带凭据';
23
+ if (parsed.search !== '' || parsed.hash !== '')
24
+ return 'URL 不能带 query / fragment';
25
+ return undefined;
26
+ }
27
+ export function gitUrlOk(url) {
28
+ if (url === '')
29
+ return false;
30
+ if (/^[^@\s]+@[^@\s:]+:[^@\s]+$/.test(url))
31
+ return true;
32
+ let parsed;
33
+ try {
34
+ parsed = new URL(url);
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'ssh:' && parsed.protocol !== 'http:')
40
+ return false;
41
+ if (parsed.password !== '')
42
+ return false;
43
+ if (parsed.username !== '' && parsed.protocol !== 'ssh:')
44
+ return false;
45
+ if (parsed.search !== '' || parsed.hash !== '')
46
+ return false;
47
+ return true;
48
+ }
49
+ function asRecord(value) {
50
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
51
+ ? value
52
+ : undefined;
53
+ }
54
+ function asText(value) {
55
+ return typeof value === 'string' ? value.trim() : '';
56
+ }
57
+ function asTextArray(value) {
58
+ return Array.isArray(value)
59
+ ? value.flatMap((item) => (typeof item === 'string' && item.trim() !== '' ? [item.trim()] : []))
60
+ : [];
61
+ }
62
+ function asLocalized(value) {
63
+ const record = asRecord(value);
64
+ if (record === undefined)
65
+ return undefined;
66
+ const zh = asText(record['zh']);
67
+ const en = asText(record['en']);
68
+ return zh === '' && en === '' ? undefined : { ...(zh === '' ? {} : { zh }), ...(en === '' ? {} : { en }) };
69
+ }
70
+ function problem(kind, detail, at) {
71
+ return { kind, detail, ...(at === undefined ? {} : { at }) };
72
+ }
73
+ function sourceOf(raw, at) {
74
+ const record = asRecord(raw);
75
+ if (record === undefined)
76
+ return problem('source', 'source 必须是一个对象', at);
77
+ if (record['type'] === 'npm') {
78
+ const registry = record['registry'];
79
+ if (registry !== undefined && asText(registry) === '') {
80
+ return problem('source', 'npm source 的 registry 必须是非空字符串', at);
81
+ }
82
+ return { type: 'npm', ...(registry === undefined ? {} : { registry: asText(registry) }) };
83
+ }
84
+ if (record['type'] === 'git') {
85
+ const url = asText(record['url']);
86
+ const commit = asText(record['commit']);
87
+ if (url === '')
88
+ return problem('source', 'git source 缺 url', at);
89
+ if (!GIT_COMMIT.test(commit)) {
90
+ return problem('git-commit', `git source 的 commit 必须是 40 位小写 SHA(现在是 ${JSON.stringify(commit)})`, at);
91
+ }
92
+ const path = record['path'];
93
+ return {
94
+ type: 'git',
95
+ url,
96
+ commit,
97
+ ...(path === undefined || asText(path) === '' ? {} : { path: asText(path) }),
98
+ };
99
+ }
100
+ return problem('source', 'source.type 只能是 npm 或 git', at);
101
+ }
102
+ function versionOf(raw, at) {
103
+ const record = asRecord(raw);
104
+ if (record === undefined)
105
+ return problem('version', '版本必须是一个对象', at);
106
+ const version = asText(record['version']);
107
+ if (!isValidVersion(version))
108
+ return problem('version', `版本号不是合法 SemVer:${JSON.stringify(version)}`, at);
109
+ const source = sourceOf(record['source'], `${at}.source`);
110
+ if ('kind' in source)
111
+ return source;
112
+ const timestamp = record['publishTimestamp'];
113
+ if (timestamp !== undefined && (!Number.isInteger(timestamp) || timestamp < 0)) {
114
+ return problem('timestamp', 'publishTimestamp 必须是 Unix 秒整数', at);
115
+ }
116
+ const engines = asRecord(record['engines']);
117
+ const dsh = engines === undefined ? '' : asText(engines['dsh']);
118
+ return {
119
+ version,
120
+ source,
121
+ ...(timestamp === undefined ? {} : { publishTimestamp: timestamp }),
122
+ ...(dsh === '' ? {} : { engines: { dsh } }),
123
+ };
124
+ }
125
+ function versionsOf(raw, at) {
126
+ if (!Array.isArray(raw) || raw.length === 0)
127
+ return problem('versions', 'versions 必须是非空数组', at);
128
+ const versions = [];
129
+ const seen = new Set();
130
+ for (const [index, item] of raw.entries()) {
131
+ const parsed = versionOf(item, `${at}[${String(index)}]`);
132
+ if ('kind' in parsed)
133
+ return parsed;
134
+ if (seen.has(parsed.version))
135
+ return problem('versions', `版本重复:${parsed.version}`, at);
136
+ seen.add(parsed.version);
137
+ versions.push(parsed);
138
+ }
139
+ for (let index = 1; index < versions.length; index += 1) {
140
+ const order = compareSemver(versions[index - 1]?.version ?? '', versions[index]?.version ?? '');
141
+ if (order === undefined || order < 0) {
142
+ return problem('version-order', 'versions 必须按 SemVer 降序', at);
143
+ }
144
+ }
145
+ return versions;
146
+ }
147
+ function iconProblem(icon) {
148
+ if (icon.length > MAX_ICON_CHARS)
149
+ return problem('icon', 'icon 超过 32 KiB');
150
+ if (icon.startsWith('data:image/'))
151
+ return icon.includes(';base64,') ? undefined : problem('icon', 'data URL 必须是 base64');
152
+ if (/^https?:\/\//i.test(icon))
153
+ return undefined;
154
+ return problem('icon', 'icon 只能是 base64 data URL 或 http(s) URL');
155
+ }
156
+ const MAX_OWNER_CHARS = 200;
157
+ const UNIT_ID = /^[a-z][a-z0-9-]*$/;
158
+ function ownerProblem(owner) {
159
+ if (/[\r\n]/u.test(owner))
160
+ return problem('owner', 'owner 只能是一行署名');
161
+ if (owner.length > MAX_OWNER_CHARS)
162
+ return problem('owner', 'owner 超过 200 字');
163
+ return undefined;
164
+ }
165
+ function metadataOf(raw) {
166
+ const record = asRecord(raw);
167
+ if (record === undefined)
168
+ return undefined;
169
+ const workbench = record['workbench'] === true;
170
+ const unit = asText(record['unit']);
171
+ const category = asText(record['category']);
172
+ const labels = asTextArray(record['labels']);
173
+ const keywords = asTextArray(record['keywords']);
174
+ const sortKeysRecord = asRecord(record['sortKeys']);
175
+ const sortKeys = sortKeysRecord === undefined
176
+ ? undefined
177
+ : Object.fromEntries(Object.entries(sortKeysRecord).flatMap(([key, value]) => typeof value === 'string' || typeof value === 'number' ? [[key, value]] : []));
178
+ return {
179
+ ...(workbench ? { workbench: true } : {}),
180
+ ...(unit === '' ? {} : { unit }),
181
+ ...(category === '' ? {} : { category }),
182
+ ...(labels.length === 0 ? {} : { labels }),
183
+ ...(keywords.length === 0 ? {} : { keywords }),
184
+ ...(sortKeys === undefined || Object.keys(sortKeys).length === 0 ? {} : { sortKeys }),
185
+ };
186
+ }
187
+ function entryOf(raw, at) {
188
+ const record = asRecord(raw);
189
+ if (record === undefined)
190
+ return problem('plugin', '插件条目必须是对象', at);
191
+ const name = asText(record['name']);
192
+ if (name === '' || name.length > 214 || !NPM_NAME.test(name)) {
193
+ return problem('name', `不是合法 npm 包名:${JSON.stringify(name)}`, at);
194
+ }
195
+ const versions = versionsOf(record['versions'], `${at}.versions`);
196
+ if ('kind' in versions)
197
+ return versions;
198
+ const icon = asText(record['icon']);
199
+ if (icon !== '') {
200
+ const badIcon = iconProblem(icon);
201
+ if (badIcon !== undefined)
202
+ return badIcon;
203
+ }
204
+ const title = asLocalized(record['title']);
205
+ const description = asLocalized(record['description']);
206
+ const author = asText(record['author']);
207
+ const owner = asText(record['owner']);
208
+ if (owner !== '') {
209
+ const badOwner = ownerProblem(owner);
210
+ if (badOwner !== undefined)
211
+ return badOwner;
212
+ }
213
+ const homepage = asText(record['homepage']);
214
+ const metadata = metadataOf(record['metadata']);
215
+ if (metadata?.unit !== undefined && !UNIT_ID.test(metadata.unit)) {
216
+ return problem('unit', `metadata.unit 只能是小写字母开头的 a-z0-9-(现在是 ${JSON.stringify(metadata.unit)})`, at);
217
+ }
218
+ return {
219
+ name,
220
+ versions,
221
+ ...(title === undefined ? {} : { title }),
222
+ ...(description === undefined ? {} : { description }),
223
+ ...(author === '' ? {} : { author }),
224
+ ...(owner === '' ? {} : { owner }),
225
+ ...(homepage === '' ? {} : { homepage }),
226
+ ...(icon === '' ? {} : { icon }),
227
+ ...(metadata === undefined ? {} : { metadata }),
228
+ };
229
+ }
230
+ export function parseMarketIndex(raw) {
231
+ const record = asRecord(raw);
232
+ if (record === undefined)
233
+ return { ok: false, problems: [problem('not-object', '索引必须是一个 JSON 对象')] };
234
+ if (record['format'] !== MARKET_INDEX_FORMAT) {
235
+ return {
236
+ ok: false,
237
+ problems: [problem('format', `format 必须是 ${String(MARKET_INDEX_FORMAT)}(现在是 ${JSON.stringify(record['format'])})`)],
238
+ };
239
+ }
240
+ const plugins = record['plugins'];
241
+ if (!Array.isArray(plugins))
242
+ return { ok: false, problems: [problem('plugins', 'plugins 必须是数组')] };
243
+ const entries = [];
244
+ const problems = [];
245
+ const seen = new Set();
246
+ for (const [index, item] of plugins.entries()) {
247
+ const parsed = entryOf(item, `plugins[${String(index)}]`);
248
+ if ('kind' in parsed) {
249
+ problems.push(parsed);
250
+ continue;
251
+ }
252
+ if (seen.has(parsed.name)) {
253
+ problems.push(problem('duplicate-name', `插件名重复:${parsed.name}`, `plugins[${String(index)}]`));
254
+ continue;
255
+ }
256
+ seen.add(parsed.name);
257
+ entries.push(parsed);
258
+ }
259
+ if (problems.length > 0)
260
+ return { ok: false, problems };
261
+ const title = asLocalized(record['title']);
262
+ const description = asLocalized(record['description']);
263
+ const metadata = asRecord(record['metadata']);
264
+ return {
265
+ ok: true,
266
+ index: {
267
+ format: MARKET_INDEX_FORMAT,
268
+ plugins: entries,
269
+ ...(title === undefined ? {} : { title }),
270
+ ...(description === undefined ? {} : { description }),
271
+ ...(metadata === undefined ? {} : { metadata: metadata }),
272
+ },
273
+ };
274
+ }
275
+ export function parseMarketIndexJson(text) {
276
+ try {
277
+ return parseMarketIndex(JSON.parse(text));
278
+ }
279
+ catch (error) {
280
+ return {
281
+ ok: false,
282
+ problems: [problem('not-json', `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`)],
283
+ };
284
+ }
285
+ }
286
+ export function sortMarketVersions(versions) {
287
+ return [...versions].sort((left, right) => (compareSemver(right.version, left.version) ?? 0));
288
+ }
289
+ export function upsertMarketEntry(index, entry) {
290
+ const found = index.plugins.findIndex((item) => item.name === entry.name);
291
+ if (found === -1)
292
+ return { ...index, plugins: [...index.plugins, entry] };
293
+ const current = index.plugins[found];
294
+ if (current === undefined)
295
+ return { ...index, plugins: [...index.plugins, entry] };
296
+ const merged = new Map();
297
+ for (const one of current.versions)
298
+ merged.set(one.version, one);
299
+ for (const one of entry.versions)
300
+ merged.set(one.version, one);
301
+ const owner = current.owner ?? entry.owner;
302
+ const plugins = [...index.plugins];
303
+ plugins[found] = {
304
+ ...current,
305
+ ...entry,
306
+ ...(owner === undefined ? {} : { owner }),
307
+ versions: sortMarketVersions([...merged.values()]),
308
+ };
309
+ return { ...index, plugins };
310
+ }
311
+ export function marketEntryLabels(entry) {
312
+ return entry.metadata?.labels ?? [];
313
+ }
314
+ export function filterMarketIndex(index, filter) {
315
+ const query = (filter.query ?? '').trim().toLowerCase();
316
+ const category = (filter.category ?? '').trim();
317
+ const label = (filter.label ?? '').trim();
318
+ return index.plugins.filter((entry) => {
319
+ if (category !== '' && entry.metadata?.category !== category)
320
+ return false;
321
+ if (label !== '' && !marketEntryLabels(entry).includes(label))
322
+ return false;
323
+ if (query === '')
324
+ return true;
325
+ const haystack = [
326
+ entry.name,
327
+ marketText(entry.title, 'zh'),
328
+ marketText(entry.title, 'en'),
329
+ marketText(entry.description, 'zh'),
330
+ marketText(entry.description, 'en'),
331
+ entry.author ?? '',
332
+ ...(entry.metadata?.keywords ?? []),
333
+ ]
334
+ .join(' ')
335
+ .toLowerCase();
336
+ return haystack.includes(query);
337
+ });
338
+ }
339
+ export function marketSourceKey(source) {
340
+ const canonical = JSON.stringify([source.kind, source.url.trim(), source.ref ?? '', source.path ?? '']);
341
+ return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
342
+ }
343
+ export function parseSourceConfig(raw) {
344
+ const record = asRecord(raw);
345
+ if (record === undefined)
346
+ return { ok: false, problem: problem('source', '索引源必须是对象') };
347
+ const id = asText(record['id']);
348
+ const label = asText(record['label']);
349
+ const kind = record['kind'];
350
+ const url = asText(record['url']);
351
+ if (id === '')
352
+ return { ok: false, problem: problem('source', '索引源缺 id') };
353
+ if (label === '')
354
+ return { ok: false, problem: problem('source', '索引源缺 label') };
355
+ if (kind !== 'http' && kind !== 'git')
356
+ return { ok: false, problem: problem('source', 'kind 只能是 http 或 git') };
357
+ if (kind === 'http') {
358
+ const bad = httpUrlProblem(url);
359
+ if (bad !== undefined)
360
+ return { ok: false, problem: problem('source', bad) };
361
+ return { ok: true, source: { id, label, kind, url } };
362
+ }
363
+ if (!gitUrlOk(url)) {
364
+ return { ok: false, problem: problem('source', 'git 地址不合格(只收 https / ssh / scp 形式,且不能带凭据)') };
365
+ }
366
+ const ref = asText(record['ref']);
367
+ const path = asText(record['path']);
368
+ const localPath = asText(record['localPath']);
369
+ const policy = record['policy'];
370
+ if (policy !== undefined && policy !== 'artifacts-in-repo' && policy !== 'catalog-only') {
371
+ return { ok: false, problem: problem('source', 'policy 只能是 artifacts-in-repo 或 catalog-only') };
372
+ }
373
+ return {
374
+ ok: true,
375
+ source: {
376
+ id,
377
+ label,
378
+ kind,
379
+ url,
380
+ ...(ref === '' ? {} : { ref }),
381
+ ...(path === '' ? {} : { path }),
382
+ ...(localPath === '' ? {} : { localPath }),
383
+ ...(policy === undefined ? {} : { policy: policy }),
384
+ },
385
+ };
386
+ }
387
+ function docFieldsOf(record, at) {
388
+ const name = asText(record['name']);
389
+ if (name === '' || name.length > 214 || !NPM_NAME.test(name)) {
390
+ return problem('name', `不是合法 npm 包名:${JSON.stringify(name)}`, at);
391
+ }
392
+ const icon = asText(record['icon']);
393
+ if (icon !== '') {
394
+ const badIcon = iconProblem(icon);
395
+ if (badIcon !== undefined)
396
+ return badIcon;
397
+ }
398
+ const owner = asText(record['owner']);
399
+ if (owner !== '') {
400
+ const badOwner = ownerProblem(owner);
401
+ if (badOwner !== undefined)
402
+ return badOwner;
403
+ }
404
+ const metadata = metadataOf(record['metadata']);
405
+ if (metadata?.unit !== undefined && !UNIT_ID.test(metadata.unit)) {
406
+ return problem('unit', `metadata.unit 只能是小写字母开头的 a-z0-9-(现在是 ${JSON.stringify(metadata.unit)})`, at);
407
+ }
408
+ const title = asLocalized(record['title']);
409
+ const description = asLocalized(record['description']);
410
+ const author = asText(record['author']);
411
+ const homepage = asText(record['homepage']);
412
+ return {
413
+ name,
414
+ ...(title === undefined ? {} : { title }),
415
+ ...(description === undefined ? {} : { description }),
416
+ ...(author === '' ? {} : { author }),
417
+ ...(owner === '' ? {} : { owner }),
418
+ ...(homepage === '' ? {} : { homepage }),
419
+ ...(icon === '' ? {} : { icon }),
420
+ ...(metadata === undefined ? {} : { metadata }),
421
+ };
422
+ }
423
+ export function parseMarketEntryDoc(raw) {
424
+ const record = asRecord(raw);
425
+ if (record === undefined)
426
+ return { ok: false, problems: [problem('entry', '条目文件必须是一个 JSON 对象')] };
427
+ const fields = docFieldsOf(record, 'entry');
428
+ if ('kind' in fields)
429
+ return { ok: false, problems: [fields] };
430
+ const version = asText(record['version']);
431
+ const timestamp = record['publishTimestamp'];
432
+ if (timestamp !== undefined && (!Number.isInteger(timestamp) || timestamp < 0)) {
433
+ return { ok: false, problems: [problem('timestamp', 'publishTimestamp 必须是 Unix 秒整数', 'entry')] };
434
+ }
435
+ if (version === '') {
436
+ if (timestamp === undefined) {
437
+ return { ok: false, problems: [problem('timestamp', '元数据文件要有 publishTimestamp', 'entry')] };
438
+ }
439
+ const epoch = record['epoch'];
440
+ if (epoch !== undefined && (!Number.isInteger(epoch) || epoch < 0)) {
441
+ return { ok: false, problems: [problem('timestamp', 'epoch 必须是非负整数', 'entry')] };
442
+ }
443
+ return {
444
+ ok: true,
445
+ doc: { ...fields, publishTimestamp: timestamp, ...(epoch === undefined ? {} : { epoch: epoch }) },
446
+ };
447
+ }
448
+ if (!isValidVersion(version)) {
449
+ return { ok: false, problems: [problem('version', `版本号不是合法 SemVer:${JSON.stringify(version)}`, 'entry')] };
450
+ }
451
+ const source = sourceOf(record['source'], 'entry.source');
452
+ if ('kind' in source)
453
+ 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 }),
463
+ ...(dsh === '' ? {} : { engines: { dsh } }),
464
+ },
465
+ };
466
+ }
467
+ function indexPathBase(dir) {
468
+ return dir.replace(/^\/+|\/+$/g, '') || MARKET_INDEX_DIR;
469
+ }
470
+ export function marketEntryPath(dir, name, version) {
471
+ if (!NPM_NAME.test(name))
472
+ throw new Error(`不是合法包名:${JSON.stringify(name)}`);
473
+ if (!isValidVersion(version))
474
+ throw new Error(`不是合法版本号:${JSON.stringify(version)}`);
475
+ return `${indexPathBase(dir)}/${name}/${version}.json`;
476
+ }
477
+ export function marketMetaPath(dir, name, epoch, suffix) {
478
+ if (!NPM_NAME.test(name))
479
+ throw new Error(`不是合法包名:${JSON.stringify(name)}`);
480
+ if (!Number.isInteger(epoch) || epoch < 0)
481
+ throw new Error(`epoch 不合法:${String(epoch)}`);
482
+ if (!/^[a-z0-9]{4,16}$/u.test(suffix))
483
+ throw new Error(`后缀不合法:${suffix}`);
484
+ return `${indexPathBase(dir)}/${name}/meta-${String(epoch)}-${suffix}.json`;
485
+ }
@@ -1,22 +1,14 @@
1
- import type { DiscoverFilter, IndexSourceSpec, MarketIndex, MarketIndexEntry, MarketIndexParse, MarketIndexProblem, MarketIndexVersion, MarketSourceConfig } from '../contract/protocol/market-index.js';
2
- export { marketBranchOk, marketInstallTarget, marketText } from '../lib/market-index.js';
3
- export declare const MARKET_INDEX_SEED = "{\n \"format\": 1,\n \"plugins\": []\n}\n";
1
+ import type { MarketIndex, MarketIndexEntry, MarketIndexVersion } from '../contract/protocol/market-index.js';
2
+ import type { MarketEntryFile, MarketIndexAssemble, MarketMetaDoc, MarketVersionDoc } from './market-index-doc.js';
3
+ export declare const MARKET_ENTRIES_README_LINES: readonly string[];
4
4
  export declare const MARKET_INDEX_README_LINES: readonly string[];
5
- export declare function compareSemver(left: string, right: string): number | undefined;
6
- export declare function parseMarketIndex(raw: unknown): MarketIndexParse;
7
- export declare function parseMarketIndexJson(text: string): MarketIndexParse;
8
- export declare function sortMarketVersions(versions: readonly MarketIndexVersion[]): MarketIndexVersion[];
9
- export declare function upsertMarketEntry(index: MarketIndex, entry: MarketIndexEntry): MarketIndex;
10
- export declare function marketEntryLabels(entry: MarketIndexEntry): readonly string[];
11
- export declare function filterMarketIndex(index: MarketIndex, filter: DiscoverFilter): readonly MarketIndexEntry[];
12
- export declare function marketSourceKey(source: IndexSourceSpec): string;
13
- export type SourceConfigParse = {
14
- readonly ok: true;
15
- readonly source: MarketSourceConfig;
16
- } | {
17
- readonly ok: false;
18
- readonly problem: MarketIndexProblem;
19
- };
20
- export declare function parseSourceConfig(raw: unknown): SourceConfigParse;
21
- export declare function httpUrlProblem(url: string): string | undefined;
22
- export declare function gitUrlOk(url: string): boolean;
5
+ export { marketBranchOk, marketInstallTarget, marketText } from '../lib/market-index.js';
6
+ export type { MarketEntryDoc, MarketEntryDocParse, MarketEntryFile, MarketIndexAssemble, MarketMetaDoc, MarketVersionDoc, } from './market-index-doc.js';
7
+ export { compareSemver, filterMarketIndex, gitUrlOk, httpUrlProblem, marketEntryLabels, marketEntryPath, marketMetaPath, marketSourceKey, parseMarketEntryDoc, parseMarketIndex, parseMarketIndexJson, parseSourceConfig, sortMarketVersions, upsertMarketEntry, } from './market-index-parse.js';
8
+ export type { SourceConfigParse } from './market-index-parse.js';
9
+ export declare function marketVersionDocOf(entry: MarketIndexEntry, version: MarketIndexVersion): MarketVersionDoc;
10
+ export declare function marketMetaDocOf(entry: MarketIndexEntry, epoch: number, at: number): MarketMetaDoc;
11
+ export declare function assembleMarketIndex(files: readonly MarketEntryFile[]): MarketIndexAssemble;
12
+ export declare function marketEntryFilesOf(index: MarketIndex, dir?: string): readonly MarketEntryFile[];
13
+ export declare function collectMarketEntryFiles(dir: string): readonly MarketEntryFile[];
14
+ export declare function marketEntryFilesOfLegacy(text: string, dir?: string): readonly MarketEntryFile[] | undefined;