@dsh-workbench/shared 1.1.7 → 1.1.9
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/api/blocker-table.d.ts +1 -0
- package/dist/api/blocker-table.js +1 -1
- package/dist/components/SelectField.css +0 -6
- package/dist/components/SelectField.d.ts +2 -3
- package/dist/components/SelectField.js +9 -30
- package/dist/components/SelectFieldNodes.d.ts +3 -1
- package/dist/components/SelectFieldNodes.js +2 -1
- package/dist/components/SelectFieldWheel.d.ts +9 -0
- package/dist/components/SelectFieldWheel.js +35 -0
- package/dist/config/constants.d.ts +4 -4
- package/dist/config/constants.js +4 -4
- package/dist/contract/capabilities.d.ts +29 -0
- package/dist/contract/capabilities.js +1 -0
- package/dist/contract/needs.d.ts +3 -3
- package/dist/contract/needs.js +1 -1
- package/dist/contract/plugin-source.d.ts +19 -0
- package/dist/contract/protocol/bundle-install.d.ts +3 -1
- package/dist/contract/protocol/bundle-install.js +3 -0
- package/dist/contract/protocol/market-index.d.ts +1 -0
- package/dist/contract/protocol/platform.d.ts +1 -1
- package/dist/contract/runner.d.ts +6 -0
- package/dist/contract/runner.js +1 -0
- package/dist/contract/unit.d.ts +4 -4
- package/dist/host/adapter-contract.d.ts +3 -0
- package/dist/host/adapter-contract.js +93 -0
- package/dist/host/adapter-set.js +6 -4
- package/dist/host/adapter-types.d.ts +1 -1
- package/dist/host/bundle-manager.d.ts +3 -0
- package/dist/host/bundle-manager.js +181 -0
- package/dist/host/cli.js +3 -1
- package/dist/host/config-sync.d.ts +48 -0
- package/dist/host/config-sync.js +342 -0
- package/dist/host/dsh-plugins.d.ts +5 -0
- package/dist/host/dsh-plugins.js +51 -0
- package/dist/host/extension.js +1 -1
- package/dist/host/flow/check.d.ts +9 -0
- package/dist/host/flow/check.js +179 -0
- package/dist/host/flow/expr.d.ts +30 -0
- package/dist/host/flow/expr.js +99 -0
- package/dist/host/flow/kit.d.ts +14 -0
- package/dist/host/flow/kit.js +13 -0
- package/dist/host/flow/run.d.ts +13 -0
- package/dist/host/flow/run.js +287 -0
- package/dist/host/flow/types.d.ts +188 -0
- package/dist/host/flow/types.js +1 -0
- package/dist/host/host-actions.d.ts +2 -0
- package/dist/host/host-actions.js +5 -0
- package/dist/host/host-kit.d.ts +19 -0
- package/dist/host/host-kit.js +14 -0
- package/dist/host/http.d.ts +2 -6
- package/dist/host/http.js +2 -2
- package/dist/host/installed-module.d.ts +23 -0
- package/dist/host/installed-module.js +264 -0
- package/dist/host/lib-install.d.ts +7 -9
- package/dist/host/lib-install.js +9 -12
- package/dist/host/lib-runners.d.ts +3 -5
- package/dist/host/lib-runners.js +2 -2
- package/dist/host/local-declaration.d.ts +7 -0
- package/dist/host/local-declaration.js +33 -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/host/module-root.d.ts +1 -0
- package/dist/host/module-root.js +5 -0
- package/dist/host/npm-registry.d.ts +10 -0
- package/dist/host/npm-registry.js +16 -0
- package/dist/host/opencli-runner.d.ts +2 -1
- package/dist/host/package-content.d.ts +7 -0
- package/dist/host/package-content.js +69 -0
- package/dist/host/package-kind.d.ts +13 -0
- package/dist/host/package-kind.js +51 -0
- package/dist/host/package-route.d.ts +31 -0
- package/dist/host/package-route.js +57 -0
- package/dist/host/paths.d.ts +11 -19
- package/dist/host/paths.js +16 -43
- package/dist/host/platform.js +1 -1
- package/dist/host/plugin-ledger.d.ts +8 -8
- package/dist/host/plugin-ledger.js +23 -16
- package/dist/host/plugin.d.ts +1 -2
- package/dist/host/plugin.js +4 -5
- package/dist/host/port-types.d.ts +42 -9
- package/dist/host/preflight.d.ts +3 -2
- package/dist/host/profile-create.d.ts +18 -0
- package/dist/host/profile-create.js +118 -0
- package/dist/host/profile-registries.d.ts +10 -0
- package/dist/host/profile-registries.js +150 -0
- package/dist/host/registry-declaration.d.ts +21 -0
- package/dist/host/registry-declaration.js +33 -0
- package/dist/host/release-probe.d.ts +34 -0
- package/dist/host/release-probe.js +88 -0
- package/dist/host/runner.d.ts +26 -0
- package/dist/host/runner.js +50 -0
- package/dist/host/shell.d.ts +0 -2
- package/dist/host/shell.js +0 -7
- package/dist/host/spec.js +1 -1
- package/dist/host/supply-index.d.ts +1 -0
- package/dist/host/supply-index.js +8 -7
- package/dist/host/supply-port.js +13 -3
- package/dist/host/unit-face.d.ts +2 -1
- package/dist/host/version.js +11 -6
- package/dist/host.d.ts +24 -6
- package/dist/host.js +23 -6
- package/dist/i18n/en.js +19 -20
- package/dist/i18n/index.d.ts +20 -21
- package/dist/i18n/runtime-terms.d.ts +15 -0
- package/dist/i18n/runtime-terms.js +22 -0
- package/dist/i18n/zh.d.ts +19 -20
- package/dist/i18n/zh.js +19 -20
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/testing/fake-adapter-set.d.ts +5 -0
- package/dist/testing/fake-adapter-set.js +150 -0
- package/dist/testing/needs.js +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/package.json +1 -1
- package/src/api/__tests__/blocker-table.test.ts +25 -0
- package/src/api/{workbench-client.test.ts → __tests__/workbench-client.test.ts} +2 -2
- package/src/api/blocker-table.ts +6 -2
- package/src/components/SelectField.css +0 -6
- package/src/components/SelectField.tsx +7 -36
- package/src/components/SelectFieldNodes.tsx +4 -1
- package/src/components/{Card.test.tsx → __tests__/Card.test.tsx} +2 -2
- package/src/components/{CardAnchor.test.tsx → __tests__/CardAnchor.test.tsx} +3 -3
- package/src/components/{CardHead.test.tsx → __tests__/CardHead.test.tsx} +1 -1
- package/src/components/{ChoiceGroup.test.tsx → __tests__/ChoiceGroup.test.tsx} +2 -2
- package/src/components/{ConfirmBar.test.tsx → __tests__/ConfirmBar.test.tsx} +5 -5
- package/src/components/{Field.test.tsx → __tests__/Field.test.tsx} +1 -1
- package/src/components/{Form.test.tsx → __tests__/Form.test.tsx} +5 -5
- package/src/components/{FormActions.test.tsx → __tests__/FormActions.test.tsx} +3 -3
- package/src/components/{FormRows.test.tsx → __tests__/FormRows.test.tsx} +2 -2
- package/src/components/{FormTypes.test.ts → __tests__/FormTypes.test.ts} +1 -1
- package/src/components/{FormWiring.test.tsx → __tests__/FormWiring.test.tsx} +4 -4
- package/src/components/{Link.test.tsx → __tests__/Link.test.tsx} +1 -1
- package/src/components/{Rows.test.tsx → __tests__/Rows.test.tsx} +2 -2
- package/src/components/{SelectField.test.tsx → __tests__/SelectField.test.tsx} +7 -37
- package/src/components/{Spinner.test.tsx → __tests__/Spinner.test.tsx} +4 -4
- package/src/config/{constants.test.ts → __tests__/constants.test.ts} +1 -1
- package/src/config/{storage.test.ts → __tests__/storage.test.ts} +1 -1
- package/src/config/constants.ts +4 -4
- package/src/contract/capabilities.ts +30 -0
- package/src/contract/needs.ts +4 -4
- package/src/contract/plugin-source.ts +24 -0
- package/src/contract/protocol/bundle-install.ts +7 -1
- package/src/contract/protocol/market-index.ts +1 -0
- package/src/contract/protocol/platform.ts +1 -1
- package/src/contract/runner.ts +6 -0
- package/src/contract/unit.ts +4 -4
- package/src/host/{extension.test.ts → __tests__/extension.test.ts} +4 -4
- package/src/host/{install-failure.test.ts → __tests__/install-failure.test.ts} +1 -1
- package/src/host/{installed-sources.test.ts → __tests__/installed-sources.test.ts} +1 -1
- package/src/host/__tests__/lib-runners.test.ts +11 -2
- package/src/host/__tests__/market-index-assemble.test.ts +26 -0
- package/src/host/__tests__/market-index.test.ts +41 -51
- package/src/host/__tests__/plugin-ledger.test.ts +19 -6
- package/src/host/{recipe.test.ts → __tests__/recipe.test.ts} +3 -3
- package/src/host/__tests__/runtime-layout.test.ts +14 -0
- package/src/host/__tests__/supply-index.test.ts +2 -2
- package/src/host/__tests__/supply-port.test.ts +32 -0
- package/src/host/adapter-contract.ts +93 -0
- package/src/host/adapter-set.ts +6 -4
- package/src/host/adapter-types.ts +1 -1
- package/src/host/bundle-manager.ts +195 -0
- package/src/host/cli.ts +3 -1
- package/src/host/config-sync.ts +386 -0
- package/src/host/dsh-plugins.ts +60 -0
- package/src/host/extension.ts +1 -1
- package/src/host/flow/__tests__/advanced.test.ts +302 -0
- package/src/host/flow/__tests__/check.test.ts +169 -0
- package/src/host/flow/__tests__/run.test.ts +301 -0
- package/src/host/flow/check.ts +216 -0
- package/src/host/flow/expr.ts +94 -0
- package/src/host/flow/kit.ts +30 -0
- package/src/host/flow/run.ts +325 -0
- package/src/host/flow/types.ts +199 -0
- package/src/host/host-actions.ts +7 -0
- package/src/host/host-kit.ts +26 -0
- package/src/host/http.ts +6 -8
- package/src/host/installed-module.ts +272 -0
- package/src/host/lib-install.ts +14 -12
- package/src/host/lib-runners.ts +6 -8
- package/src/host/local-declaration.ts +37 -0
- 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/host/module-root.ts +6 -0
- package/src/host/npm-registry.ts +21 -0
- package/src/host/package-content.ts +76 -0
- package/src/host/package-kind.ts +63 -0
- package/src/host/package-route.ts +93 -0
- package/src/host/paths.ts +27 -52
- package/src/host/platform.ts +1 -1
- package/src/host/plugin-ledger.ts +25 -18
- package/src/host/plugin.ts +4 -5
- package/src/host/port-types.ts +43 -9
- package/src/host/preflight.ts +3 -2
- package/src/host/profile-create.ts +133 -0
- package/src/host/profile-registries.ts +145 -0
- package/src/host/registry-declaration.ts +70 -0
- package/src/host/release-probe.ts +202 -0
- package/src/host/shell.ts +0 -9
- package/src/host/spec.ts +1 -1
- package/src/host/supply-index.ts +9 -7
- package/src/host/supply-port.ts +13 -3
- package/src/host/unit-face.ts +2 -1
- package/src/host/version.ts +12 -7
- package/src/host.ts +34 -6
- package/src/i18n/{index.test.ts → __tests__/index.test.ts} +3 -3
- package/src/i18n/{merge.test.ts → __tests__/merge.test.ts} +1 -1
- package/src/i18n/{zh.test.ts → __tests__/zh.test.ts} +2 -2
- package/src/i18n/en.ts +19 -21
- package/src/i18n/runtime-terms.ts +26 -0
- package/src/i18n/zh.ts +19 -21
- package/src/index.ts +4 -0
- package/src/lib/{board-fact.test.ts → __tests__/board-fact.test.ts} +1 -1
- package/src/lib/{busy.test.ts → __tests__/busy.test.ts} +1 -1
- package/src/lib/{form-problem.test.ts → __tests__/form-problem.test.ts} +1 -1
- package/src/lib/{git-address.test.ts → __tests__/git-address.test.ts} +1 -1
- package/src/lib/{react.test.ts → __tests__/react.test.ts} +1 -1
- package/src/lib/{text.test.ts → __tests__/text.test.ts} +1 -1
- package/src/lib/{ui.test.ts → __tests__/ui.test.ts} +1 -1
- package/src/testing/__tests__/fake-adapter-set.test.ts +28 -0
- package/src/testing/fake-adapter-set.ts +173 -0
- package/src/testing/needs.ts +1 -1
- package/src/testing.ts +1 -0
- package/src/host/__tests__/command-name.test.ts +0 -16
- package/src/host/__tests__/opencli-runner.test.ts +0 -120
- package/src/host/channel-plugin.ts +0 -55
- package/src/host/command-name.ts +0 -15
- package/src/host/opencli-runner.ts +0 -76
- package/src/host/paths.test.ts +0 -26
- package/src/host/plugin-source.ts +0 -137
- package/src/host/runtime-links.test.ts +0 -31
- package/src/host/runtime-links.ts +0 -23
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { DshHostContext } from '../api/dsh.js';
|
|
4
|
+
import { declarationProblemsOf, readDeclaration } from '../contract/unit-declaration.js';
|
|
5
|
+
import { packageSourceOf } from './installed-sources.js';
|
|
6
|
+
import { scopeOf } from './adapter-types.js';
|
|
7
|
+
import { isPackageName } from './spec.js';
|
|
8
|
+
import type { InstalledModule, ProfileModule, ProfilePackage } from './adapter-types.js';
|
|
9
|
+
export interface UnitDeclarationSource {
|
|
10
|
+
readonly pkg: string;
|
|
11
|
+
readonly declaration: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
15
|
+
return typeof value === 'object' && value !== null ? (value as Readonly<Record<string, unknown>>) : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function asText(value: unknown): string {
|
|
19
|
+
return typeof value === 'string' ? value : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function profileDirOf(ctx: DshHostContext): string | null {
|
|
23
|
+
const profile = ctx.get<{ dir?: unknown }>('profileContext');
|
|
24
|
+
const dir = typeof profile?.dir === 'string' ? profile.dir.trim() : '';
|
|
25
|
+
return dir === '' ? null : dir;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stringsOf(value: unknown): string[] {
|
|
29
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function profileManifestOf(profileDir: string | null): Readonly<Record<string, unknown>> | undefined {
|
|
33
|
+
if (profileDir === null) return undefined;
|
|
34
|
+
try {
|
|
35
|
+
return asRecord(JSON.parse(fs.readFileSync(path.join(profileDir, 'package.json'), 'utf8')));
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function profileModules(profileDir: string | null): readonly ProfileModule[] {
|
|
42
|
+
const record = profileManifestOf(profileDir);
|
|
43
|
+
if (record === undefined) return [];
|
|
44
|
+
const bundles = stringsOf(asRecord(asRecord(record.dsh)?.profile)?.bundles);
|
|
45
|
+
const dependencies = asRecord(record.dependencies) ?? {};
|
|
46
|
+
const names = bundles.length > 0 ? bundles : Object.keys(dependencies);
|
|
47
|
+
const found: ProfileModule[] = [];
|
|
48
|
+
for (const pkg of names) {
|
|
49
|
+
const read = readInstalledModule(profileDir, pkg);
|
|
50
|
+
if (read.kind === 'module') {
|
|
51
|
+
const spec = asText(dependencies[read.pkg]);
|
|
52
|
+
found.push({
|
|
53
|
+
pkg: read.pkg,
|
|
54
|
+
unit: read.unit,
|
|
55
|
+
version: read.version,
|
|
56
|
+
...(read.registry === undefined ? {} : { registry: read.registry }),
|
|
57
|
+
local: isLocalDependencySpec(spec),
|
|
58
|
+
fromRegistry: isRegistryDependencySpec(spec),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createDeclarationLookup(
|
|
66
|
+
profileDir: string | null,
|
|
67
|
+
): (unit: string) => UnitDeclarationSource | undefined {
|
|
68
|
+
let roster: Map<string, string> | undefined;
|
|
69
|
+
const scan = (): Map<string, string> =>
|
|
70
|
+
new Map(profileModules(profileDir).map((row) => [row.unit, row.pkg]));
|
|
71
|
+
return (unit) => {
|
|
72
|
+
roster ??= scan();
|
|
73
|
+
let pkg = roster.get(unit);
|
|
74
|
+
if (pkg === undefined) {
|
|
75
|
+
roster = scan();
|
|
76
|
+
pkg = roster.get(unit);
|
|
77
|
+
}
|
|
78
|
+
return pkg === undefined ? undefined : { pkg, declaration: moduleDeclarationOf(profileDir, pkg) };
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function registryFromLockfile(profileDir: string, pkg: string): string | undefined {
|
|
83
|
+
let text: string;
|
|
84
|
+
try {
|
|
85
|
+
text = fs.readFileSync(path.join(profileDir, 'pnpm-lock.yaml'), 'utf8');
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const at = text.indexOf(` ${pkg}@`);
|
|
90
|
+
const quoted = at === -1 ? text.indexOf(` '${pkg}@`) : at;
|
|
91
|
+
if (quoted === -1) return undefined;
|
|
92
|
+
const headerEnd = text.indexOf('\n', quoted);
|
|
93
|
+
const end = headerEnd === -1 ? -1 : text.indexOf('\n ', headerEnd + 1);
|
|
94
|
+
const block = text.slice(quoted, end === -1 ? quoted + 900 : end);
|
|
95
|
+
const match = /tarball:\s*([^\s,}]+)/.exec(block);
|
|
96
|
+
const tarball = match?.[1];
|
|
97
|
+
if (tarball === undefined || !tarball.startsWith('http')) return undefined;
|
|
98
|
+
const cut = tarball.lastIndexOf(`/${pkg}/`);
|
|
99
|
+
if (cut <= 'https://'.length) return undefined;
|
|
100
|
+
return tarball.slice(0, cut);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function unquoted(value: string): string {
|
|
104
|
+
const quote = value.charAt(0);
|
|
105
|
+
if (value.length >= 2 && (quote === '"' || quote === "'") && value.endsWith(quote)) {
|
|
106
|
+
return value.slice(1, -1).trim();
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function registryFromNpmrc(profileDir: string, pkg: string): string | undefined {
|
|
112
|
+
let text: string;
|
|
113
|
+
try {
|
|
114
|
+
text = fs.readFileSync(path.join(profileDir, '.npmrc'), 'utf8');
|
|
115
|
+
} catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const scope = scopeOf(pkg);
|
|
119
|
+
let fallback: string | undefined;
|
|
120
|
+
for (const line of text.split('\n')) {
|
|
121
|
+
const trimmed = line.trim();
|
|
122
|
+
if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith(';')) continue;
|
|
123
|
+
const separator = trimmed.indexOf('=');
|
|
124
|
+
if (separator <= 0) continue;
|
|
125
|
+
const key = trimmed.slice(0, separator).trim();
|
|
126
|
+
const value = unquoted(trimmed.slice(separator + 1).trim());
|
|
127
|
+
if (value === '') continue;
|
|
128
|
+
if (scope !== null && key === `${scope}:registry`) return value;
|
|
129
|
+
if (key === 'registry') fallback = value;
|
|
130
|
+
}
|
|
131
|
+
return fallback;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function registryOf(
|
|
135
|
+
profileDir: string,
|
|
136
|
+
pkg: string,
|
|
137
|
+
manifest: Readonly<Record<string, unknown>>,
|
|
138
|
+
): string | undefined {
|
|
139
|
+
const declared = asText(asRecord(manifest.publishConfig)?.registry);
|
|
140
|
+
if (declared !== '') return declared;
|
|
141
|
+
const recorded = packageSourceOf(profileDir, pkg);
|
|
142
|
+
if (recorded !== undefined) return recorded.registry;
|
|
143
|
+
const locked = registryFromLockfile(profileDir, pkg);
|
|
144
|
+
return locked === undefined ? registryFromNpmrc(profileDir, pkg) : locked;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function moduleDeclarationOf(profileDir: string | null, pkg: string): unknown {
|
|
148
|
+
if (profileDir === null || !isPackageName(pkg)) return undefined;
|
|
149
|
+
try {
|
|
150
|
+
const text = fs.readFileSync(path.join(profileDir, 'node_modules', pkg, 'package.json'), 'utf8');
|
|
151
|
+
return asRecord(asRecord(asRecord(JSON.parse(text))?.dsh)?.workbench);
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface DeclarationGap {
|
|
158
|
+
readonly pkg: string;
|
|
159
|
+
readonly problems: readonly string[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function declarationGaps(profileDir: string | null): readonly DeclarationGap[] | undefined {
|
|
163
|
+
if (profileManifestOf(profileDir) === undefined) return undefined;
|
|
164
|
+
const gaps: DeclarationGap[] = [];
|
|
165
|
+
for (const module of profileModules(profileDir)) {
|
|
166
|
+
const problems = declarationProblemsOf(module.pkg, moduleDeclarationOf(profileDir, module.pkg));
|
|
167
|
+
if (problems.length > 0) gaps.push({ pkg: module.pkg, problems });
|
|
168
|
+
}
|
|
169
|
+
return gaps;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function declaredPluginCommands(profileDir: string | null): ReadonlyMap<string, ReadonlySet<string>> {
|
|
173
|
+
const declared = new Map<string, Set<string>>();
|
|
174
|
+
for (const module of profileModules(profileDir)) {
|
|
175
|
+
const read = readDeclaration(module.pkg, moduleDeclarationOf(profileDir, module.pkg));
|
|
176
|
+
for (const need of read.manifest?.needs ?? []) {
|
|
177
|
+
if (need.kind !== 'extension') continue;
|
|
178
|
+
const name = need.name.trim();
|
|
179
|
+
if (name === '') continue;
|
|
180
|
+
const commands = declared.get(name) ?? new Set<string>();
|
|
181
|
+
for (const command of need.commands ?? []) commands.add(command);
|
|
182
|
+
declared.set(name, commands);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return declared;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function readInstalledModule(profileDir: string | null, pkg: string): InstalledModule {
|
|
189
|
+
if (profileDir === null || !isPackageName(pkg)) return { kind: 'unknown', pkg };
|
|
190
|
+
let manifest: unknown;
|
|
191
|
+
try {
|
|
192
|
+
manifest = JSON.parse(fs.readFileSync(path.join(profileDir, 'node_modules', pkg, 'package.json'), 'utf8'));
|
|
193
|
+
} catch {
|
|
194
|
+
return { kind: 'unknown', pkg };
|
|
195
|
+
}
|
|
196
|
+
const record = asRecord(manifest);
|
|
197
|
+
if (record === undefined) return { kind: 'unknown', pkg };
|
|
198
|
+
const declared = asRecord(asRecord(record.dsh)?.workbench);
|
|
199
|
+
const unit = declared === undefined ? '' : asText(declared.unit);
|
|
200
|
+
if (unit === '') return { kind: 'not-module', pkg };
|
|
201
|
+
const registry = registryOf(profileDir, pkg, record);
|
|
202
|
+
return {
|
|
203
|
+
kind: 'module',
|
|
204
|
+
pkg,
|
|
205
|
+
unit,
|
|
206
|
+
version: asText(record.version),
|
|
207
|
+
...(registry === undefined ? {} : { registry }),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const PACKED_ARTIFACT = /\.(?:tgz|tar\.gz|tar)$/i;
|
|
212
|
+
|
|
213
|
+
export function isPackedArtifactSpec(spec: string): boolean {
|
|
214
|
+
const trimmed = spec.trim();
|
|
215
|
+
const pkgPath = trimmed.startsWith('file:') ? trimmed.slice('file:'.length) : trimmed;
|
|
216
|
+
return PACKED_ARTIFACT.test(pkgPath);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function isLocalDependencySpec(spec: string): boolean {
|
|
220
|
+
const trimmed = spec.trim();
|
|
221
|
+
if (isPackedArtifactSpec(trimmed)) return false;
|
|
222
|
+
return (
|
|
223
|
+
trimmed.startsWith('link:') ||
|
|
224
|
+
trimmed.startsWith('file:') ||
|
|
225
|
+
trimmed.startsWith('./') ||
|
|
226
|
+
trimmed.startsWith('../') ||
|
|
227
|
+
trimmed.startsWith('/')
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const NON_REGISTRY_SPEC = /^(?:git\+|git:|github:|gitlab:|bitbucket:|https?:|ssh:|workspace:)/i;
|
|
232
|
+
|
|
233
|
+
export function isRegistryDependencySpec(spec: string): boolean {
|
|
234
|
+
const trimmed = spec.trim();
|
|
235
|
+
if (trimmed === '') return true;
|
|
236
|
+
if (isLocalDependencySpec(trimmed) || isPackedArtifactSpec(trimmed)) return false;
|
|
237
|
+
return !NON_REGISTRY_SPEC.test(trimmed);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function readProfilePackage(profileDir: string | null, pkg: string): ProfilePackage | undefined {
|
|
241
|
+
if (profileDir === null || !isPackageName(pkg)) return undefined;
|
|
242
|
+
let manifest: unknown;
|
|
243
|
+
try {
|
|
244
|
+
manifest = JSON.parse(fs.readFileSync(path.join(profileDir, 'node_modules', pkg, 'package.json'), 'utf8'));
|
|
245
|
+
} catch {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
const record = asRecord(manifest);
|
|
249
|
+
if (record === undefined) return undefined;
|
|
250
|
+
const version = asText(record.version);
|
|
251
|
+
if (version === '') return undefined;
|
|
252
|
+
const registry = registryOf(profileDir, pkg, record);
|
|
253
|
+
const dependencies = asRecord(profileManifestOf(profileDir)?.dependencies) ?? {};
|
|
254
|
+
const local = isLocalDependencySpec(asText(dependencies[pkg]));
|
|
255
|
+
return {
|
|
256
|
+
pkg,
|
|
257
|
+
version,
|
|
258
|
+
...(registry === undefined ? {} : { registry }),
|
|
259
|
+
...(local ? { local: true } : {}),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function packageVersionAt(packageRoot: string): string | undefined {
|
|
264
|
+
let manifest: unknown;
|
|
265
|
+
try {
|
|
266
|
+
manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
267
|
+
} catch {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
const version = asText(asRecord(manifest)?.version);
|
|
271
|
+
return version === '' ? undefined : version;
|
|
272
|
+
}
|
package/src/host/lib-install.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { BinNeed, LibNeed, SupplySource } from '../contract/needs.js';
|
|
2
2
|
import type { HostOs } from '../contract/protocol/platform.js';
|
|
3
|
-
import { npmRegistryOf } from './
|
|
3
|
+
import { npmRegistryOf } from './npm-registry.js';
|
|
4
4
|
import { quoteShellArg } from './shell-text.js';
|
|
5
|
-
import { libsPrefix, npmCacheDir } from './paths.js';
|
|
6
5
|
|
|
7
6
|
export type NpmSource = Extract<SupplySource, { kind: 'npm' }>;
|
|
8
7
|
|
|
@@ -11,26 +10,23 @@ export function libInstallCommand(options: {
|
|
|
11
10
|
readonly source: NpmSource;
|
|
12
11
|
readonly version?: string | undefined;
|
|
13
12
|
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
13
|
+
readonly prefix: string;
|
|
14
|
+
readonly cacheDir?: string | undefined;
|
|
14
15
|
}): string {
|
|
15
16
|
const { need, source, version } = options;
|
|
16
17
|
const registry = npmRegistryOf(source, options.env ?? process.env);
|
|
17
18
|
const spec = version ?? need.version ?? 'latest';
|
|
18
19
|
const registryFlag = registry.ok ? ` --registry ${quoteShellArg(registry.value)}` : '';
|
|
19
|
-
|
|
20
|
+
const cacheFlag = options.cacheDir === undefined ? '' : ` --cache ${quoteShellArg(options.cacheDir)}`;
|
|
21
|
+
return `npm i --prefix ${quoteShellArg(options.prefix)} ${quoteShellArg(`${need.name}@${spec}`)}${registryFlag}${cacheFlag}`;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
/**
|
|
23
|
-
* 这个 bin 在这台机器上怎么装:作者写了 `install` 且平台对得上才给出那条命令。
|
|
24
|
-
* `command` 原样照抄;`npm` 装进隔离运行时的共用依赖前缀(那份包里的 bin 会自动上 PATH)。
|
|
25
|
-
* @param need 一条 bin 需要。
|
|
26
|
-
* @param os 宿主平台(由平台适配器给出)。
|
|
27
|
-
* @param env 取 registry 环境变量的地方。
|
|
28
|
-
* @returns 一条能直接粘进终端的命令;没声明或平台不对就是 undefined。
|
|
29
|
-
*/
|
|
30
24
|
export function binInstallCommand(
|
|
31
25
|
need: BinNeed,
|
|
32
26
|
os: HostOs,
|
|
33
|
-
env: NodeJS.ProcessEnv
|
|
27
|
+
env: NodeJS.ProcessEnv,
|
|
28
|
+
prefix: string,
|
|
29
|
+
cacheDir?: string,
|
|
34
30
|
): string | undefined {
|
|
35
31
|
const install = need.install;
|
|
36
32
|
if (install === undefined) return undefined;
|
|
@@ -42,6 +38,8 @@ export function binInstallCommand(
|
|
|
42
38
|
const packageName = install.package.trim();
|
|
43
39
|
if (packageName === '') return undefined;
|
|
44
40
|
return libInstallCommand({
|
|
41
|
+
prefix,
|
|
42
|
+
...(cacheDir === undefined ? {} : { cacheDir }),
|
|
45
43
|
need: { kind: 'lib', name: packageName, purpose: need.purpose },
|
|
46
44
|
source: {
|
|
47
45
|
kind: 'npm',
|
|
@@ -52,3 +50,7 @@ export function binInstallCommand(
|
|
|
52
50
|
env,
|
|
53
51
|
});
|
|
54
52
|
}
|
|
53
|
+
|
|
54
|
+
export function libUninstallCommand(options: { readonly need: LibNeed; readonly prefix: string }): string {
|
|
55
|
+
return `npm uninstall --prefix ${quoteShellArg(options.prefix)} ${quoteShellArg(options.need.name)}`;
|
|
56
|
+
}
|
package/src/host/lib-runners.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import type { LibNeed } from '../contract/needs.js';
|
|
2
2
|
import type { LibReader } from '../contract/lib.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { LibRunner } from '../contract/runner.js';
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
readonly opencli?: OpencliRunner | undefined;
|
|
7
|
-
}
|
|
5
|
+
export type LibRuntimes = Readonly<Record<string, LibRunner>>;
|
|
8
6
|
|
|
9
7
|
interface Logger {
|
|
10
8
|
warn?(message: string): void;
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
function runnerOf(runtimes: LibRuntimes, runtime: string):
|
|
14
|
-
return runtime
|
|
11
|
+
function runnerOf(runtimes: LibRuntimes, runtime: string): LibRunner | undefined {
|
|
12
|
+
return runtimes[runtime];
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
function setterNameOf(runtime: string): string {
|
|
@@ -19,7 +17,7 @@ function setterNameOf(runtime: string): string {
|
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
/**
|
|
22
|
-
* 给声明了 `runs` 的 lib 注入「跑这个运行时」的执行器:库侧按约定暴露 `
|
|
20
|
+
* 给声明了 `runs` 的 lib 注入「跑这个运行时」的执行器:库侧按约定暴露 `set<Runtime>Runner`
|
|
23
21
|
* (运行时名字首字母大写 + `Runner`),外壳在每次 `load` 之后把执行器交给它。
|
|
24
22
|
*
|
|
25
23
|
* 没暴露的(版本旧)就保持它自己的行为(按 PATH 找),只记一条 warn;不熟悉的运行时同样只 warn。
|
|
@@ -62,7 +60,7 @@ export function withLibRunners(
|
|
|
62
60
|
);
|
|
63
61
|
continue;
|
|
64
62
|
}
|
|
65
|
-
(setter as (runner:
|
|
63
|
+
(setter as (runner: LibRunner) => void).call(module, runner);
|
|
66
64
|
}
|
|
67
65
|
return module;
|
|
68
66
|
},
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { localDirOf } from './spec.js';
|
|
4
|
+
export { localDirOf };
|
|
5
|
+
import { declarationProblemsOf } from '../contract/unit-declaration.js';
|
|
6
|
+
|
|
7
|
+
function asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
8
|
+
return typeof value === 'object' && value !== null ? (value as Readonly<Record<string, unknown>>) : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function packageManifestOf(dir: string): Readonly<Record<string, unknown>> | undefined {
|
|
12
|
+
try {
|
|
13
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
14
|
+
return asRecord(parsed);
|
|
15
|
+
} catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function dshRecordOf(dir: string): unknown | undefined {
|
|
21
|
+
return packageManifestOf(dir)?.dsh;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function packageNameOf(dir: string): string | undefined {
|
|
25
|
+
const name = packageManifestOf(dir)?.name;
|
|
26
|
+
return typeof name === 'string' && name.trim() !== '' ? name.trim() : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function pathDeclarationOf(spec: string): unknown | undefined {
|
|
30
|
+
const dir = localDirOf(spec);
|
|
31
|
+
return dir === undefined ? undefined : asRecord(dshRecordOf(dir))?.workbench;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function declarationRefusalOf(name: string, raw: unknown): readonly string[] {
|
|
35
|
+
if (raw === undefined) return [];
|
|
36
|
+
return declarationProblemsOf(name, raw);
|
|
37
|
+
}
|
|
@@ -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
|
+
|