@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.
- 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 +451 -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 +12 -1
- package/src/cli/options.js +17 -0
- package/src/extractor/check.js +155 -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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
|
|
7
|
+
import { packageInstallHint, packageResolutionBase } from "./spec.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} PackageManifestResolution
|
|
11
|
+
* @property {string|null} manifestPath
|
|
12
|
+
* @property {string|null} packageRoot
|
|
13
|
+
* @property {string|null} error
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{ ok: boolean, errors: string[] }} ManifestValidation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} packageName
|
|
22
|
+
* @param {string} manifestFile
|
|
23
|
+
* @param {string|null|undefined} rootDir
|
|
24
|
+
* @param {string} packageLabel
|
|
25
|
+
* @returns {PackageManifestResolution}
|
|
26
|
+
*/
|
|
27
|
+
export function resolvePackageManifestPath(packageName, manifestFile, rootDir = process.cwd(), packageLabel = "Package") {
|
|
28
|
+
const requireFromRoot = createRequire(packageResolutionBase(rootDir));
|
|
29
|
+
try {
|
|
30
|
+
const manifestPath = requireFromRoot.resolve(`${packageName}/${manifestFile}`);
|
|
31
|
+
return {
|
|
32
|
+
manifestPath,
|
|
33
|
+
packageRoot: path.dirname(manifestPath),
|
|
34
|
+
error: null
|
|
35
|
+
};
|
|
36
|
+
} catch (manifestError) {
|
|
37
|
+
try {
|
|
38
|
+
const packageJsonPath = requireFromRoot.resolve(`${packageName}/package.json`);
|
|
39
|
+
const packageRoot = path.dirname(packageJsonPath);
|
|
40
|
+
const manifestPath = path.join(packageRoot, manifestFile);
|
|
41
|
+
if (!fs.existsSync(manifestPath)) {
|
|
42
|
+
return {
|
|
43
|
+
manifestPath: null,
|
|
44
|
+
packageRoot,
|
|
45
|
+
error: `${packageLabel} '${packageName}' is missing ${manifestFile}`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
manifestPath,
|
|
50
|
+
packageRoot,
|
|
51
|
+
error: null
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
const detail = manifestError instanceof Error ? manifestError.message : String(manifestError);
|
|
55
|
+
const installHint = packageInstallHint(packageName);
|
|
56
|
+
return {
|
|
57
|
+
manifestPath: null,
|
|
58
|
+
packageRoot: null,
|
|
59
|
+
error: `${packageLabel} '${packageName}' could not be resolved from '${rootDir || process.cwd()}': ${detail}${installHint ? `. ${installHint}` : ""}`
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @template T
|
|
67
|
+
* @param {{
|
|
68
|
+
* packageName: string,
|
|
69
|
+
* rootDir?: string|null,
|
|
70
|
+
* manifestFile: string,
|
|
71
|
+
* packageLabel: string,
|
|
72
|
+
* validateManifest: (manifest: any) => ManifestValidation
|
|
73
|
+
* }} options
|
|
74
|
+
* @returns {{ manifest: T|null, errors: string[], manifestPath: string|null, packageRoot: string|null }}
|
|
75
|
+
*/
|
|
76
|
+
export function loadPackageManifest(options) {
|
|
77
|
+
const resolved = resolvePackageManifestPath(
|
|
78
|
+
options.packageName,
|
|
79
|
+
options.manifestFile,
|
|
80
|
+
options.rootDir || process.cwd(),
|
|
81
|
+
options.packageLabel
|
|
82
|
+
);
|
|
83
|
+
if (!resolved.manifestPath) {
|
|
84
|
+
return {
|
|
85
|
+
manifest: null,
|
|
86
|
+
errors: [resolved.error || `${options.packageLabel} '${options.packageName}' could not be resolved`],
|
|
87
|
+
manifestPath: null,
|
|
88
|
+
packageRoot: resolved.packageRoot
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const manifest = JSON.parse(fs.readFileSync(resolved.manifestPath, "utf8"));
|
|
93
|
+
const validation = options.validateManifest(manifest);
|
|
94
|
+
return {
|
|
95
|
+
manifest: validation.ok ? /** @type {T} */ (manifest) : null,
|
|
96
|
+
errors: validation.errors,
|
|
97
|
+
manifestPath: resolved.manifestPath,
|
|
98
|
+
packageRoot: resolved.packageRoot
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return {
|
|
102
|
+
manifest: null,
|
|
103
|
+
errors: [`${options.packageLabel} '${options.packageName}' manifest could not be read: ${error instanceof Error ? error.message : String(error)}`],
|
|
104
|
+
manifestPath: resolved.manifestPath,
|
|
105
|
+
packageRoot: resolved.packageRoot
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} PackagePolicy
|
|
5
|
+
* @property {string} version
|
|
6
|
+
* @property {string[]} allowedPackageScopes
|
|
7
|
+
* @property {string[]} allowedPackages
|
|
8
|
+
* @property {Record<string, string>} pinnedVersions
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {unknown} value
|
|
13
|
+
* @param {string} fieldName
|
|
14
|
+
* @param {string} policyPath
|
|
15
|
+
* @returns {string[]}
|
|
16
|
+
*/
|
|
17
|
+
export function optionalStringArray(value, fieldName, policyPath) {
|
|
18
|
+
if (value == null) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
if (!Array.isArray(value)) {
|
|
22
|
+
throw new Error(`${policyPath} ${fieldName} must be an array of strings.`);
|
|
23
|
+
}
|
|
24
|
+
return value.map((item) => {
|
|
25
|
+
if (typeof item !== "string" || item.length === 0) {
|
|
26
|
+
throw new Error(`${policyPath} ${fieldName} must contain only non-empty strings.`);
|
|
27
|
+
}
|
|
28
|
+
return item;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {unknown} value
|
|
34
|
+
* @param {string} policyPath
|
|
35
|
+
* @param {string} [pinnedLabel]
|
|
36
|
+
* @returns {Record<string, string>}
|
|
37
|
+
*/
|
|
38
|
+
export function optionalStringRecord(value, policyPath, pinnedLabel = "package ids") {
|
|
39
|
+
if (value == null) {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
43
|
+
throw new Error(`${policyPath} pinnedVersions must be an object of ${pinnedLabel} to versions.`);
|
|
44
|
+
}
|
|
45
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
46
|
+
if (typeof item !== "string" || item.length === 0) {
|
|
47
|
+
throw new Error(`${policyPath} pinnedVersions['${key}'] must be a non-empty string.`);
|
|
48
|
+
}
|
|
49
|
+
return [key, item];
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} packageName
|
|
55
|
+
* @returns {string|null}
|
|
56
|
+
*/
|
|
57
|
+
export function packageScopeFromName(packageName) {
|
|
58
|
+
return packageName.startsWith("@") ? packageName.split("/")[0] || null : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} allowed
|
|
63
|
+
* @param {string|null} scope
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
function packageScopeMatches(allowed, scope) {
|
|
67
|
+
return Boolean(scope && (allowed === scope || allowed === `${scope}/*`));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {PackagePolicy} policy
|
|
72
|
+
* @param {string} packageName
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
export function packageAllowedByPolicy(policy, packageName) {
|
|
76
|
+
if (policy.allowedPackages.includes(packageName)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
const scope = packageScopeFromName(packageName);
|
|
80
|
+
return policy.allowedPackageScopes.some((allowed) => packageScopeMatches(allowed, scope));
|
|
81
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} spec
|
|
8
|
+
* @param {string} cwd
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function isPathSpec(spec, cwd) {
|
|
12
|
+
return spec.startsWith(".") || spec.startsWith("/") || fs.existsSync(path.resolve(cwd, spec));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} spec
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function packageNameFromSpec(spec) {
|
|
20
|
+
if (spec.startsWith("@")) {
|
|
21
|
+
const versionIndex = spec.indexOf("@", 1);
|
|
22
|
+
return versionIndex > 0 ? spec.slice(0, versionIndex) : spec;
|
|
23
|
+
}
|
|
24
|
+
const versionIndex = spec.indexOf("@");
|
|
25
|
+
return versionIndex > 0 ? spec.slice(0, versionIndex) : spec;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string|null|undefined} rootDir
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function packageResolutionBase(rootDir) {
|
|
33
|
+
return path.join(rootDir || process.cwd(), "package.json");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string|null|undefined} packageName
|
|
38
|
+
* @returns {string|null}
|
|
39
|
+
*/
|
|
40
|
+
export function packageInstallCommand(packageName) {
|
|
41
|
+
return packageName ? `npm install -D ${packageName}` : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string|null|undefined} packageName
|
|
46
|
+
* @returns {string|null}
|
|
47
|
+
*/
|
|
48
|
+
export function packageInstallHint(packageName) {
|
|
49
|
+
const command = packageInstallCommand(packageName);
|
|
50
|
+
return command ? `Install it from the project root with: ${command}` : null;
|
|
51
|
+
}
|