@topogram/cli 0.3.79 → 0.3.80

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,8 +1,11 @@
1
1
  // @ts-check
2
2
 
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { createRequire } from "node:module";
3
+ import {
4
+ loadPackageManifest,
5
+ packageInstallCommand,
6
+ packageInstallHint,
7
+ resolvePackageManifestPath
8
+ } from "../../package-adapters/index.js";
6
9
  import { UI_GENERATOR_RENDERED_COMPONENT_PATTERNS } from "../../ui/taxonomy.js";
7
10
 
8
11
  /**
@@ -208,20 +211,12 @@ export function getGeneratorManifest(generatorId) {
208
211
  return GENERATOR_BY_ID.get(generatorId) || null;
209
212
  }
210
213
 
211
- /**
212
- * @param {string|null|undefined} rootDir
213
- * @returns {string}
214
- */
215
- function packageResolutionBase(rootDir) {
216
- return path.join(rootDir || process.cwd(), "package.json");
217
- }
218
-
219
214
  /**
220
215
  * @param {string|null|undefined} packageName
221
216
  * @returns {string|null}
222
217
  */
223
218
  export function packageGeneratorInstallCommand(packageName) {
224
- return packageName ? `npm install -D ${packageName}` : null;
219
+ return packageInstallCommand(packageName);
225
220
  }
226
221
 
227
222
  /**
@@ -229,8 +224,7 @@ export function packageGeneratorInstallCommand(packageName) {
229
224
  * @returns {string|null}
230
225
  */
231
226
  export function packageGeneratorInstallHint(packageName) {
232
- const command = packageGeneratorInstallCommand(packageName);
233
- return command ? `Install it from the project root with: ${command}` : null;
227
+ return packageInstallHint(packageName);
234
228
  }
235
229
 
236
230
  /**
@@ -239,41 +233,7 @@ export function packageGeneratorInstallHint(packageName) {
239
233
  * @returns {{ manifestPath: string|null, packageRoot: string|null, error: string|null }}
240
234
  */
241
235
  export function resolvePackageGeneratorManifestPath(packageName, rootDir = process.cwd()) {
242
- const requireFromRoot = createRequire(packageResolutionBase(rootDir));
243
- try {
244
- const manifestPath = requireFromRoot.resolve(`${packageName}/topogram-generator.json`);
245
- return {
246
- manifestPath,
247
- packageRoot: path.dirname(manifestPath),
248
- error: null
249
- };
250
- } catch (manifestError) {
251
- try {
252
- const packageJsonPath = requireFromRoot.resolve(`${packageName}/package.json`);
253
- const packageRoot = path.dirname(packageJsonPath);
254
- const manifestPath = path.join(packageRoot, "topogram-generator.json");
255
- if (!fs.existsSync(manifestPath)) {
256
- return {
257
- manifestPath: null,
258
- packageRoot,
259
- error: `Generator package '${packageName}' is missing topogram-generator.json`
260
- };
261
- }
262
- return {
263
- manifestPath,
264
- packageRoot,
265
- error: null
266
- };
267
- } catch {
268
- const detail = manifestError instanceof Error ? manifestError.message : String(manifestError);
269
- const installHint = packageGeneratorInstallHint(packageName);
270
- return {
271
- manifestPath: null,
272
- packageRoot: null,
273
- error: `Generator package '${packageName}' could not be resolved from '${rootDir || process.cwd()}': ${detail}${installHint ? `. ${installHint}` : ""}`
274
- };
275
- }
276
- }
236
+ return resolvePackageManifestPath(packageName, "topogram-generator.json", rootDir, "Generator package");
277
237
  }
278
238
 
279
239
  /**
@@ -282,32 +242,13 @@ export function resolvePackageGeneratorManifestPath(packageName, rootDir = proce
282
242
  * @returns {{ manifest: GeneratorManifest|null, errors: string[], manifestPath: string|null, packageRoot: string|null }}
283
243
  */
