@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/src/utils-env.js CHANGED
@@ -1,31 +1,71 @@
1
- import { decrypt, isEncrypted } from './utils-crypto.js';
1
+ import { decrypt, isEncrypted } from "./utils-crypto.js";
2
2
 
3
- // ========================================
4
- // Helpers for core logic
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
- const variables = Object.entries(variablesForAllTargets).reduce((acc, entry) => {
13
- const [key, value] = entry;
14
- return {
15
- ...acc,
16
- [key]: extractValue(secret, value[target] || value['@@'] || '')
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.map(v => vars[v] || v).join('')
49
- : value
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
- export async function findMonorepoPackages(rootDir, scope = null, maxDepth = 4) {
5
- const packages = [];
6
-
7
- async function processPackage(pkgPath, pkgDir) {
8
- try {
9
- const pkgContent = await fs.readFile(pkgPath, 'utf8');
10
- const pkg = JSON.parse(pkgContent);
11
-
12
- if (scope && !pkg.name?.startsWith(`${scope}/`)) {
13
- return null;
14
- }
15
-
16
- return {
17
- name: pkg.name,
18
- dir: pkgDir,
19
- };
20
- } catch (err) {
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
- const pkgPath = path.join(fullPath, 'package.json');
41
- const exists = await fs.pathExists(pkgPath);
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
- const rootPkgPath = path.join(rootDir, 'package.json');
56
- const exists = await fs.pathExists(rootPkgPath);
57
-
58
- if (!exists) return [];
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
- const pkg = await processPackage(rootPkgPath, rootDir);
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
- if (!pkg) return [];
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
- packages.push(pkg);
65
- await traverse(rootDir, 0);
82
+ if (!rootPkg) return [];
66
83
 
67
- return packages;
84
+ return [rootPkg, ...collectPackages(rootDir, scope, maxDepth)];
68
85
  }