@dsh-workbench/shared 1.1.8 → 1.1.10
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 -1
- 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/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 +23 -24
- package/dist/i18n/index.d.ts +23 -24
- package/dist/i18n/runtime-terms.d.ts +15 -0
- package/dist/i18n/runtime-terms.js +22 -0
- package/dist/i18n/zh.d.ts +22 -23
- package/dist/i18n/zh.js +22 -23
- 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__/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/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 +23 -25
- package/src/i18n/runtime-terms.ts +26 -0
- package/src/i18n/zh.ts +22 -24
- 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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function envValue(
|
|
2
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
3
|
+
name: string,
|
|
4
|
+
): { readonly ok: true; readonly value: string } | { readonly ok: false } {
|
|
5
|
+
const value = env[name]?.trim();
|
|
6
|
+
return value === undefined || value === '' ? { ok: false } : { ok: true, value };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function npmRegistryOf(
|
|
10
|
+
source: { readonly registryEnv?: string; readonly registryDefault?: string },
|
|
11
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
12
|
+
): { readonly ok: true; readonly value: string } | { readonly ok: false; readonly env: string } {
|
|
13
|
+
const name = source.registryEnv ?? '';
|
|
14
|
+
if (name !== '') {
|
|
15
|
+
const fromEnv = envValue(env, name);
|
|
16
|
+
if (fromEnv.ok) return fromEnv;
|
|
17
|
+
}
|
|
18
|
+
const fallback = source.registryDefault?.trim();
|
|
19
|
+
if (fallback === undefined || fallback === '') return { ok: false, env: name };
|
|
20
|
+
return { ok: true, value: fallback };
|
|
21
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type { DshShell } from '../api/dsh.js';
|
|
5
|
+
import { INSTALL_TIMEOUT_MS } from '../config/constants.js';
|
|
6
|
+
import { parseJsonLoose } from '../lib/text.js';
|
|
7
|
+
import { quoteShellArg } from './shell-text.js';
|
|
8
|
+
import { runShell } from './shell.js';
|
|
9
|
+
import { splitNpmSpec } from './package-kind.js';
|
|
10
|
+
import { localDirOf } from './local-declaration.js';
|
|
11
|
+
|
|
12
|
+
function cacheFlag(cacheDir: string | undefined): string {
|
|
13
|
+
return cacheDir === undefined ? '' : ` --cache ${quoteShellArg(cacheDir)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function packedFileFrom(stdout: string, root: string): string | undefined {
|
|
17
|
+
const parsed = parseJsonLoose(stdout);
|
|
18
|
+
if (Array.isArray(parsed)) {
|
|
19
|
+
const first: unknown = parsed[0];
|
|
20
|
+
const filename =
|
|
21
|
+
typeof first === 'object' && first !== null ? (first as { filename?: unknown }).filename : undefined;
|
|
22
|
+
if (typeof filename === 'string' && filename.trim() !== '') {
|
|
23
|
+
const at = path.join(root, path.basename(filename.trim()));
|
|
24
|
+
if (fs.existsSync(at)) return at;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const found = fs.readdirSync(root).filter((name) => name.endsWith('.tgz'));
|
|
29
|
+
return found.length === 1 ? path.join(root, found[0] ?? '') : undefined;
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function withPackageDir<T>(
|
|
36
|
+
options: {
|
|
37
|
+
readonly shell?: DshShell | undefined;
|
|
38
|
+
readonly spec: string;
|
|
39
|
+
readonly registry?: string | undefined;
|
|
40
|
+
readonly cacheDir?: string | undefined;
|
|
41
|
+
},
|
|
42
|
+
visit: (dir: string) => T,
|
|
43
|
+
): Promise<T | undefined> {
|
|
44
|
+
const local = localDirOf(options.spec);
|
|
45
|
+
if (local !== undefined) return visit(local);
|
|
46
|
+
if (splitNpmSpec(options.spec) === undefined) return undefined;
|
|
47
|
+
const shell = options.shell;
|
|
48
|
+
if (shell === undefined) return undefined;
|
|
49
|
+
|
|
50
|
+
let root: string;
|
|
51
|
+
try {
|
|
52
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'wb-package-'));
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const at = options.registry === undefined ? '' : ` --registry ${quoteShellArg(options.registry)}`;
|
|
58
|
+
const packed = await runShell(
|
|
59
|
+
shell,
|
|
60
|
+
`npm pack ${quoteShellArg(options.spec)} --ignore-scripts --json --pack-destination ${quoteShellArg(root)}${cacheFlag(options.cacheDir)}${at}`,
|
|
61
|
+
{ timeoutMs: INSTALL_TIMEOUT_MS },
|
|
62
|
+
).catch(() => undefined);
|
|
63
|
+
if (packed === undefined || packed.exitCode !== 0) return undefined;
|
|
64
|
+
const file = packedFileFrom(packed.stdout, root);
|
|
65
|
+
if (file === undefined) return undefined;
|
|
66
|
+
const unpacked = await runShell(shell, `tar -xzf ${quoteShellArg(file)} -C ${quoteShellArg(root)}`, {
|
|
67
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
68
|
+
}).catch(() => undefined);
|
|
69
|
+
if (unpacked === undefined || unpacked.exitCode !== 0) return undefined;
|
|
70
|
+
const dir = path.join(root, 'package');
|
|
71
|
+
if (!fs.existsSync(dir)) return undefined;
|
|
72
|
+
return visit(dir);
|
|
73
|
+
} finally {
|
|
74
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export { splitNpmSpec } from './spec.js';
|
|
4
|
+
export type { NpmRef } from './spec.js';
|
|
5
|
+
import { dshRecordOf, packageManifestOf, packageNameOf } from './local-declaration.js';
|
|
6
|
+
import { pluginAt } from './plugin.js';
|
|
7
|
+
|
|
8
|
+
export type PackageKind = 'runtime-plugin' | 'dsh-bundle' | 'both' | 'neither';
|
|
9
|
+
|
|
10
|
+
export interface PackageVerdict {
|
|
11
|
+
readonly kind: PackageKind;
|
|
12
|
+
readonly pkg?: string;
|
|
13
|
+
readonly pluginName?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function asRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
17
|
+
return typeof value === 'object' && value !== null ? (value as Readonly<Record<string, unknown>>) : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isCommandFile(name: string): boolean {
|
|
21
|
+
if (name.endsWith('.d.ts') || name.endsWith('.test.ts')) return false;
|
|
22
|
+
if (name.endsWith('.d.js')) return false;
|
|
23
|
+
return name.endsWith('.ts') || name.endsWith('.js');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function pluginManifestOf(dir: string, manifestFile: string): Readonly<Record<string, unknown>> | undefined {
|
|
27
|
+
try {
|
|
28
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(dir, manifestFile), 'utf8'));
|
|
29
|
+
if (Array.isArray(parsed)) return undefined;
|
|
30
|
+
return asRecord(parsed);
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function hasCommandFile(dir: string): boolean {
|
|
37
|
+
try {
|
|
38
|
+
return fs.readdirSync(dir).some(isCommandFile);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function dshMarkersOf(raw: unknown): boolean {
|
|
45
|
+
const record = asRecord(raw);
|
|
46
|
+
return record !== undefined && ('bundle' in record || 'workbench' in record);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function verdictOf(dir: string, manifestFile: string): PackageVerdict | undefined {
|
|
50
|
+
const plugin = pluginAt(dir, manifestFile);
|
|
51
|
+
if (packageManifestOf(dir) === undefined && plugin === undefined) return undefined;
|
|
52
|
+
const dsh = dshMarkersOf(dshRecordOf(dir));
|
|
53
|
+
const kind: PackageKind =
|
|
54
|
+
plugin !== undefined && dsh ? 'both' : plugin !== undefined ? 'runtime-plugin' : dsh ? 'dsh-bundle' : 'neither';
|
|
55
|
+
const pkg = packageNameOf(dir);
|
|
56
|
+
return {
|
|
57
|
+
kind,
|
|
58
|
+
...(pkg === undefined ? {} : { pkg }),
|
|
59
|
+
...(plugin?.name === undefined ? {} : { pluginName: plugin.name }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { DshShell } from '../api/dsh.js';
|
|
2
|
+
import { VERSION_CHECK_TTL_MS } from '../config/constants.js';
|
|
3
|
+
import { dshMarkersOf, splitNpmSpec, verdictOf, type PackageVerdict } from './package-kind.js';
|
|
4
|
+
import { withPackageDir } from './package-content.js';
|
|
5
|
+
import { localDirOf } from './local-declaration.js';
|
|
6
|
+
|
|
7
|
+
export type PackageRouteKind = 'runtime-plugin' | 'dsh-bundle' | 'neither';
|
|
8
|
+
|
|
9
|
+
export interface PackageRoute {
|
|
10
|
+
readonly kind: PackageRouteKind;
|
|
11
|
+
readonly pkg?: string;
|
|
12
|
+
readonly pluginName?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function routeOfVerdict(verdict: PackageVerdict, fallback?: string): PackageRoute {
|
|
16
|
+
const pkg = verdict.pkg ?? fallback;
|
|
17
|
+
if (verdict.kind === 'neither') return { kind: 'neither', ...(pkg === undefined ? {} : { pkg }) };
|
|
18
|
+
if (verdict.kind === 'dsh-bundle' || verdict.kind === 'both') return { kind: 'dsh-bundle' };
|
|
19
|
+
return {
|
|
20
|
+
kind: 'runtime-plugin',
|
|
21
|
+
...(pkg === undefined ? {} : { pkg }),
|
|
22
|
+
...(verdict.pluginName === undefined ? {} : { pluginName: verdict.pluginName }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PackageRouter {
|
|
27
|
+
packageRouteOf(options: RouteOptions): Promise<PackageRoute | undefined>;
|
|
28
|
+
reset(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface RouteOptions {
|
|
32
|
+
readonly spec: string;
|
|
33
|
+
readonly shell?: DshShell | undefined;
|
|
34
|
+
readonly registry?: string | undefined;
|
|
35
|
+
readonly force?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PackageRouterOptions {
|
|
39
|
+
readonly manifestFile: string;
|
|
40
|
+
readonly cacheDir?: string | undefined;
|
|
41
|
+
readonly dshLookup?: ((options: { shell: DshShell; name: string; registry: string }) => Promise<{ readable: boolean; raw: unknown }>) | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createPackageRouter(options: PackageRouterOptions): PackageRouter {
|
|
45
|
+
const cache = new Map<string, { at: number; route: PackageRoute | null }>();
|
|
46
|
+
return {
|
|
47
|
+
packageRouteOf: (routeOptions) => routeOf(cache, options, routeOptions),
|
|
48
|
+
reset: () => {
|
|
49
|
+
cache.clear();
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function routeOf(
|
|
55
|
+
cache: Map<string, { at: number; route: PackageRoute | null }>,
|
|
56
|
+
options: PackageRouterOptions,
|
|
57
|
+
routeOptions: RouteOptions,
|
|
58
|
+
): Promise<PackageRoute | undefined> {
|
|
59
|
+
const local = localDirOf(routeOptions.spec);
|
|
60
|
+
if (local !== undefined) {
|
|
61
|
+
const verdict = verdictOf(local, options.manifestFile);
|
|
62
|
+
return verdict === undefined ? undefined : routeOfVerdict(verdict, splitNpmSpec(routeOptions.spec)?.name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ref = splitNpmSpec(routeOptions.spec);
|
|
66
|
+
if (ref === undefined) return undefined;
|
|
67
|
+
const shell = routeOptions.shell;
|
|
68
|
+
const registry = routeOptions.registry;
|
|
69
|
+
if (shell === undefined || registry === undefined) return undefined;
|
|
70
|
+
|
|
71
|
+
const key = `${registry}|${routeOptions.spec}`;
|
|
72
|
+
const hit = cache.get(key);
|
|
73
|
+
if (routeOptions.force !== true && hit !== undefined && Date.now() - hit.at < VERSION_CHECK_TTL_MS) {
|
|
74
|
+
return hit.route ?? undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const found =
|
|
78
|
+
options.dshLookup === undefined
|
|
79
|
+
? { readable: false, raw: undefined }
|
|
80
|
+
: await options.dshLookup({ shell, name: routeOptions.spec, registry });
|
|
81
|
+
let route: PackageRoute | undefined;
|
|
82
|
+
if (found.readable && dshMarkersOf(found.raw)) {
|
|
83
|
+
route = { kind: 'dsh-bundle' };
|
|
84
|
+
} else {
|
|
85
|
+
const verdict = await withPackageDir(
|
|
86
|
+
{ shell, spec: routeOptions.spec, registry, ...(options.cacheDir === undefined ? {} : { cacheDir: options.cacheDir }) },
|
|
87
|
+
(dir) => verdictOf(dir, options.manifestFile),
|
|
88
|
+
);
|
|
89
|
+
route = verdict === undefined ? undefined : routeOfVerdict(verdict, ref.name);
|
|
90
|
+
}
|
|
91
|
+
cache.set(key, { at: Date.now(), route: route ?? null });
|
|
92
|
+
return route;
|
|
93
|
+
}
|