284
244
  export function loadPackageGeneratorManifest(packageName, rootDir = process.cwd()) {
285
- const resolved = resolvePackageGeneratorManifestPath(packageName, rootDir);
286
- if (!resolved.manifestPath) {
287
- return {
288
- manifest: null,
289
- errors: [resolved.error || `Generator package '${packageName}' could not be resolved`],
290
- manifestPath: null,
291
- packageRoot: resolved.packageRoot
292
- };
293
- }
294
- try {
295
- const manifest = JSON.parse(fs.readFileSync(resolved.manifestPath, "utf8"));
296
- const validation = validateGeneratorManifest(manifest);
297
- return {
298
- manifest: validation.ok ? manifest : null,
299
- errors: validation.errors,
300
- manifestPath: resolved.manifestPath,
301
- packageRoot: resolved.packageRoot
302
- };
303
- } catch (error) {
304
- return {
305
- manifest: null,
306
- errors: [`Generator package '${packageName}' manifest could not be read: ${error instanceof Error ? error.message : String(error)}`],
307
- manifestPath: resolved.manifestPath,
308
- packageRoot: resolved.packageRoot
309
- };
310
- }
245
+ return loadPackageManifest({
246
+ packageName,
247
+ rootDir,
248
+ manifestFile: "topogram-generator.json",
249
+ packageLabel: "Generator package",
250
+ validateManifest: validateGeneratorManifest
251
+ });
311
252
  }
312
253
 
313
254
  /**
@@ -4,6 +4,12 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
 
6
6
  import { stableStringify } from "./format.js";
7
+ import {
8
+ optionalStringArray,
9
+ optionalStringRecord,
10
+ packageAllowedByPolicy,
11
+ packageScopeFromName as sharedPackageScopeFromName
12
+ } from "./package-adapters/index.js";
7
13
 
8
14
  export const GENERATOR_POLICY_FILE = "topogram.generator-policy.json";
9
15
 
@@ -66,47 +72,6 @@ function generatorPolicyDiagnostic(input) {
66
72
  };
67
73
  }
68
74
 
69
- /**
70
- * @param {unknown} value
71
- * @param {string} fieldName
72
- * @param {string} policyPath
73
- * @returns {string[]}
74
- */
75
- function optionalStringArray(value, fieldName, policyPath) {
76
- if (value == null) {
77
- return [];
78
- }
79
- if (!Array.isArray(value)) {
80
- throw new Error(`${policyPath} ${fieldName} must be an array of strings.`);
81
- }
82
- return value.map((item) => {
83
- if (typeof item !== "string" || item.length === 0) {
84
- throw new Error(`${policyPath} ${fieldName} must contain only non-empty strings.`);
85
- }
86
- return item;
87
- });
88
- }
89
-
90
- /**
91
- * @param {unknown} value
92
- * @param {string} policyPath
93
- * @returns {Record<string, string>}
94
- */
95
- function optionalStringRecord(value, policyPath) {
96
- if (value == null) {
97
- return {};
98
- }
99
- if (typeof value !== "object" || Array.isArray(value)) {
100
- throw new Error(`${policyPath} pinnedVersions must be an object of package-or-generator ids to versions.`);
101
- }
102
- return Object.fromEntries(Object.entries(value).map(([key, item]) => {
103
- if (typeof item !== "string" || item.length === 0) {
104
- throw new Error(`${policyPath} pinnedVersions['${key}'] must be a non-empty string.`);
105
- }
106
- return [key, item];
107
- }));
108
- }
109
-
110
75
  /**
111
76
  * @returns {GeneratorPolicy}
112
77
  */
@@ -136,7 +101,7 @@ export function validateGeneratorPolicy(value, policyPath) {
136
101
  ? defaults.allowedPackageScopes
137
102
  : optionalStringArray(raw.allowedPackageScopes, "allowedPackageScopes", policyPath),
138
103
  allowedPackages: optionalStringArray(raw.allowedPackages, "allowedPackages", policyPath),
139
- pinnedVersions: optionalStringRecord(raw.pinnedVersions, policyPath)
104
+ pinnedVersions: optionalStringRecord(raw.pinnedVersions, policyPath, "package-or-generator ids")
140
105
  };
