@topogram/cli 0.3.79 → 0.3.81
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/package.json +1 -1
- package/src/cli/command-parser.js +2 -0
- package/src/cli/command-parsers/extractor.js +40 -0
- package/src/cli/commands/extractor.js +621 -0
- package/src/cli/commands/import/help.js +3 -1
- package/src/cli/commands/import/workspace.js +8 -2
- package/src/cli/commands/import-runner.js +4 -2
- package/src/cli/dispatcher.js +14 -0
- package/src/cli/help-dispatch.js +11 -0
- package/src/cli/help.js +14 -1
- package/src/cli/options.js +17 -0
- package/src/extractor/check.js +155 -0
- package/src/extractor/first-party.js +93 -0
- package/src/extractor/packages.js +295 -0
- package/src/extractor/registry.js +196 -0
- package/src/extractor-policy.js +249 -0
- package/src/generator/check.js +24 -87
- package/src/generator/registry/index.js +16 -75
- package/src/generator-policy.js +9 -57
- package/src/import/core/registry.d.ts +3 -0
- package/src/import/core/registry.js +82 -8
- package/src/import/core/runner/run.js +2 -0
- package/src/import/core/runner/tracks.js +66 -4
- package/src/import/provenance.js +2 -1
- package/src/package-adapters/adapter.js +64 -0
- package/src/package-adapters/file-map.js +30 -0
- package/src/package-adapters/index.js +27 -0
- package/src/package-adapters/manifest.js +108 -0
- package/src/package-adapters/policy.js +81 -0
- package/src/package-adapters/spec.js +51 -0
- package/src/topogram-config.js +14 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { stableStringify } from "./format.js";
|
|
7
|
+
import {
|
|
8
|
+
optionalStringArray,
|
|
9
|
+
optionalStringRecord,
|
|
10
|
+
packageAllowedByPolicy,
|
|
11
|
+
packageScopeFromName as sharedPackageScopeFromName
|
|
12
|
+
} from "./package-adapters/index.js";
|
|
13
|
+
|
|
14
|
+
export const EXTRACTOR_POLICY_FILE = "topogram.extractor-policy.json";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} ExtractorPolicy
|
|
18
|
+
* @property {string} version
|
|
19
|
+
* @property {string[]} allowedPackageScopes
|
|
20
|
+
* @property {string[]} allowedPackages
|
|
21
|
+
* @property {Record<string, string>} pinnedVersions
|
|
22
|
+
* @property {string[]} enabledPackages
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} PackageExtractorBinding
|
|
27
|
+
* @property {string} packageName
|
|
28
|
+
* @property {string} version
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} ExtractorPolicyInfo
|
|
33
|
+
* @property {string} path
|
|
34
|
+
* @property {ExtractorPolicy|null} policy
|
|
35
|
+
* @property {boolean} exists
|
|
36
|
+
* @property {ExtractorPolicyDiagnostic[]} diagnostics
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} ExtractorPolicyDiagnostic
|
|
41
|
+
* @property {string} code
|
|
42
|
+
* @property {"error"|"warning"} severity
|
|
43
|
+
* @property {string} message
|
|
44
|
+
* @property {string|null} path
|
|
45
|
+
* @property {string|null} suggestedFix
|
|
46
|
+
* @property {string|null} step
|
|
47
|
+
* @property {string|null} [packageName]
|
|
48
|
+
* @property {string|null} [version]
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {Record<string, any>} input
|
|
53
|
+
* @returns {ExtractorPolicyDiagnostic}
|
|
54
|
+
*/
|
|
55
|
+
function extractorPolicyDiagnostic(input) {
|
|
56
|
+
return {
|
|
57
|
+
code: String(input.code || "extractor_policy_failed"),
|
|
58
|
+
severity: input.severity === "warning" ? "warning" : "error",
|
|
59
|
+
message: String(input.message || "Extractor policy check failed."),
|
|
60
|
+
path: typeof input.path === "string" ? input.path : null,
|
|
61
|
+
suggestedFix: typeof input.suggestedFix === "string" ? input.suggestedFix : null,
|
|
62
|
+
step: typeof input.step === "string" ? input.step : null,
|
|
63
|
+
packageName: typeof input.packageName === "string" ? input.packageName : null,
|
|
64
|
+
version: typeof input.version === "string" ? input.version : null
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @returns {ExtractorPolicy}
|
|
70
|
+
*/
|
|
71
|
+
export function defaultExtractorPolicy() {
|
|
72
|
+
return {
|
|
73
|
+
version: "0.1",
|
|
74
|
+
allowedPackageScopes: [],
|
|
75
|
+
allowedPackages: [],
|
|
76
|
+
pinnedVersions: {},
|
|
77
|
+
enabledPackages: []
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {unknown} value
|
|
83
|
+
* @param {string} policyPath
|
|
84
|
+
* @returns {ExtractorPolicy}
|
|
85
|
+
*/
|
|
86
|
+
export function validateExtractorPolicy(value, policyPath) {
|
|
87
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
88
|
+
throw new Error(`${EXTRACTOR_POLICY_FILE} must contain a JSON object.`);
|
|
89
|
+
}
|
|
90
|
+
const raw = /** @type {Record<string, unknown>} */ (value);
|
|
91
|
+
const defaults = defaultExtractorPolicy();
|
|
92
|
+
return {
|
|
93
|
+
version: typeof raw.version === "string" && raw.version ? raw.version : defaults.version,
|
|
94
|
+
allowedPackageScopes: raw.allowedPackageScopes == null
|
|
95
|
+
? defaults.allowedPackageScopes
|
|
96
|
+
: optionalStringArray(raw.allowedPackageScopes, "allowedPackageScopes", policyPath),
|
|
97
|
+
allowedPackages: optionalStringArray(raw.allowedPackages, "allowedPackages", policyPath),
|
|
98
|
+
pinnedVersions: optionalStringRecord(raw.pinnedVersions, policyPath, "package-or-extractor ids"),
|
|
99
|
+
enabledPackages: optionalStringArray(raw.enabledPackages, "enabledPackages", policyPath)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} packageName
|
|
105
|
+
* @returns {string|null}
|
|
106
|
+
*/
|
|
107
|
+
export function packageScopeFromName(packageName) {
|
|
108
|
+
return sharedPackageScopeFromName(packageName);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {ExtractorPolicy} policy
|
|
113
|
+
* @param {string} packageName
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
export function extractorPackageAllowed(policy, packageName) {
|
|
117
|
+
return packageName.startsWith("@topogram/extractor-") || packageAllowedByPolicy(policy, packageName);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} projectRoot
|
|
122
|
+
* @param {string|null|undefined} policyPath
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
function resolvePolicyPath(projectRoot, policyPath) {
|
|
126
|
+
if (policyPath) {
|
|
127
|
+
const resolved = path.resolve(projectRoot, policyPath);
|
|
128
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
129
|
+
return path.join(resolved, EXTRACTOR_POLICY_FILE);
|
|
130
|
+
}
|
|
131
|
+
return resolved;
|
|
132
|
+
}
|
|
133
|
+
return path.join(projectRoot, EXTRACTOR_POLICY_FILE);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {string} projectRoot
|
|
138
|
+
* @param {string|null|undefined} [policyPath]
|
|
139
|
+
* @returns {ExtractorPolicyInfo}
|
|
140
|
+
*/
|
|
141
|
+
export function loadExtractorPolicy(projectRoot, policyPath = null) {
|
|
142
|
+
const resolvedPolicyPath = resolvePolicyPath(projectRoot, policyPath);
|
|
143
|
+
if (!fs.existsSync(resolvedPolicyPath)) {
|
|
144
|
+
return {
|
|
145
|
+
path: resolvedPolicyPath,
|
|
146
|
+
policy: null,
|
|
147
|
+
exists: false,
|
|
148
|
+
diagnostics: []
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
return {
|
|
153
|
+
path: resolvedPolicyPath,
|
|
154
|
+
policy: validateExtractorPolicy(JSON.parse(fs.readFileSync(resolvedPolicyPath, "utf8")), resolvedPolicyPath),
|
|
155
|
+
exists: true,
|
|
156
|
+
diagnostics: []
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
path: resolvedPolicyPath,
|
|
161
|
+
policy: null,
|
|
162
|
+
exists: true,
|
|
163
|
+
diagnostics: [extractorPolicyDiagnostic({
|
|
164
|
+
code: "extractor_policy_invalid",
|
|
165
|
+
message: error instanceof Error ? error.message : String(error),
|
|
166
|
+
path: resolvedPolicyPath,
|
|
167
|
+
suggestedFix: `Fix ${EXTRACTOR_POLICY_FILE} or regenerate it with \`topogram extractor policy init\`.`,
|
|
168
|
+
step: "extractor-policy"
|
|
169
|
+
})]
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {ExtractorPolicyInfo} policyInfo
|
|
176
|
+
* @returns {ExtractorPolicy}
|
|
177
|
+
*/
|
|
178
|
+
export function effectiveExtractorPolicy(policyInfo) {
|
|
179
|
+
return policyInfo.policy || defaultExtractorPolicy();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {string} projectRoot
|
|
184
|
+
* @param {ExtractorPolicy} policy
|
|
185
|
+
* @param {string|null|undefined} [policyPath]
|
|
186
|
+
* @returns {ExtractorPolicy}
|
|
187
|
+
*/
|
|
188
|
+
export function writeExtractorPolicy(projectRoot, policy, policyPath = null) {
|
|
189
|
+
const resolvedPolicyPath = resolvePolicyPath(projectRoot, policyPath);
|
|
190
|
+
fs.writeFileSync(resolvedPolicyPath, `${stableStringify(policy)}\n`, "utf8");
|
|
191
|
+
return policy;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {ExtractorPolicyInfo} policyInfo
|
|
196
|
+
* @param {PackageExtractorBinding[]} bindings
|
|
197
|
+
* @param {string} [step]
|
|
198
|
+
* @returns {ExtractorPolicyDiagnostic[]}
|
|
199
|
+
*/
|
|
200
|
+
export function extractorPolicyDiagnosticsForPackages(policyInfo, bindings, step = "extractor-policy") {
|
|
201
|
+
if (policyInfo.diagnostics.length > 0) {
|
|
202
|
+
return policyInfo.diagnostics;
|
|
203
|
+
}
|
|
204
|
+
const policy = effectiveExtractorPolicy(policyInfo);
|
|
205
|
+
/** @type {ExtractorPolicyDiagnostic[]} */
|
|
206
|
+
const diagnostics = [];
|
|
207
|
+
for (const binding of bindings) {
|
|
208
|
+
if (!extractorPackageAllowed(policy, binding.packageName)) {
|
|
209
|
+
const scope = packageScopeFromName(binding.packageName);
|
|
210
|
+
diagnostics.push(extractorPolicyDiagnostic({
|
|
211
|
+
code: "extractor_package_denied",
|
|
212
|
+
message: `Extractor package '${binding.packageName}' is not allowed by ${EXTRACTOR_POLICY_FILE}.`,
|
|
213
|
+
path: policyInfo.path,
|
|
214
|
+
suggestedFix: `Review '${binding.packageName}', then run \`topogram extractor policy pin ${binding.packageName}@${binding.version}\` or add '${scope || binding.packageName}' to ${EXTRACTOR_POLICY_FILE}.`,
|
|
215
|
+
step,
|
|
216
|
+
packageName: binding.packageName,
|
|
217
|
+
version: binding.version
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
const pinnedVersion = policy.pinnedVersions[binding.packageName] || null;
|
|
221
|
+
if (pinnedVersion && pinnedVersion !== binding.version) {
|
|
222
|
+
diagnostics.push(extractorPolicyDiagnostic({
|
|
223
|
+
code: "extractor_version_mismatch",
|
|
224
|
+
message: `Extractor package '${binding.packageName}' uses version '${binding.version}', but ${EXTRACTOR_POLICY_FILE} pins it to '${pinnedVersion}'.`,
|
|
225
|
+
path: policyInfo.path,
|
|
226
|
+
suggestedFix: `Use extractor version '${pinnedVersion}', or run \`topogram extractor policy pin ${binding.packageName}@${binding.version}\` after review.`,
|
|
227
|
+
step,
|
|
228
|
+
packageName: binding.packageName,
|
|
229
|
+
version: binding.version
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return diagnostics;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* @param {string} spec
|
|
238
|
+
* @returns {{ packageName: string, version: string }}
|
|
239
|
+
*/
|
|
240
|
+
export function parseExtractorPolicyPin(spec) {
|
|
241
|
+
const separator = spec.lastIndexOf("@");
|
|
242
|
+
if (separator <= 0 || separator === spec.length - 1) {
|
|
243
|
+
throw new Error("Extractor policy pin requires a package name and extractor version, for example @topogram/extractor-node-cli@1.");
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
packageName: spec.slice(0, separator),
|
|
247
|
+
version: spec.slice(separator + 1)
|
|
248
|
+
};
|
|
249
|
+
}
|
package/src/generator/check.js
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { createRequire } from "node:module";
|
|
6
5
|
|
|
7
6
|
import {
|
|
8
7
|
loadPackageGeneratorManifest,
|
|
9
8
|
validateGeneratorManifest
|
|
10
9
|
} from "./registry.js";
|
|
10
|
+
import {
|
|
11
|
+
isPathSpec,
|
|
12
|
+
loadInstalledPackageAdapter,
|
|
13
|
+
loadLocalPackageAdapter,
|
|
14
|
+
packageNameFromSpec,
|
|
15
|
+
validateRelativeStringFileMap
|
|
16
|
+
} from "../package-adapters/index.js";
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* @typedef {import("./registry.js").GeneratorManifest} GeneratorManifest
|
|
@@ -28,82 +34,6 @@ import {
|
|
|
28
34
|
* @property {boolean} executesPackageCode
|
|
29
35
|
*/
|
|
30
36
|
|
|
31
|
-
/**
|
|
32
|
-
* @param {string} spec
|
|
33
|
-
* @param {string} cwd
|
|
34
|
-
* @returns {boolean}
|
|
35
|
-
*/
|
|
36
|
-
function isPathSpec(spec, cwd) {
|
|
37
|
-
return spec.startsWith(".") || spec.startsWith("/") || fs.existsSync(path.resolve(cwd, spec));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string} spec
|
|
42
|
-
* @returns {string}
|
|
43
|
-
*/
|
|
44
|
-
function packageNameFromSpec(spec) {
|
|
45
|
-
if (spec.startsWith("@")) {
|
|
46
|
-
const versionIndex = spec.indexOf("@", 1);
|
|
47
|
-
return versionIndex > 0 ? spec.slice(0, versionIndex) : spec;
|
|
48
|
-
}
|
|
49
|
-
const versionIndex = spec.indexOf("@");
|
|
50
|
-
return versionIndex > 0 ? spec.slice(0, versionIndex) : spec;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @param {any} moduleValue
|
|
55
|
-
* @param {string|null|undefined} exportName
|
|
56
|
-
* @returns {any}
|
|
57
|
-
*/
|
|
58
|
-
function selectPackageExport(moduleValue, exportName) {
|
|
59
|
-
if (exportName) {
|
|
60
|
-
return moduleValue?.[exportName] || moduleValue?.default?.[exportName] || null;
|
|
61
|
-
}
|
|
62
|
-
return moduleValue?.default || moduleValue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* @param {string} root
|
|
67
|
-
* @param {GeneratorManifest} manifest
|
|
68
|
-
* @returns {{ adapter: any|null, error: string|null }}
|
|
69
|
-
*/
|
|
70
|
-
function loadLocalAdapter(root, manifest) {
|
|
71
|
-
try {
|
|
72
|
-
const packageJsonPath = path.join(root, "package.json");
|
|
73
|
-
const requireFromPackage = createRequire(packageJsonPath);
|
|
74
|
-
return {
|
|
75
|
-
adapter: selectPackageExport(requireFromPackage(root), manifest.export),
|
|
76
|
-
error: null
|
|
77
|
-
};
|
|
78
|
-
} catch (error) {
|
|
79
|
-
return {
|
|
80
|
-
adapter: null,
|
|
81
|
-
error: `Generator package export could not be loaded from '${root}': ${error instanceof Error ? error.message : String(error)}`
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {string} packageName
|
|
88
|
-
* @param {string} rootDir
|
|
89
|
-
* @param {GeneratorManifest} manifest
|
|
90
|
-
* @returns {{ adapter: any|null, error: string|null }}
|
|
91
|
-
*/
|
|
92
|
-
function loadInstalledAdapter(packageName, rootDir, manifest) {
|
|
93
|
-
try {
|
|
94
|
-
const requireFromRoot = createRequire(path.join(rootDir, "package.json"));
|
|
95
|
-
return {
|
|
96
|
-
adapter: selectPackageExport(requireFromRoot(packageName), manifest.export),
|
|
97
|
-
error: null
|
|
98
|
-
};
|
|
99
|
-
} catch (error) {
|
|
100
|
-
return {
|
|
101
|
-
adapter: null,
|
|
102
|
-
error: `Generator package '${packageName}' export could not be loaded from '${rootDir}': ${error instanceof Error ? error.message : String(error)}`
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
37
|
/**
|
|
108
38
|
* @param {GeneratorManifest} manifest
|
|
109
39
|
* @returns {Record<string, any>}
|
|
@@ -208,14 +138,12 @@ function validateSmokeResult(result) {
|
|
|
208
138
|
if (!result.files || typeof result.files !== "object" || Array.isArray(result.files)) {
|
|
209
139
|
return { ok: false, message: "generate(context) result must include a files object", smoke: null };
|
|
210
140
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return { ok: false, message: `generated file '${filePath}' content must be a string`, smoke: null };
|
|
218
|
-
}
|
|
141
|
+
const fileMapValidation = validateRelativeStringFileMap(result.files, {
|
|
142
|
+
filePathMessage: "generated file paths must be non-empty relative paths",
|
|
143
|
+
contentMessage: (filePath) => `generated file '${filePath}' content must be a string`
|
|
144
|
+
});
|
|
145
|
+
if (!fileMapValidation.ok) {
|
|
146
|
+
return { ok: false, message: fileMapValidation.message, smoke: null };
|
|
219
147
|
}
|
|
220
148
|
return {
|
|
221
149
|
ok: true,
|
|
@@ -304,7 +232,11 @@ export function checkGeneratorPack(sourceSpec, options = {}) {
|
|
|
304
232
|
}
|
|
305
233
|
|
|
306
234
|
if (payload.source === "path") {
|
|
307
|
-
const loaded =
|
|
235
|
+
const loaded = loadLocalPackageAdapter({
|
|
236
|
+
packageRoot: payload.packageRoot || cwd,
|
|
237
|
+
exportName: payload.manifest.export,
|
|
238
|
+
packageLabel: "Generator package"
|
|
239
|
+
});
|
|
308
240
|
adapter = loaded.adapter;
|
|
309
241
|
if (loaded.error) {
|
|
310
242
|
payload.errors.push(loaded.error);
|
|
@@ -312,7 +244,12 @@ export function checkGeneratorPack(sourceSpec, options = {}) {
|
|
|
312
244
|
return payload;
|
|
313
245
|
}
|
|
314
246
|
} else if (payload.packageName) {
|
|
315
|
-
const loaded =
|
|
247
|
+
const loaded = loadInstalledPackageAdapter({
|
|
248
|
+
packageName: payload.packageName,
|
|
249
|
+
rootDir: cwd,
|
|
250
|
+
exportName: payload.manifest.export,
|
|
251
|
+
packageLabel: "Generator package"
|
|
252
|
+
});
|
|
316
253
|
adapter = loaded.adapter;
|
|
317
254
|
if (loaded.error) {
|
|
318
255
|
payload.errors.push(loaded.error);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
/**
|
package/src/generator-policy.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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;
|