@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.
@@ -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
+ }