@saasak/tool-env 1.1.0 → 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/bin/index.js +157 -115
- package/package.json +2 -1
- package/src/core.js +119 -74
- package/src/index.d.ts +82 -0
- package/src/utils-env.js +23 -18
package/bin/index.js
CHANGED
|
@@ -1,65 +1,72 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import minimist from
|
|
4
|
-
import fs from
|
|
5
|
-
import path from
|
|
6
|
-
import { execa } from
|
|
7
|
-
|
|
8
|
-
import { encrypt, isEncrypted } from
|
|
9
|
-
import { buildEnv } from
|
|
10
|
-
import { findMonorepoPackages } from
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
import minimist from "minimist";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { execa } from "execa";
|
|
7
|
+
|
|
8
|
+
import { encrypt, isEncrypted } from "../src/utils-crypto.js";
|
|
9
|
+
import { buildEnv } from "../src/utils-env.js";
|
|
10
|
+
import { findMonorepoPackages } from "../src/utils-pkg.js";
|
|
11
|
+
import {
|
|
12
|
+
resolveTarget,
|
|
13
|
+
resolveSecret,
|
|
14
|
+
verifyCanDecrypt,
|
|
15
|
+
getOutputName,
|
|
16
|
+
load,
|
|
17
|
+
} from "../src/core.js";
|
|
18
|
+
|
|
19
|
+
const args = minimist(process.argv.slice(2), { "--": true });
|
|
14
20
|
const __root = process.cwd();
|
|
15
21
|
|
|
16
|
-
const command = args._[0] ||
|
|
22
|
+
const command = args._[0] || "write";
|
|
17
23
|
|
|
18
|
-
const secret = args.secret
|
|
19
|
-
? (args.secret === 'stdin' ? fs.readFileSync(0, 'utf8').trim() : fs.readFileSync(args.secret, 'utf8').trim())
|
|
20
|
-
: process.env.WRENV_SECRET || process.env.TARGET_SECRET || '';
|
|
24
|
+
const secret = resolveSecret(args.secret);
|
|
21
25
|
|
|
22
|
-
const envPath = args.env ||
|
|
26
|
+
const envPath = args.env || ".env.json";
|
|
23
27
|
const envFile = path.resolve(__root, envPath);
|
|
24
28
|
|
|
25
29
|
// Check env file exists for commands that need it
|
|
26
|
-
const commandsRequiringEnvFile = [
|
|
30
|
+
const commandsRequiringEnvFile = ["write", "show", "encrypt", "besafe", "add"];
|
|
27
31
|
if (commandsRequiringEnvFile.includes(command) && !fs.existsSync(envFile)) {
|
|
28
|
-
console.error(
|
|
32
|
+
console.error("No env file found.");
|
|
29
33
|
process.exit(1);
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
async function showCommand() {
|
|
33
37
|
if (!secret) {
|
|
34
|
-
console.log(
|
|
38
|
+
console.log(
|
|
39
|
+
"Secret not provided. Use --secret flag or WRENV_SECRET env variable.",
|
|
40
|
+
);
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
const target = resolveTarget(args.target);
|
|
38
44
|
|
|
39
|
-
const envContent = fs.readFileSync(envFile,
|
|
45
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
40
46
|
const env = JSON.parse(envContent);
|
|
41
47
|
|
|
42
|
-
const rootPkgPath = path.resolve(__root,
|
|
43
|
-
const rootPkgContent = fs.readFileSync(rootPkgPath,
|
|
48
|
+
const rootPkgPath = path.resolve(__root, "package.json");
|
|
49
|
+
const rootPkgContent = fs.readFileSync(rootPkgPath, "utf8");
|
|
44
50
|
const rootPkg = JSON.parse(rootPkgContent);
|
|
45
51
|
|
|
46
|
-
const scope = rootPkg.name.split(
|
|
52
|
+
const scope = rootPkg.name.split("/")[0];
|
|
47
53
|
const [_, ...foundPackages] = findMonorepoPackages(__root, scope);
|
|
48
54
|
|
|
49
55
|
const deps = [
|
|
50
56
|
...foundPackages.filter(Boolean).map((pkg) => ({
|
|
51
|
-
name: pkg.name.replace(`${scope}/`,
|
|
57
|
+
name: pkg.name.replace(`${scope}/`, ""),
|
|
52
58
|
dir: pkg.dir,
|
|
53
59
|
})),
|
|
54
60
|
{
|
|
55
|
-
name:
|
|
61
|
+
name: "root",
|
|
56
62
|
dir: __root,
|
|
57
63
|
},
|
|
58
64
|
];
|
|
59
65
|
|
|
60
66
|
const depsName = deps.map((dep) => dep.name);
|
|
61
67
|
|
|
62
|
-
const allowedEnvs =
|
|
68
|
+
const allowedEnvs =
|
|
69
|
+
env && env.envs && Array.isArray(env.envs) ? env.envs : ["dev"];
|
|
63
70
|
const targetEnv = allowedEnvs.find((env) => env === target);
|
|
64
71
|
if (!targetEnv) {
|
|
65
72
|
console.error(`Target env "${target}" is not allowed.`);
|
|
@@ -71,46 +78,50 @@ async function showCommand() {
|
|
|
71
78
|
for await (const dep of deps) {
|
|
72
79
|
const pkgEnv = Object.entries(allEnvs[dep.name] || {})
|
|
73
80
|
.map(([key, value]) => `${key}=${value}`)
|
|
74
|
-
.join(
|
|
75
|
-
console.log(
|
|
76
|
-
console.log(
|
|
81
|
+
.join("\n");
|
|
82
|
+
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
|
|
83
|
+
console.log(
|
|
84
|
+
`=== ENV for ${dep.name} in ${dep.dir} with target ${targetEnv}`,
|
|
85
|
+
);
|
|
77
86
|
console.log(pkgEnv);
|
|
78
87
|
}
|
|
79
|
-
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
async function writeCommand() {
|
|
83
91
|
if (!secret) {
|
|
84
|
-
console.log(
|
|
92
|
+
console.log(
|
|
93
|
+
"Secret not provided. Use --secret flag or WRENV_SECRET env variable.",
|
|
94
|
+
);
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
const target = resolveTarget(args.target);
|
|
88
98
|
|
|
89
|
-
const envContent = fs.readFileSync(envFile,
|
|
99
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
90
100
|
const env = JSON.parse(envContent);
|
|
91
101
|
|
|
92
|
-
const rootPkgPath = path.resolve(__root,
|
|
93
|
-
const rootPkgContent = fs.readFileSync(rootPkgPath,
|
|
102
|
+
const rootPkgPath = path.resolve(__root, "package.json");
|
|
103
|
+
const rootPkgContent = fs.readFileSync(rootPkgPath, "utf8");
|
|
94
104
|
const rootPkg = JSON.parse(rootPkgContent);
|
|
95
|
-
const scope = rootPkg.name.split(
|
|
105
|
+
const scope = rootPkg.name.split("/")[0];
|
|
96
106
|
|
|
97
107
|
const [_, ...foundPackages] = await findMonorepoPackages(__root, scope);
|
|
98
108
|
const deps = [
|
|
99
109
|
...foundPackages.map((pkg) => ({
|
|
100
|
-
name: pkg.name.replace(`${scope}/`,
|
|
110
|
+
name: pkg.name.replace(`${scope}/`, ""),
|
|
101
111
|
dir: pkg.dir,
|
|
102
112
|
})),
|
|
103
113
|
{
|
|
104
|
-
name:
|
|
114
|
+
name: "root",
|
|
105
115
|
dir: __root,
|
|
106
116
|
},
|
|
107
117
|
];
|
|
108
118
|
const depsName = deps.map((dep) => dep.name);
|
|
109
119
|
|
|
110
|
-
const allowedEnvs =
|
|
120
|
+
const allowedEnvs =
|
|
121
|
+
env && env.envs && Array.isArray(env.envs) ? env.envs : ["dev"];
|
|
111
122
|
|
|
112
123
|
// Handle 'all' target: write all environments with suffixed filenames
|
|
113
|
-
const isAllTarget = target ===
|
|
124
|
+
const isAllTarget = target === "all";
|
|
114
125
|
const targetEnvs = isAllTarget ? allowedEnvs : [target];
|
|
115
126
|
|
|
116
127
|
for (const targetEnv of targetEnvs) {
|
|
@@ -124,9 +135,11 @@ async function writeCommand() {
|
|
|
124
135
|
for await (const dep of deps) {
|
|
125
136
|
const pkgEnv = Object.entries(allEnvs[dep.name] || {})
|
|
126
137
|
.map(([key, value]) => `${key}=${value}`)
|
|
127
|
-
.join(
|
|
138
|
+
.join("\n");
|
|
128
139
|
// Use suffixed filename (.env.{outputName}) for 'all', plain .env otherwise
|
|
129
|
-
const filename = isAllTarget
|
|
140
|
+
const filename = isAllTarget
|
|
141
|
+
? `.env.${getOutputName(targetEnv)}`
|
|
142
|
+
: ".env";
|
|
130
143
|
console.log(`Writing ${filename} for ${dep.name} in ${dep.dir}`);
|
|
131
144
|
await fs.writeFile(path.join(dep.dir, filename), pkgEnv);
|
|
132
145
|
}
|
|
@@ -134,27 +147,35 @@ async function writeCommand() {
|
|
|
134
147
|
}
|
|
135
148
|
|
|
136
149
|
async function encryptCommand() {
|
|
137
|
-
const envContent = fs.readFileSync(envFile,
|
|
150
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
138
151
|
const env = JSON.parse(envContent);
|
|
139
152
|
|
|
140
153
|
if (!secret) {
|
|
141
|
-
console.error(
|
|
154
|
+
console.error(
|
|
155
|
+
"Cannot encrypt env file: secret not provided. Doing nothing",
|
|
156
|
+
);
|
|
142
157
|
return;
|
|
143
158
|
}
|
|
144
159
|
|
|
145
160
|
// Verify we can decrypt all existing encrypted values before encrypting new ones
|
|
146
161
|
const verification = verifyCanDecrypt(env, secret);
|
|
147
162
|
if (!verification.success) {
|
|
148
|
-
console.error(
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
console.error(
|
|
164
|
+
`Cannot encrypt: the provided secret cannot decrypt existing encrypted value at "${verification.path}".`,
|
|
165
|
+
);
|
|
166
|
+
console.error(
|
|
167
|
+
"This would result in a file with mixed encryption passwords.",
|
|
168
|
+
);
|
|
169
|
+
console.error(
|
|
170
|
+
"Please provide the correct secret that was used for existing encrypted values.",
|
|
171
|
+
);
|
|
151
172
|
process.exit(1);
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
if (env.variables) {
|
|
155
176
|
for (const [varName, varValue] of Object.entries(env.variables)) {
|
|
156
177
|
for (const [envKey, envVal] of Object.entries(varValue)) {
|
|
157
|
-
if (typeof envVal ===
|
|
178
|
+
if (typeof envVal === "string" && !isEncrypted(envVal)) {
|
|
158
179
|
env.variables[varName][envKey] = encrypt(envVal, secret);
|
|
159
180
|
}
|
|
160
181
|
}
|
|
@@ -165,7 +186,7 @@ async function encryptCommand() {
|
|
|
165
186
|
for (const [pkgName, pkgOverrides] of Object.entries(env.overrides)) {
|
|
166
187
|
for (const [varName, varValue] of Object.entries(pkgOverrides)) {
|
|
167
188
|
for (const [envKey, envVal] of Object.entries(varValue)) {
|
|
168
|
-
if (typeof envVal ===
|
|
189
|
+
if (typeof envVal === "string" && !isEncrypted(envVal)) {
|
|
169
190
|
env.overrides[pkgName][varName][envKey] = encrypt(envVal, secret);
|
|
170
191
|
}
|
|
171
192
|
}
|
|
@@ -178,7 +199,11 @@ async function encryptCommand() {
|
|
|
178
199
|
|
|
179
200
|
async function besafeCommand() {
|
|
180
201
|
// Find git root to ensure we're in a repository and use correct base path
|
|
181
|
-
const { stdout: gitRoot, exitCode: gitCheck } = await execa(
|
|
202
|
+
const { stdout: gitRoot, exitCode: gitCheck } = await execa(
|
|
203
|
+
"git",
|
|
204
|
+
["rev-parse", "--show-toplevel"],
|
|
205
|
+
{ cwd: __root, reject: false },
|
|
206
|
+
);
|
|
182
207
|
if (gitCheck !== 0) {
|
|
183
208
|
return;
|
|
184
209
|
}
|
|
@@ -186,9 +211,21 @@ async function besafeCommand() {
|
|
|
186
211
|
const relativeEnvPath = path.relative(gitRoot, envFile);
|
|
187
212
|
|
|
188
213
|
// Check if file has any changes (staged, unstaged, or untracked)
|
|
189
|
-
const { stdout: staged } = await execa(
|
|
190
|
-
|
|
191
|
-
|
|
214
|
+
const { stdout: staged } = await execa(
|
|
215
|
+
"git",
|
|
216
|
+
["diff", "--cached", "--name-only", "--", relativeEnvPath],
|
|
217
|
+
{ cwd: gitRoot, reject: false },
|
|
218
|
+
);
|
|
219
|
+
const { stdout: unstaged } = await execa(
|
|
220
|
+
"git",
|
|
221
|
+
["diff", "--name-only", "--", relativeEnvPath],
|
|
222
|
+
{ cwd: gitRoot, reject: false },
|
|
223
|
+
);
|
|
224
|
+
const { stdout: untracked } = await execa(
|
|
225
|
+
"git",
|
|
226
|
+
["ls-files", "--others", "--exclude-standard", "--", relativeEnvPath],
|
|
227
|
+
{ cwd: gitRoot, reject: false },
|
|
228
|
+
);
|
|
192
229
|
|
|
193
230
|
if (!staged?.trim() && !unstaged?.trim() && !untracked?.trim()) {
|
|
194
231
|
return;
|
|
@@ -197,24 +234,28 @@ async function besafeCommand() {
|
|
|
197
234
|
await encryptCommand();
|
|
198
235
|
|
|
199
236
|
if (!secret) {
|
|
200
|
-
console.error(
|
|
237
|
+
console.error(
|
|
238
|
+
"Warning: potential security risk: secret not provided. Variables WERE NOT encrypted.",
|
|
239
|
+
);
|
|
201
240
|
}
|
|
202
241
|
|
|
203
|
-
await execa(
|
|
242
|
+
await execa("git", ["add", relativeEnvPath], { cwd: gitRoot });
|
|
204
243
|
}
|
|
205
244
|
|
|
206
245
|
async function addCommand() {
|
|
207
246
|
const varName = args._[1];
|
|
208
247
|
if (!varName) {
|
|
209
|
-
console.error(
|
|
248
|
+
console.error("Variable name is required.");
|
|
210
249
|
process.exit(1);
|
|
211
250
|
}
|
|
212
251
|
|
|
213
252
|
if (!secret) {
|
|
214
|
-
console.error(
|
|
253
|
+
console.error(
|
|
254
|
+
"Warning: potential security risk: secret not provided. Variable WILL NOT be encrypted.",
|
|
255
|
+
);
|
|
215
256
|
}
|
|
216
257
|
|
|
217
|
-
const envContent = fs.readFileSync(envFile,
|
|
258
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
218
259
|
const env = JSON.parse(envContent);
|
|
219
260
|
|
|
220
261
|
if (!env.variables) {
|
|
@@ -226,10 +267,10 @@ async function addCommand() {
|
|
|
226
267
|
}
|
|
227
268
|
|
|
228
269
|
for (const arg of args._.slice(2)) {
|
|
229
|
-
if (!arg.startsWith(
|
|
230
|
-
const [key, ...valueParts] = arg.slice(1).split(
|
|
231
|
-
const value = valueParts.join(
|
|
232
|
-
const envKey = key ===
|
|
270
|
+
if (!arg.startsWith("+")) continue;
|
|
271
|
+
const [key, ...valueParts] = arg.slice(1).split("=");
|
|
272
|
+
const value = valueParts.join("=");
|
|
273
|
+
const envKey = key === "fallback" ? "@@" : key;
|
|
233
274
|
env.variables[varName][envKey] = secret ? encrypt(value, secret) : value;
|
|
234
275
|
}
|
|
235
276
|
|
|
@@ -237,40 +278,23 @@ async function addCommand() {
|
|
|
237
278
|
}
|
|
238
279
|
|
|
239
280
|
async function runCommand() {
|
|
240
|
-
const commandParts = args[
|
|
281
|
+
const commandParts = args["--"] || [];
|
|
241
282
|
const [cmd, ...cmdArgs] = commandParts;
|
|
242
283
|
|
|
243
284
|
if (!cmd) {
|
|
244
|
-
console.error(
|
|
285
|
+
console.error("Usage: wrenv run [options] -- <command> [args...]");
|
|
245
286
|
process.exit(1);
|
|
246
287
|
}
|
|
247
288
|
|
|
248
|
-
// Load environment variables
|
|
289
|
+
// Load environment variables (without applying to current process.env)
|
|
249
290
|
let envVars = {};
|
|
250
291
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
? (args.secret === 'stdin' ? fs.readFileSync(0, 'utf8').trim() : fs.readFileSync(args.secret, 'utf8').trim())
|
|
255
|
-
: process.env.WRENV_SECRET || process.env.TARGET_SECRET || null;
|
|
256
|
-
|
|
257
|
-
const target = resolveTarget(args.target);
|
|
258
|
-
|
|
259
|
-
envVars = loadEnvJson({
|
|
260
|
-
secret: resolvedSecret,
|
|
261
|
-
targetEnv: target,
|
|
292
|
+
envVars = load({
|
|
293
|
+
secret: resolveSecret(args.secret),
|
|
294
|
+
targetEnv: args.target,
|
|
262
295
|
envPath: args.env,
|
|
296
|
+
applyToProcess: false,
|
|
263
297
|
});
|
|
264
|
-
|
|
265
|
-
// Apply .env.local overrides only in dev mode (security: prevent local overrides in production)
|
|
266
|
-
if (target === 'dev') {
|
|
267
|
-
const localOverrides = loadLocalOverrides();
|
|
268
|
-
for (const [key, value] of Object.entries(localOverrides)) {
|
|
269
|
-
if (Object.hasOwn(envVars, key)) {
|
|
270
|
-
envVars[key] = value;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
298
|
} catch (error) {
|
|
275
299
|
console.error(`wrenv: ${error.message}`);
|
|
276
300
|
process.exit(1);
|
|
@@ -278,48 +302,66 @@ async function runCommand() {
|
|
|
278
302
|
|
|
279
303
|
// Use execa for cross-platform compatibility with minimal overhead
|
|
280
304
|
const subprocess = execa(cmd, cmdArgs, {
|
|
281
|
-
stdio:
|
|
305
|
+
stdio: "inherit",
|
|
282
306
|
env: { ...process.env, ...envVars },
|
|
283
307
|
reject: false,
|
|
284
308
|
});
|
|
285
309
|
|
|
286
310
|
// Forward signals to child process
|
|
287
311
|
const forwardSignal = (signal) => subprocess.kill(signal);
|
|
288
|
-
process.on(
|
|
289
|
-
process.on(
|
|
290
|
-
process.on(
|
|
312
|
+
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
313
|
+
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
|
314
|
+
process.on("SIGHUP", () => forwardSignal("SIGHUP"));
|
|
291
315
|
|
|
292
316
|
const result = await subprocess;
|
|
293
317
|
process.exit(result.exitCode ?? 0);
|
|
294
318
|
}
|
|
295
319
|
|
|
296
320
|
function helpCommand() {
|
|
297
|
-
console.log(
|
|
298
|
-
console.log(
|
|
299
|
-
console.log(
|
|
300
|
-
console.log(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
console.log(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
console.log(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
console.log(
|
|
310
|
-
console.log(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
console.log(
|
|
314
|
-
console.log(
|
|
315
|
-
console.log(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
console.log(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
console.log(
|
|
322
|
-
|
|
321
|
+
console.log("Usage: wrenv [command] [options]\n");
|
|
322
|
+
console.log("Commands:");
|
|
323
|
+
console.log(" write Write .env files for all packages (default)");
|
|
324
|
+
console.log(
|
|
325
|
+
" show Show environment variables without writing files",
|
|
326
|
+
);
|
|
327
|
+
console.log(
|
|
328
|
+
" run Run a command with injected environment variables",
|
|
329
|
+
);
|
|
330
|
+
console.log(
|
|
331
|
+
" encrypt Encrypt all unencrypted variables in .env.json",
|
|
332
|
+
);
|
|
333
|
+
console.log(" besafe Encrypt and stage .env.json if it has changes");
|
|
334
|
+
console.log(
|
|
335
|
+
" add <var> Add a new variable with environment-specific values",
|
|
336
|
+
);
|
|
337
|
+
console.log(" help Show this help message\n");
|
|
338
|
+
console.log("Options:");
|
|
339
|
+
console.log(
|
|
340
|
+
' --secret <path|stdin> Path to secret file or "stdin" to read from stdin',
|
|
341
|
+
);
|
|
342
|
+
console.log(
|
|
343
|
+
" --target <env> Target environment (dev, staging, prod, all)",
|
|
344
|
+
);
|
|
345
|
+
console.log(
|
|
346
|
+
" --env <path> Path to env file (default: .env.json)\n",
|
|
347
|
+
);
|
|
348
|
+
console.log("Environment Variables:");
|
|
349
|
+
console.log(" WRENV_SECRET Secret for encryption/decryption");
|
|
350
|
+
console.log(" WRENV_TARGET Target environment");
|
|
351
|
+
console.log(" TARGET_SECRET Alias for WRENV_SECRET");
|
|
352
|
+
console.log(" TARGET_ENV Alias for WRENV_TARGET\n");
|
|
353
|
+
console.log("Examples:");
|
|
354
|
+
console.log(" wrenv --secret ~/.big-secret write --target dev");
|
|
355
|
+
console.log(" wrenv --secret stdin write < ~/.big-secret");
|
|
356
|
+
console.log(" wrenv show --target prod");
|
|
357
|
+
console.log(" wrenv run --target prod -- node server.js");
|
|
358
|
+
console.log(" wrenv run -- npm test --coverage");
|
|
359
|
+
console.log(
|
|
360
|
+
" wrenv add NEW_VAR +fallback=value +dev=dev_value +production=prod_value",
|
|
361
|
+
);
|
|
362
|
+
console.log(
|
|
363
|
+
" wrenv write --target all # writes .env.development, .env.staging, .env.production",
|
|
364
|
+
);
|
|
323
365
|
}
|
|
324
366
|
|
|
325
367
|
const commands = {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasak/tool-env",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"author": "dev@saasak.studio",
|
|
6
6
|
"description": "A small util to manage environment variables for your monorepo",
|
|
7
7
|
"keywords": [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "src/index.js",
|
|
14
|
+
"types": "src/index.d.ts",
|
|
14
15
|
"files": [
|
|
15
16
|
"bin",
|
|
16
17
|
"src"
|
package/src/core.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { decrypt, isEncrypted } from
|
|
4
|
-
import { buildEnv } from
|
|
5
|
-
import { findMonorepoPackages } from
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { decrypt, isEncrypted } from "./utils-crypto.js";
|
|
4
|
+
import { buildEnv } from "./utils-env.js";
|
|
5
|
+
import { findMonorepoPackages } from "./utils-pkg.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @typedef {Object.<string, string>} EnvRecord
|
|
@@ -34,7 +34,19 @@ import { findMonorepoPackages } from './utils-pkg.js';
|
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
/** @type {string} */
|
|
37
|
-
const DEFAULT_ENV =
|
|
37
|
+
const DEFAULT_ENV = "dev";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Environment variables that must never be injected by .env.json content.
|
|
41
|
+
* These control wrenv's own behavior and must not be overridden.
|
|
42
|
+
* @type {Set<string>}
|
|
43
|
+
*/
|
|
44
|
+
export const RESERVED_ENV_VARS = new Set([
|
|
45
|
+
"WRENV_TARGET",
|
|
46
|
+
"WRENV_SECRET",
|
|
47
|
+
"TARGET_ENV",
|
|
48
|
+
"TARGET_SECRET",
|
|
49
|
+
]);
|
|
38
50
|
|
|
39
51
|
/**
|
|
40
52
|
* Mapping of environment name aliases to canonical names
|
|
@@ -42,14 +54,14 @@ const DEFAULT_ENV = 'dev';
|
|
|
42
54
|
* @type {Object.<string, string>}
|
|
43
55
|
*/
|
|
44
56
|
const targets = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
development: "dev",
|
|
58
|
+
dev: "dev",
|
|
59
|
+
staging: "preprod",
|
|
60
|
+
pp: "preprod",
|
|
61
|
+
preprod: "preprod",
|
|
62
|
+
prod: "production",
|
|
63
|
+
production: "production",
|
|
64
|
+
all: "all",
|
|
53
65
|
};
|
|
54
66
|
|
|
55
67
|
/**
|
|
@@ -58,9 +70,9 @@ const targets = {
|
|
|
58
70
|
* @type {Object.<string, string>}
|
|
59
71
|
*/
|
|
60
72
|
const outputNames = {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
dev: "development",
|
|
74
|
+
preprod: "staging",
|
|
75
|
+
production: "production",
|
|
64
76
|
};
|
|
65
77
|
|
|
66
78
|
/**
|
|
@@ -104,39 +116,52 @@ export function resolveTarget(targetParam) {
|
|
|
104
116
|
* @returns {string[]} - Resolved target sliases environment
|
|
105
117
|
*/
|
|
106
118
|
export function resolveTargetAliases(allowedEnvs) {
|
|
107
|
-
const allowedAliases = allowedEnvs.flatMap(
|
|
108
|
-
|
|
119
|
+
const allowedAliases = allowedEnvs.flatMap((env) =>
|
|
120
|
+
Object.entries(targets)
|
|
109
121
|
.find(([_, val]) => val === env)
|
|
110
|
-
.map(([alias]) => alias)
|
|
111
|
-
|
|
112
|
-
)
|
|
122
|
+
.map(([alias]) => alias),
|
|
123
|
+
);
|
|
113
124
|
|
|
114
125
|
return Array.from(new Set(allowedAliases));
|
|
115
126
|
}
|
|
116
127
|
|
|
117
|
-
|
|
118
128
|
/**
|
|
119
|
-
* Resolve secret with
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
129
|
+
* Resolve secret with unified fallback chain:
|
|
130
|
+
* param (stdin | file:// | file path | literal) -> WRENV_SECRET -> TARGET_SECRET -> null
|
|
131
|
+
*
|
|
132
|
+
* If reading from stdin or a file:// path fails, falls back to env vars.
|
|
133
|
+
* A bare string that is not a readable file is treated as a literal secret.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} [secretParam] - Secret source: "stdin", "file://path", file path, or literal string
|
|
136
|
+
* @returns {string} - Resolved secret or empty string if not provided
|
|
124
137
|
*/
|
|
125
138
|
export function resolveSecret(secretParam) {
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
139
|
+
if (secretParam) {
|
|
140
|
+
if (secretParam === "stdin") {
|
|
141
|
+
try {
|
|
142
|
+
const value = fs.readFileSync(0, "utf8").trim();
|
|
143
|
+
if (value) return value;
|
|
144
|
+
} catch (_) {
|
|
145
|
+
// fall through to env vars
|
|
146
|
+
}
|
|
147
|
+
} else if (secretParam.startsWith("file://")) {
|
|
148
|
+
try {
|
|
149
|
+
const value = fs.readFileSync(secretParam.slice(7), "utf8").trim();
|
|
150
|
+
if (value) return value;
|
|
151
|
+
} catch (_) {
|
|
152
|
+
// fall through to env vars
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
try {
|
|
156
|
+
return fs.readFileSync(secretParam, "utf8").trim();
|
|
157
|
+
} catch (_) {
|
|
158
|
+
// Not a readable file path — treat as literal secret string
|
|
159
|
+
return secretParam;
|
|
160
|
+
}
|
|
136
161
|
}
|
|
137
162
|
}
|
|
138
163
|
|
|
139
|
-
return
|
|
164
|
+
return process.env.WRENV_SECRET || process.env.TARGET_SECRET || "";
|
|
140
165
|
}
|
|
141
166
|
|
|
142
167
|
/**
|
|
@@ -148,18 +173,18 @@ export function resolveSecret(secretParam) {
|
|
|
148
173
|
export function parseEnvFile(content) {
|
|
149
174
|
/** @type {EnvRecord} */
|
|
150
175
|
const result = {};
|
|
151
|
-
const lines = content.split(
|
|
176
|
+
const lines = content.split("\n");
|
|
152
177
|
|
|
153
178
|
for (const line of lines) {
|
|
154
179
|
const trimmed = line.trim();
|
|
155
180
|
|
|
156
181
|
// Skip empty lines and comments
|
|
157
|
-
if (!trimmed || trimmed.startsWith(
|
|
182
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
158
183
|
continue;
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
// Find the first = sign
|
|
162
|
-
const eqIndex = trimmed.indexOf(
|
|
187
|
+
const eqIndex = trimmed.indexOf("=");
|
|
163
188
|
if (eqIndex === -1) {
|
|
164
189
|
continue;
|
|
165
190
|
}
|
|
@@ -168,8 +193,10 @@ export function parseEnvFile(content) {
|
|
|
168
193
|
let value = trimmed.slice(eqIndex + 1).trim();
|
|
169
194
|
|
|
170
195
|
// Remove surrounding quotes if present
|
|
171
|
-
if (
|
|
172
|
-
(value.startsWith("'
|
|
196
|
+
if (
|
|
197
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
198
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
199
|
+
) {
|
|
173
200
|
value = value.slice(1, -1);
|
|
174
201
|
}
|
|
175
202
|
|
|
@@ -190,14 +217,15 @@ export function parseEnvFile(content) {
|
|
|
190
217
|
function findNearestPackageName(cwd, scope) {
|
|
191
218
|
let currentDir = cwd;
|
|
192
219
|
|
|
193
|
-
while (currentDir !== path.dirname(currentDir)) {
|
|
194
|
-
|
|
220
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
221
|
+
// Stop at filesystem root
|
|
222
|
+
const pkgPath = path.join(currentDir, "package.json");
|
|
195
223
|
if (fs.existsSync(pkgPath)) {
|
|
196
224
|
try {
|
|
197
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath,
|
|
225
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
198
226
|
if (pkg.name) {
|
|
199
227
|
// Return name without scope prefix
|
|
200
|
-
return pkg.name.replace(`${scope}/`,
|
|
228
|
+
return pkg.name.replace(`${scope}/`, "");
|
|
201
229
|
}
|
|
202
230
|
} catch (e) {
|
|
203
231
|
// Continue traversing upward
|
|
@@ -206,7 +234,7 @@ function findNearestPackageName(cwd, scope) {
|
|
|
206
234
|
currentDir = path.dirname(currentDir);
|
|
207
235
|
}
|
|
208
236
|
|
|
209
|
-
return
|
|
237
|
+
return "root"; // fallback
|
|
210
238
|
}
|
|
211
239
|
|
|
212
240
|
/**
|
|
@@ -215,14 +243,14 @@ function findNearestPackageName(cwd, scope) {
|
|
|
215
243
|
* @returns {EnvRecord} - Local overrides or empty object
|
|
216
244
|
*/
|
|
217
245
|
export function loadLocalOverrides(cwd = process.cwd()) {
|
|
218
|
-
const localEnvPath = path.join(cwd,
|
|
246
|
+
const localEnvPath = path.join(cwd, ".env.local");
|
|
219
247
|
|
|
220
248
|
if (!fs.existsSync(localEnvPath)) {
|
|
221
249
|
return {};
|
|
222
250
|
}
|
|
223
251
|
|
|
224
252
|
try {
|
|
225
|
-
const content = fs.readFileSync(localEnvPath,
|
|
253
|
+
const content = fs.readFileSync(localEnvPath, "utf8");
|
|
226
254
|
return parseEnvFile(content);
|
|
227
255
|
} catch (error) {
|
|
228
256
|
return {};
|
|
@@ -260,8 +288,8 @@ export function loadEnvJson(options = {}) {
|
|
|
260
288
|
const {
|
|
261
289
|
secret: secretParam,
|
|
262
290
|
targetEnv: targetParam,
|
|
263
|
-
envPath =
|
|
264
|
-
cwd = process.cwd()
|
|
291
|
+
envPath = ".env.json",
|
|
292
|
+
cwd = process.cwd(),
|
|
265
293
|
} = options;
|
|
266
294
|
|
|
267
295
|
const secret = resolveSecret(secretParam);
|
|
@@ -274,32 +302,35 @@ export function loadEnvJson(options = {}) {
|
|
|
274
302
|
}
|
|
275
303
|
|
|
276
304
|
const envFile = path.join(monorepoRoot, envPath);
|
|
277
|
-
const envContent = fs.readFileSync(envFile,
|
|
305
|
+
const envContent = fs.readFileSync(envFile, "utf8");
|
|
278
306
|
const env = JSON.parse(envContent);
|
|
279
307
|
|
|
280
|
-
const allowedEnvs =
|
|
308
|
+
const allowedEnvs =
|
|
309
|
+
env && env.envs && Array.isArray(env.envs) ? env.envs : [DEFAULT_ENV];
|
|
281
310
|
const resolvedTarget = allowedEnvs.find((e) => e === targetEnv);
|
|
282
311
|
|
|
283
312
|
if (!resolvedTarget) {
|
|
284
|
-
throw new Error(
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Target env "${targetEnv}" is not allowed. Allowed: ${allowedEnvs.join(", ")}`,
|
|
315
|
+
);
|
|
285
316
|
}
|
|
286
317
|
|
|
287
318
|
// Get scope from monorepo root package.json
|
|
288
|
-
const rootPkgPath = path.join(monorepoRoot,
|
|
289
|
-
let scope =
|
|
319
|
+
const rootPkgPath = path.join(monorepoRoot, "package.json");
|
|
320
|
+
let scope = "";
|
|
290
321
|
/** @type {string[]} */
|
|
291
|
-
let depsName = [
|
|
322
|
+
let depsName = ["root"];
|
|
292
323
|
|
|
293
324
|
if (fs.existsSync(rootPkgPath)) {
|
|
294
325
|
try {
|
|
295
|
-
const rootPkgContent = fs.readFileSync(rootPkgPath,
|
|
326
|
+
const rootPkgContent = fs.readFileSync(rootPkgPath, "utf8");
|
|
296
327
|
const rootPkg = JSON.parse(rootPkgContent);
|
|
297
|
-
scope = rootPkg.name.split(
|
|
328
|
+
scope = rootPkg.name.split("/")[0];
|
|
298
329
|
|
|
299
330
|
const foundPackages = findMonorepoPackages(monorepoRoot, scope);
|
|
300
331
|
depsName = [
|
|
301
|
-
...foundPackages.map((pkg) => pkg.name.replace(`${scope}/`,
|
|
302
|
-
|
|
332
|
+
...foundPackages.map((pkg) => pkg.name.replace(`${scope}/`, "")),
|
|
333
|
+
"root",
|
|
303
334
|
];
|
|
304
335
|
} catch (error) {
|
|
305
336
|
// Not a monorepo or error parsing, use default
|
|
@@ -339,7 +370,7 @@ export function load(options = {}) {
|
|
|
339
370
|
secret,
|
|
340
371
|
envPath,
|
|
341
372
|
cwd = process.cwd(),
|
|
342
|
-
applyToProcess = true
|
|
373
|
+
applyToProcess = true,
|
|
343
374
|
} = options;
|
|
344
375
|
|
|
345
376
|
// Resolve target first to determine if we're in dev mode
|
|
@@ -350,13 +381,12 @@ export function load(options = {}) {
|
|
|
350
381
|
secret,
|
|
351
382
|
targetEnv: resolvedTarget,
|
|
352
383
|
envPath,
|
|
353
|
-
cwd
|
|
384
|
+
cwd,
|
|
354
385
|
});
|
|
355
386
|
|
|
356
387
|
// Load .env.local overrides only in dev mode (security: prevent local overrides in production)
|
|
357
|
-
const localOverrides =
|
|
358
|
-
? loadLocalOverrides(cwd)
|
|
359
|
-
: {};
|
|
388
|
+
const localOverrides =
|
|
389
|
+
resolvedTarget === "dev" ? loadLocalOverrides(cwd) : {};
|
|
360
390
|
|
|
361
391
|
// Merge: env.json values, then .env.local overrides (local takes precedence)
|
|
362
392
|
// Note: .env.local only overrides env.json values, not existing process.env
|
|
@@ -369,6 +399,15 @@ export function load(options = {}) {
|
|
|
369
399
|
}
|
|
370
400
|
}
|
|
371
401
|
|
|
402
|
+
// - Reserved vars (WRENV_*, TARGET_*) are never injected
|
|
403
|
+
// - NODE_ENV is only injected if not already set
|
|
404
|
+
for (const key of RESERVED_ENV_VARS) {
|
|
405
|
+
delete finalConfig[key];
|
|
406
|
+
}
|
|
407
|
+
if (process.env.NODE_ENV) {
|
|
408
|
+
delete finalConfig.NODE_ENV;
|
|
409
|
+
}
|
|
410
|
+
|
|
372
411
|
// Apply to process.env if requested
|
|
373
412
|
if (applyToProcess) {
|
|
374
413
|
for (const [key, value] of Object.entries(finalConfig)) {
|
|
@@ -397,15 +436,18 @@ export function verifyCanDecrypt(env, secret) {
|
|
|
397
436
|
const encryptedValues = [];
|
|
398
437
|
|
|
399
438
|
if (!secret) {
|
|
400
|
-
return { success: false, path: null, error:
|
|
439
|
+
return { success: false, path: null, error: "No secret provided" };
|
|
401
440
|
}
|
|
402
441
|
|
|
403
442
|
// Collect all encrypted values from variables
|
|
404
443
|
if (env.variables) {
|
|
405
444
|
for (const [varName, varValue] of Object.entries(env.variables)) {
|
|
406
445
|
for (const [envKey, envVal] of Object.entries(varValue)) {
|
|
407
|
-
if (typeof envVal ===
|
|
408
|
-
encryptedValues.push({
|
|
446
|
+
if (typeof envVal === "string" && isEncrypted(envVal)) {
|
|
447
|
+
encryptedValues.push({
|
|
448
|
+
path: `variables.${varName}.${envKey}`,
|
|
449
|
+
value: envVal,
|
|
450
|
+
});
|
|
409
451
|
}
|
|
410
452
|
}
|
|
411
453
|
}
|
|
@@ -416,8 +458,11 @@ export function verifyCanDecrypt(env, secret) {
|
|
|
416
458
|
for (const [pkgName, pkgOverrides] of Object.entries(env.overrides)) {
|
|
417
459
|
for (const [varName, varValue] of Object.entries(pkgOverrides)) {
|
|
418
460
|
for (const [envKey, envVal] of Object.entries(varValue)) {
|
|
419
|
-
if (typeof envVal ===
|
|
420
|
-
encryptedValues.push({
|
|
461
|
+
if (typeof envVal === "string" && isEncrypted(envVal)) {
|
|
462
|
+
encryptedValues.push({
|
|
463
|
+
path: `overrides.${pkgName}.${varName}.${envKey}`,
|
|
464
|
+
value: envVal,
|
|
465
|
+
});
|
|
421
466
|
}
|
|
422
467
|
}
|
|
423
468
|
}
|
|
@@ -437,7 +482,7 @@ export function verifyCanDecrypt(env, secret) {
|
|
|
437
482
|
return {
|
|
438
483
|
success: false,
|
|
439
484
|
path,
|
|
440
|
-
error: error.message
|
|
485
|
+
error: error.message,
|
|
441
486
|
};
|
|
442
487
|
}
|
|
443
488
|
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Key-value pairs of environment variable name to value */
|
|
2
|
+
export type EnvRecord = Record<string, string>;
|
|
3
|
+
|
|
4
|
+
export interface LoadOptions {
|
|
5
|
+
/** Target environment (e.g. 'dev', 'preprod', 'production') */
|
|
6
|
+
targetEnv?: string;
|
|
7
|
+
/** Secret for decryption: "stdin", "file://path", file path, or literal string */
|
|
8
|
+
secret?: string;
|
|
9
|
+
/** Path to env.json file (default: '.env.json') */
|
|
10
|
+
envPath?: string;
|
|
11
|
+
/** Working directory (default: process.cwd()) */
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/** Whether to apply resolved vars to process.env (default: true) */
|
|
14
|
+
applyToProcess?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load environment variables from .env.json with optional .env.local overrides.
|
|
19
|
+
* By default, applies resolved variables to process.env.
|
|
20
|
+
*/
|
|
21
|
+
export function load(options?: LoadOptions): EnvRecord;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load environment configuration from .env.json without .env.local overrides.
|
|
25
|
+
*/
|
|
26
|
+
export function loadEnvJson(options?: {
|
|
27
|
+
secret?: string;
|
|
28
|
+
targetEnv?: string;
|
|
29
|
+
envPath?: string;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
}): EnvRecord;
|
|
32
|
+
|
|
33
|
+
/** Load .env.local overrides if the file exists */
|
|
34
|
+
export function loadLocalOverrides(cwd?: string): EnvRecord;
|
|
35
|
+
|
|
36
|
+
/** Parse .env file content into key-value pairs */
|
|
37
|
+
export function parseEnvFile(content: string): EnvRecord;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve target environment with fallback chain:
|
|
41
|
+
* param -> WRENV_TARGET -> TARGET_ENV -> NODE_ENV (mapped) -> 'dev'
|
|
42
|
+
*/
|
|
43
|
+
export function resolveTarget(targetParam?: string): string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve secret with fallback chain:
|
|
47
|
+
* param (stdin | file:// | file path | literal) -> WRENV_SECRET -> TARGET_SECRET -> ''
|
|
48
|
+
*/
|
|
49
|
+
export function resolveSecret(secretParam?: string): string;
|
|
50
|
+
|
|
51
|
+
/** Get the conventional output file name for an internal environment name */
|
|
52
|
+
export function getOutputName(internalName: string): string;
|
|
53
|
+
|
|
54
|
+
/** Verify that all encrypted values can be decrypted with the provided secret */
|
|
55
|
+
export function verifyCanDecrypt(env: object, secret: string): {
|
|
56
|
+
success: boolean;
|
|
57
|
+
path?: string | null;
|
|
58
|
+
error?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Encrypt a plaintext value with the given secret */
|
|
62
|
+
export function encrypt(value: string, secret: string): string;
|
|
63
|
+
|
|
64
|
+
/** Decrypt an encrypted value with the given secret */
|
|
65
|
+
export function decrypt(value: string, secret: string): string;
|
|
66
|
+
|
|
67
|
+
/** Check if a value is encrypted */
|
|
68
|
+
export function isEncrypted(value: string): boolean;
|
|
69
|
+
|
|
70
|
+
/** Build environment variables for all packages from an env config */
|
|
71
|
+
export function buildEnv(
|
|
72
|
+
secret: string,
|
|
73
|
+
target: string,
|
|
74
|
+
env: object,
|
|
75
|
+
names: string[],
|
|
76
|
+
): Record<string, EnvRecord>;
|
|
77
|
+
|
|
78
|
+
/** Find all packages in a monorepo */
|
|
79
|
+
export function findMonorepoPackages(
|
|
80
|
+
root: string,
|
|
81
|
+
scope: string,
|
|
82
|
+
): Array<{ name: string; dir: string }>;
|
package/src/utils-env.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { decrypt, isEncrypted } from
|
|
1
|
+
import { decrypt, isEncrypted } from "./utils-crypto.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @
|
|
7
|
-
* @property {string} [preprod] - Value for preprod environment
|
|
8
|
-
* @property {string} [production] - Value for production environment
|
|
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
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
/**
|
|
@@ -47,13 +45,16 @@ export function buildEnv(secret, target, env, names) {
|
|
|
47
45
|
|
|
48
46
|
// For each variable, extract the value for the target env
|
|
49
47
|
/** @type {EnvRecord} */
|
|
50
|
-
const variables = Object.entries(variablesForAllTargets).reduce(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
);
|
|
57
58
|
|
|
58
59
|
// For each package, get all variables
|
|
59
60
|
// compute the overrides and merge them
|
|
@@ -64,7 +65,7 @@ export function buildEnv(secret, target, env, names) {
|
|
|
64
65
|
const allVars = names.reduce((acc, name) => {
|
|
65
66
|
acc[name] = {
|
|
66
67
|
...variables,
|
|
67
|
-
...parseOverrides(secret, target, overrides[name], variables)
|
|
68
|
+
...parseOverrides(secret, target, overrides[name], variables),
|
|
68
69
|
};
|
|
69
70
|
|
|
70
71
|
return acc;
|
|
@@ -77,7 +78,7 @@ export function buildEnv(secret, target, env, names) {
|
|
|
77
78
|
* Parse package-specific overrides for a target environment
|
|
78
79
|
* @param {string} secret - Secret for decrypting encrypted values
|
|
79
80
|
* @param {string} target - Target environment
|
|
80
|
-
* @param {Object.<string, EnvVariableValue>}
|
|
81
|
+
* @param {Object.<string, EnvVariableValue>} overrides - Package overrides (may be undefined)
|
|
81
82
|
* @param {EnvRecord} vars - Base variables for reference substitution
|
|
82
83
|
* @returns {EnvRecord} - Parsed override values
|
|
83
84
|
*/
|
|
@@ -86,12 +87,16 @@ function parseOverrides(secret, target, overrides, vars) {
|
|
|
86
87
|
|
|
87
88
|
return Object.entries(overrides).reduce((acc, entry) => {
|
|
88
89
|
const [key, val] = entry;
|
|
89
|
-
const value = extractValue(secret, val[target] || val[
|
|
90
|
+
const value = extractValue(secret, val[target] || val["@@"] || "");
|
|
90
91
|
|
|
91
92
|
if (value === null) return acc;
|
|
92
93
|
|
|
93
94
|
acc[key] = Array.isArray(value)
|
|
94
|
-
? value
|
|
95
|
+
? value
|
|
96
|
+
.map((v) =>
|
|
97
|
+
Object.prototype.hasOwnProperty.call(vars, v) ? vars[v] : v,
|
|
98
|
+
)
|
|
99
|
+
.join("")
|
|
95
100
|
: value;
|
|
96
101
|
return acc;
|
|
97
102
|
}, {});
|
|
@@ -105,6 +110,6 @@ function parseOverrides(secret, target, overrides, vars) {
|
|
|
105
110
|
*/
|
|
106
111
|
function extractValue(secret, value) {
|
|
107
112
|
if (!isEncrypted(value)) return value;
|
|
108
|
-
if (!secret) return
|
|
113
|
+
if (!secret) return "";
|
|
109
114
|
return decrypt(value, secret);
|
|
110
115
|
}
|