@saasak/tool-env 1.0.2 → 1.2.0
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/README.md +25 -4
- package/bin/index.js +218 -103
- package/package.json +10 -2
- package/src/core.js +491 -0
- package/src/index.d.ts +82 -0
- package/src/index.js +42 -0
- package/src/utils-crypto.js +25 -6
- package/src/utils-env.js +74 -17
- package/src/utils-pkg.js +72 -55
package/src/utils-env.js
CHANGED
|
@@ -1,31 +1,71 @@
|
|
|
1
|
-
import { decrypt, isEncrypted } from
|
|
1
|
+
import { decrypt, isEncrypted } from "./utils-crypto.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Environment-specific values for a single variable.
|
|
5
|
+
* Keys are environment names (e.g. 'dev', 'production') or '@@' for the fallback.
|
|
6
|
+
* @typedef {Object.<string, string>} EnvVariableValue
|
|
7
|
+
*/
|
|
6
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object.<string, EnvVariableValue>} EnvVariables
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object.<string, Object.<string, EnvVariableValue>>} EnvOverrides
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} EnvConfig
|
|
19
|
+
* @property {string[]} [envs] - List of allowed environment names
|
|
20
|
+
* @property {EnvVariables} variables - Environment variables by name
|
|
21
|
+
* @property {EnvOverrides} [overrides] - Package-specific overrides
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object.<string, string>} EnvRecord
|
|
26
|
+
* Key-value pairs of environment variable name to value
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object.<string, EnvRecord>} AllEnvs
|
|
31
|
+
* Environment variables grouped by package name
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build environment variables for all packages
|
|
36
|
+
* @param {string} secret - Secret for decrypting encrypted values
|
|
37
|
+
* @param {string} target - Target environment (e.g. 'dev', 'production')
|
|
38
|
+
* @param {EnvConfig} env - The env.json configuration object
|
|
39
|
+
* @param {string[]} names - List of package names to build env for
|
|
40
|
+
* @returns {AllEnvs} - Environment variables grouped by package name
|
|
41
|
+
*/
|
|
7
42
|
export function buildEnv(secret, target, env, names) {
|
|
8
43
|
const variablesForAllTargets = env.variables;
|
|
9
44
|
const overrides = env.overrides || {};
|
|
10
45
|
|
|
11
46
|
// For each variable, extract the value for the target env
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
47
|
+
/** @type {EnvRecord} */
|
|
48
|
+
const variables = Object.entries(variablesForAllTargets).reduce(
|
|
49
|
+
(acc, entry) => {
|
|
50
|
+
const [key, value] = entry;
|
|
51
|
+
return {
|
|
52
|
+
...acc,
|
|
53
|
+
[key]: extractValue(secret, value[target] || value["@@"] || ""),
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
{},
|
|
57
|
+
);
|
|
19
58
|
|
|
20
59
|
// For each package, get all variables
|
|
21
60
|
// compute the overrides and merge them
|
|
22
61
|
// allVars is an object with the package name as key
|
|
23
62
|
// and the variables as value like so
|
|
24
63
|
// => { [name]: { [variable]: value } }
|
|
64
|
+
/** @type {AllEnvs} */
|
|
25
65
|
const allVars = names.reduce((acc, name) => {
|
|
26
66
|
acc[name] = {
|
|
27
67
|
...variables,
|
|
28
|
-
...parseOverrides(secret, target, overrides[name], variables)
|
|
68
|
+
...parseOverrides(secret, target, overrides[name], variables),
|
|
29
69
|
};
|
|
30
70
|
|
|
31
71
|
return acc;
|
|
@@ -34,25 +74,42 @@ export function buildEnv(secret, target, env, names) {
|
|
|
34
74
|
return allVars;
|
|
35
75
|
}
|
|
36
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Parse package-specific overrides for a target environment
|
|
79
|
+
* @param {string} secret - Secret for decrypting encrypted values
|
|
80
|
+
* @param {string} target - Target environment
|
|
81
|
+
* @param {Object.<string, EnvVariableValue>} overrides - Package overrides (may be undefined)
|
|
82
|
+
* @param {EnvRecord} vars - Base variables for reference substitution
|
|
83
|
+
* @returns {EnvRecord} - Parsed override values
|
|
84
|
+
*/
|
|
37
85
|
function parseOverrides(secret, target, overrides, vars) {
|
|
38
|
-
if (!secret) return {};
|
|
39
86
|
if (!overrides) return {};
|
|
40
87
|
|
|
41
88
|
return Object.entries(overrides).reduce((acc, entry) => {
|
|
42
89
|
const [key, val] = entry;
|
|
43
|
-
const value = extractValue(secret, val[target] || val[
|
|
90
|
+
const value = extractValue(secret, val[target] || val["@@"] || "");
|
|
44
91
|
|
|
45
92
|
if (value === null) return acc;
|
|
46
93
|
|
|
47
94
|
acc[key] = Array.isArray(value)
|
|
48
|
-
? value
|
|
49
|
-
|
|
95
|
+
? value
|
|
96
|
+
.map((v) =>
|
|
97
|
+
Object.prototype.hasOwnProperty.call(vars, v) ? vars[v] : v,
|
|
98
|
+
)
|
|
99
|
+
.join("")
|
|
100
|
+
: value;
|
|
50
101
|
return acc;
|
|
51
102
|
}, {});
|
|
52
103
|
}
|
|
53
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Extract and decrypt a value if needed
|
|
107
|
+
* @param {string} secret - Secret for decryption
|
|
108
|
+
* @param {string} value - Value to extract (may be encrypted)
|
|
109
|
+
* @returns {string} - Decrypted/plain value, or empty string if no secret
|
|
110
|
+
*/
|
|
54
111
|
function extractValue(secret, value) {
|
|
55
112
|
if (!isEncrypted(value)) return value;
|
|
56
|
-
if (!secret) return
|
|
113
|
+
if (!secret) return "";
|
|
57
114
|
return decrypt(value, secret);
|
|
58
115
|
}
|
package/src/utils-pkg.js
CHANGED
|
@@ -1,68 +1,85 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} PackageInfo
|
|
6
|
+
* @property {string} name - Package name (e.g. '@scope/package-name')
|
|
7
|
+
* @property {string} dir - Absolute path to the package directory
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Try to read and parse a package.json, returning PackageInfo if valid and matches scope
|
|
12
|
+
* @param {string} dir - Directory containing package.json
|
|
13
|
+
* @param {string|null} scope - Package scope to filter by
|
|
14
|
+
* @returns {PackageInfo|null}
|
|
15
|
+
*/
|
|
16
|
+
function readPackageInfo(dir, scope) {
|
|
17
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
21
|
+
|
|
22
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
23
|
+
|
|
24
|
+
if (scope && !pkg.name?.startsWith(`${scope}/`)) {
|
|
21
25
|
return null;
|
|
22
26
|
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function traverse(dir, currentDepth = 0) {
|
|
26
|
-
if (currentDepth > maxDepth) return;
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
30
|
-
|
|
31
|
-
for (const entry of entries) {
|
|
32
|
-
const fullPath = path.join(dir, entry.name);
|
|
33
|
-
|
|
34
|
-
if (entry.name === 'node_modules' || entry.isSymbolicLink()) {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!entry.isDirectory()) continue;
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (exists) {
|
|
44
|
-
const pkg = await processPackage(pkgPath, fullPath);
|
|
45
|
-
packages.push(pkg);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
await traverse(fullPath, currentDepth + 1);
|
|
49
|
-
}
|
|
50
|
-
} catch (err) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
28
|
+
return { name: pkg.name, dir };
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
53
31
|
}
|
|
32
|
+
}
|
|
54
33
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Get valid subdirectories (excluding node_modules and symlinks)
|
|
36
|
+
* @param {string} dir - Directory to list
|
|
37
|
+
* @returns {string[]} - Array of absolute paths to subdirectories
|
|
38
|
+
*/
|
|
39
|
+
function getSubdirectories(dir) {
|
|
40
|
+
try {
|
|
41
|
+
return fs
|
|
42
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
43
|
+
.filter(
|
|
44
|
+
(entry) =>
|
|
45
|
+
entry.isDirectory() &&
|
|
46
|
+
!entry.isSymbolicLink() &&
|
|
47
|
+
entry.name !== 'node_modules'
|
|
48
|
+
)
|
|
49
|
+
.map((entry) => path.join(dir, entry.name));
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Recursively collect packages from a directory tree
|
|
57
|
+
* @param {string} dir - Directory to search
|
|
58
|
+
* @param {string|null} scope - Package scope to filter by
|
|
59
|
+
* @param {number} depth - Remaining depth to traverse
|
|
60
|
+
* @returns {PackageInfo[]}
|
|
61
|
+
*/
|
|
62
|
+
function collectPackages(dir, scope, depth) {
|
|
63
|
+
if (depth < 0) return [];
|
|
64
|
+
|
|
65
|
+
return getSubdirectories(dir).flatMap((subdir) => {
|
|
66
|
+
const pkg = readPackageInfo(subdir, scope);
|
|
67
|
+
const nested = collectPackages(subdir, scope, depth - 1);
|
|
68
|
+
return pkg ? [pkg, ...nested] : nested;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Find all packages in a monorepo by traversing the directory tree
|
|
74
|
+
* @param {string} rootDir - Root directory to start searching from
|
|
75
|
+
* @param {string|null} [scope=null] - Package scope to filter by (e.g. '@saasak')
|
|
76
|
+
* @param {number} [maxDepth=4] - Maximum directory depth to traverse
|
|
77
|
+
* @returns {PackageInfo[]} - Array of found packages (root package first, no nulls)
|
|
78
|
+
*/
|
|
79
|
+
export function findMonorepoPackages(rootDir, scope = null, maxDepth = 4) {
|
|
80
|
+
const rootPkg = readPackageInfo(rootDir, scope);
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
await traverse(rootDir, 0);
|
|
82
|
+
if (!rootPkg) return [];
|
|
66
83
|
|
|
67
|
-
return
|
|
84
|
+
return [rootPkg, ...collectPackages(rootDir, scope, maxDepth)];
|
|
68
85
|
}
|