141
106
  }
142
107
 
@@ -145,16 +110,7 @@ export function validateGeneratorPolicy(value, policyPath) {
145
110
  * @returns {string|null}
146
111
  */
147
112
  export function packageScopeFromName(packageName) {
148
- return packageName.startsWith("@") ? packageName.split("/")[0] || null : null;
149
- }
150
-
151
- /**
152
- * @param {string} allowed
153
- * @param {string|null} scope
154
- * @returns {boolean}
155
- */
156
- function packageScopeMatches(allowed, scope) {
157
- return Boolean(scope && (allowed === scope || allowed === `${scope}/*`));
113
+ return sharedPackageScopeFromName(packageName);
158
114
  }
159
115
 
160
116
  /**
@@ -163,11 +119,7 @@ function packageScopeMatches(allowed, scope) {
163
119
  * @returns {boolean}
164
120
  */
165
121
  export function generatorPackageAllowed(policy, packageName) {
166
- if (policy.allowedPackages.includes(packageName)) {
167
- return true;
168
- }
169
- const scope = packageScopeFromName(packageName);
170
- return policy.allowedPackageScopes.some((allowed) => packageScopeMatches(allowed, scope));
122
+ return packageAllowedByPolicy(policy, packageName);
171
123
  }
172
124
 
173
125
  /**
@@ -1,4 +1,7 @@
1
1
  export const extractorRegistry: Record<string, any[]>;
2
2
  export const enricherRegistry: Record<string, any[]>;
3
+ export const BUILTIN_EXTRACTOR_PACKS: any[];
3
4
  export function getExtractorsForTrack(...args: any[]): any[];
4
5
  export function getEnrichersForTrack(...args: any[]): any[];
6
+ export function getBundledExtractorPack(...args: any[]): any;
7
+ export function getBundledExtractorById(...args: any[]): any;
@@ -57,14 +57,80 @@ import { railsControllerEnricher } from "../enrichers/rails-controllers.js";
57
57
  import { workflowTargetStateEnricher } from "../enrichers/workflow-target-state.js";
58
58
  import { docLinkingEnricher } from "../enrichers/doc-linking.js";
59
59
 
60
- export const extractorRegistry = {
61
- db: [prismaExtractor, djangoModelsExtractor, efCoreExtractor, roomExtractor, swiftDataExtractor, dotnetModelsExtractor, flutterEntitiesExtractor, reactNativeEntitiesExtractor, railsSchemaExtractor, liquibaseExtractor, myBatisXmlExtractor, jpaExtractor, drizzleExtractor, sqlExtractor, snapshotExtractor],
62
- api: [openApiExtractor, openApiCodeExtractor, graphQlSdlExtractor, graphQlCodeFirstExtractor, trpcExtractor, aspNetCoreExtractor, retrofitExtractor, swiftWebApiExtractor, flutterDioExtractor, reactNativeRepositoryExtractor, fastifyExtractor, expressExtractor, djangoRoutesExtractor, railsRoutesExtractor, micronautExtractor, jaxRsExtractor, springWebExtractor, nextRouteExtractor, genericRouteFallbackExtractor, nextServerActionExtractor, nextAuthExtractor],
63
- ui: [nextAppRouterUiExtractor, nextPagesRouterUiExtractor, androidComposeUiExtractor, blazorUiExtractor, razorPagesUiExtractor, swiftUiExtractor, uiKitExtractor, mauiXamlUiExtractor, flutterScreensUiExtractor, reactNativeScreensExtractor, reactRouterUiExtractor, svelteKitUiExtractor, backendOnlyUiExtractor],
64
- cli: [genericCliExtractor],
65
- workflows: [genericWorkflowExtractor],
66
- verification: [genericVerificationExtractor]
67
- };
60
+ function extractorPack(id, tracks, extractors, candidateKinds, stack = {}, capabilities = {}) {
61
+ return {
62
+ manifest: {
63
+ id,
64
+ version: "1",
65
+ tracks,
66
+ source: "bundled",
67
+ extractors: extractors.map((extractor) => extractor.id),
68
+ stack,
69
+ capabilities,
70
+ candidateKinds,
71
+ evidenceTypes: ["runtime_source", "parser_config", "docs", "tests", "fixtures"]
72
+ },
73
+ extractors
74
+ };
75
+ }
76
+
77
+ export const BUILTIN_EXTRACTOR_PACKS = [
78
+ extractorPack(
79
+ "topogram/db-extractors",
80
+ ["db"],
81
+ [prismaExtractor, djangoModelsExtractor, efCoreExtractor, roomExtractor, swiftDataExtractor, dotnetModelsExtractor, flutterEntitiesExtractor, reactNativeEntitiesExtractor, railsSchemaExtractor, liquibaseExtractor, myBatisXmlExtractor, jpaExtractor, drizzleExtractor, sqlExtractor, snapshotExtractor],
82
+ ["entity", "enum", "relation", "index", "maintained_db_migration_seam"],
83
+ { domain: "database" },
84
+ { schema: true, migrations: true, maintainedSeams: true }
85
+ ),
86
+ extractorPack(
87
+ "topogram/api-extractors",
88
+ ["api"],
89
+ [openApiExtractor, openApiCodeExtractor, graphQlSdlExtractor, graphQlCodeFirstExtractor, trpcExtractor, aspNetCoreExtractor, retrofitExtractor, swiftWebApiExtractor, flutterDioExtractor, reactNativeRepositoryExtractor, fastifyExtractor, expressExtractor, djangoRoutesExtractor, railsRoutesExtractor, micronautExtractor, jaxRsExtractor, springWebExtractor, nextRouteExtractor, genericRouteFallbackExtractor, nextServerActionExtractor, nextAuthExtractor],
90
+ ["capability", "route", "stack"],
91
+ { domain: "api" },
92
+ { routes: true, openapi: true, graphql: true }
93
+ ),
94
+ extractorPack(
95
+ "topogram/ui-extractors",
96
+ ["ui"],
97
+ [nextAppRouterUiExtractor, nextPagesRouterUiExtractor, androidComposeUiExtractor, blazorUiExtractor, razorPagesUiExtractor, swiftUiExtractor, uiKitExtractor, mauiXamlUiExtractor, flutterScreensUiExtractor, reactNativeScreensExtractor, reactRouterUiExtractor, svelteKitUiExtractor, backendOnlyUiExtractor],
98
+ ["screen", "route", "action", "flow", "widget", "shape", "stack"],
99
+ { domain: "ui" },
100
+ { screens: true, widgets: true, flows: true }
101
+ ),
102
+ extractorPack(
103
+ "topogram/cli-extractors",
104
+ ["cli"],
105
+ [genericCliExtractor],
106
+ ["command", "capability", "cli_surface"],
107
+ { domain: "cli" },
108
+ { commands: true, options: true, effects: true }
109
+ ),
110
+ extractorPack(
111
+ "topogram/workflow-extractors",
112
+ ["workflows"],
113
+ [genericWorkflowExtractor],
114
+ ["workflow", "workflow_state", "workflow_transition"],
115
+ { domain: "workflow" },
116
+ { workflows: true }
117
+ ),
118
+ extractorPack(
119
+ "topogram/verification-extractors",
120
+ ["verification"],
121
+ [genericVerificationExtractor],
122
+ ["verification", "scenario", "framework", "script"],
123
+ { domain: "verification" },
124
+ { verifications: true }
125
+ )
126
+ ];
127
+
128
+ export const extractorRegistry = Object.fromEntries(
129
+ ["db", "api", "ui", "cli", "workflows", "verification"].map((track) => [
130
+ track,
131
+ BUILTIN_EXTRACTOR_PACKS.flatMap((pack) => pack.extractors).filter((extractor) => extractor.track === track)
132
+ ])
133
+ );
68
134
 
69
135
  export const enricherRegistry = {
70
136
  db: [railsModelEnricher],
@@ -82,3 +148,11 @@ export function getExtractorsForTrack(track) {
82
148
  export function getEnrichersForTrack(track) {
83
149
  return enricherRegistry[track] || [];
84
150
  }
151
+
152
+ export function getBundledExtractorPack(id) {
153
+ return BUILTIN_EXTRACTOR_PACKS.find((pack) => pack.manifest.id === id) || null;
154
+ }
155
+
156
+ export function getBundledExtractorById(id) {
157
+ return BUILTIN_EXTRACTOR_PACKS.flatMap((pack) => pack.extractors).find((extractor) => extractor.id === id) || null;
158
+ }
@@ -6,6 +6,7 @@ import { parseImportTracks } from "./options.js";
6
6
  import { appReportMarkdown, reportMarkdown } from "./reports.js";
7
7
  import { runTrack } from "./tracks.js";
8
8
  import { draftUiProjectionFiles } from "./ui-drafts.js";
9
+ import { packageExtractorsForContext } from "../../../extractor/packages.js";
9
10
 
10
11
  /**
11
12
  * @param {string} inputPath
@@ -64,6 +65,7 @@ export function runImportApp(inputPath, options = {}) {
64
65
  tracks,
65
66
  findings_count: Object.values(findings).reduce((total, entries) => total + entries.length, 0),
66
67
  extractor_detections: Object.fromEntries(Object.entries(resultsByTrack).map(([track, result]) => [track, result.extractor_detections])),
68
+ package_extractors: packageExtractorsForContext(context).provenance,
67
69
  candidates
68
70
  };
69
71
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { getEnrichersForTrack, getExtractorsForTrack } from "../registry.js";
4
4
  import { normalizeCandidatesForTrack } from "./candidates.js";
5
+ import { packageExtractorsForContext } from "../../../extractor/packages.js";
5
6
 
6
7
  /**
7
8
  * @param {any} context
@@ -10,7 +11,12 @@ import { normalizeCandidatesForTrack } from "./candidates.js";
10
11
  */
11
12
  function sortExtractors(context, extractors) {
12
13
  return extractors
13
- .map((extractor) => ({ extractor, detection: extractor.detect(context) || { score: 0, reasons: [] } }))
14
+ .map((extractor) => {
15
+ const extractorContext = extractor.source === "package" && typeof extractor.packageContext === "function"
16
+ ? extractor.packageContext(context)
17
+ : context;
18
+ return { extractor, detection: extractor.detect(extractorContext) || { score: 0, reasons: [] } };
19
+ })
14
20
  .filter((entry) => entry.detection.score > 0)
15
21
  .sort((a, b) => b.detection.score - a.detection.score || a.extractor.id.localeCompare(b.extractor.id));
16
22
  }
@@ -20,7 +26,7 @@ function sortExtractors(context, extractors) {
20
26
  * @param {Array<{ extractor: any, detection: any }>} detections
21
27
  * @returns {Array<{ extractor: any, detection: any }>}
22
28
  */
23
- function selectDetectionsForTrack(track, detections) {
29
+ function selectBundledDetectionsForTrack(track, detections) {
24
30
  if (track === "db") {
25
31
  const prisma = detections.find((entry) => entry.extractor.id === "db.prisma");
26
32
  if (prisma) return [prisma];
@@ -90,6 +96,20 @@ function selectDetectionsForTrack(track, detections) {
90
96
  return detections;
91
97
  }
92
98
 
99
+ /**
100
+ * @param {string} track
101
+ * @param {Array<{ extractor: any, detection: any }>} detections
102
+ * @returns {Array<{ extractor: any, detection: any }>}
103
+ */
104
+ function selectDetectionsForTrack(track, detections) {
105
+ const packageDetections = detections.filter((entry) => entry.extractor.source === "package");
106
+ const bundledDetections = selectBundledDetectionsForTrack(
107
+ track,
108
+ detections.filter((entry) => entry.extractor.source !== "package")
109
+ );
110
+ return [...bundledDetections, ...packageDetections];
111
+ }
112
+
93
113
  /**
94
114
  * @param {string} track
95
115
  * @returns {any}
@@ -113,6 +133,35 @@ function initialCandidatesForTrack(track) {
113
133
  return { workflows: [], workflow_states: [], workflow_transitions: [] };
114
134
  }
115
135
 
136
+ /**
137
+ * @param {any} extractor
138
+ * @param {any} result
139
+ * @returns {void}
140
+ */
141
+ function assertExtractorResultShape(extractor, result) {
142
+ const label = extractor?.id || "unknown";
143
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
144
+ throw new Error(`Extractor '${label}' extract(context) must return an object.`);
145
+ }
146
+ if (result.findings != null && !Array.isArray(result.findings)) {
147
+ throw new Error(`Extractor '${label}' extract(context) findings must be an array when present.`);
148
+ }
149
+ if (result.diagnostics != null && !Array.isArray(result.diagnostics)) {
150
+ throw new Error(`Extractor '${label}' extract(context) diagnostics must be an array when present.`);
151
+ }
152
+ if (result.candidates == null) {
153
+ result.candidates = {};
154
+ }
155
+ if (!result.candidates || typeof result.candidates !== "object" || Array.isArray(result.candidates)) {
156
+ throw new Error(`Extractor '${label}' extract(context) candidates must be an object.`);
157
+ }
158
+ for (const [key, value] of Object.entries(result.candidates)) {
159
+ if (!Array.isArray(value)) {
160
+ throw new Error(`Extractor '${label}' extract(context) candidates.${key} must be an array.`);
161
+ }
162
+ }
163
+ }
164
+
116
165
  /**
117
166
  * @param {any} context
118
167
  * @param {string} track
@@ -121,9 +170,22 @@ function initialCandidatesForTrack(track) {
121
170
  export function runTrack(context, track) {
122
171
  const findings = [];
123
172
  const rawCandidates = initialCandidatesForTrack(track);
173
+ const packageState = packageExtractorsForContext(context);
174
+ const packageErrors = packageState.diagnostics.filter((diagnostic) => diagnostic.severity !== "warning");
175
+ if (packageErrors.length > 0) {
176
+ throw new Error(packageErrors.map((diagnostic) => diagnostic.message || String(diagnostic)).join("\n"));
177
+ }
178
+ const extractors = [
179
+ ...getExtractorsForTrack(track),
180
+ ...packageState.extractors.filter((extractor) => extractor.track === track)
181
+ ];
124
182
 
125
- for (const { extractor, detection } of selectDetectionsForTrack(track, sortExtractors(context, getExtractorsForTrack(track)))) {
126
- const result = extractor.extract(context) || { findings: [], candidates: {} };
183
+ for (const { extractor, detection } of selectDetectionsForTrack(track, sortExtractors(context, extractors))) {
184
+ const extractorContext = extractor.source === "package" && typeof extractor.packageContext === "function"
185
+ ? extractor.packageContext(context)
186
+ : context;
187
+ const result = extractor.extract(extractorContext) || { findings: [], candidates: {} };
188
+ assertExtractorResultShape(extractor, result);
127
189
  findings.push({
128
190
  extractor: extractor.id,
129
191
  detection,
@@ -53,7 +53,8 @@ export function writeTopogramImportRecord(projectRoot, input) {
53
53
  extract: {
54
54
  tracks: input.tracks || [],
55
55
  findingsCount: input.findingsCount || 0,
56
- candidateCounts: input.candidateCounts || {}
56
+ candidateCounts: input.candidateCounts || {},
57
+ extractorPackages: input.extractorPackages || []
57
58
  },
58
59
  ownership: {
59
60
  extractedArtifacts: "project-owned",
@@ -0,0 +1,64 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+ import { createRequire } from "node:module";
5
+
6
+ /**
7
+ * @param {any} moduleValue
8
+ * @param {string|null|undefined} exportName
9
+ * @returns {any}
10
+ */
11
+ export function selectPackageExport(moduleValue, exportName) {
12
+ if (exportName) {
13
+ return moduleValue?.[exportName] || moduleValue?.default?.[exportName] || null;
14
+ }
15
+ return moduleValue?.default || moduleValue;
16
+ }
17
+
18
+ /**
19
+ * @param {{
20
+ * packageRoot: string,
21
+ * exportName?: string|null,
22
+ * packageLabel: string
23
+ * }} options
24
+ * @returns {{ adapter: any|null, error: string|null }}
25
+ */
26
+ export function loadLocalPackageAdapter(options) {
27
+ try {
28
+ const packageJsonPath = path.join(options.packageRoot, "package.json");
29
+ const requireFromPackage = createRequire(packageJsonPath);
30
+ return {
31
+ adapter: selectPackageExport(requireFromPackage(options.packageRoot), options.exportName),
32
+ error: null
33
+ };
34
+ } catch (error) {
35
+ return {
36
+ adapter: null,
37
+ error: `${options.packageLabel} export could not be loaded from '${options.packageRoot}': ${error instanceof Error ? error.message : String(error)}`
38
+ };
39
+ }
40
+ }
41
+
42
+ /**
43
+ * @param {{
44
+ * packageName: string,
45
+ * rootDir: string,
46
+ * exportName?: string|null,
47
+ * packageLabel: string
48
+ * }} options
49
+ * @returns {{ adapter: any|null, error: string|null }}
50
+ */
51
+ export function loadInstalledPackageAdapter(options) {
52
+ try {
53
+ const requireFromRoot = createRequire(path.join(options.rootDir, "package.json"));
54
+ return {
55
+ adapter: selectPackageExport(requireFromRoot(options.packageName), options.exportName),
56
+ error: null
57
+ };
58
+ } catch (error) {
59
+ return {
60
+ adapter: null,
61
+ error: `${options.packageLabel} '${options.packageName}' export could not be loaded from '${options.rootDir}': ${error instanceof Error ? error.message : String(error)}`
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,30 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * @param {any} files
7
+ * @param {{ filePathMessage: string, contentMessage: (filePath: string) => string }} messages
8
+ * @returns {{ ok: boolean, message: string }}
9
+ */
10
+ export function validateRelativeStringFileMap(files, messages) {
11
+ if (!files || typeof files !== "object" || Array.isArray(files)) {
12
+ return { ok: false, message: "files must be an object" };
13
+ }
14
+ for (const [filePath, content] of Object.entries(files)) {
15
+ const normalizedPath = typeof filePath === "string" ? path.normalize(filePath) : "";
16
+ if (
17
+ typeof filePath !== "string" ||
18
+ filePath.length === 0 ||
19
+ path.isAbsolute(filePath) ||
20
+ normalizedPath === ".." ||
21
+ normalizedPath.startsWith(`..${path.sep}`)
22
+ ) {
23
+ return { ok: false, message: messages.filePathMessage };
24
+ }
25
+ if (typeof content !== "string") {
26
+ return { ok: false, message: messages.contentMessage(filePath) };
27
+ }
28
+ }
29
+ return { ok: true, message: "files are valid" };
30
+ }
@@ -0,0 +1,27 @@
1
+ // @ts-check
2
+
3
+ export {
4
+ loadInstalledPackageAdapter,
5
+ loadLocalPackageAdapter,
6
+ selectPackageExport
7
+ } from "./adapter.js";
8
+ export {
9
+ loadPackageManifest,
10
+ resolvePackageManifestPath
11
+ } from "./manifest.js";
12
+ export {
13
+ isPathSpec,
14
+ packageInstallCommand,
15
+ packageInstallHint,
16
+ packageNameFromSpec,
17
+ packageResolutionBase
18
+ } from "./spec.js";
19
+ export {
20
+ optionalStringArray,
21
+ optionalStringRecord,
22
+ packageAllowedByPolicy,
23
+ packageScopeFromName
24
+ } from "./policy.js";
25
+ export {
26
+ validateRelativeStringFileMap
27
+ } from "./file-map.js";