@willbooster/wb 14.1.0 → 15.0.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/dotenv.js +133 -23
- package/dist/index.js +38 -31
- package/package.json +1 -1
package/bin/dotenv.js
CHANGED
|
@@ -39,14 +39,12 @@ export function runDotenvCommand(args) {
|
|
|
39
39
|
stdio: 'inherit',
|
|
40
40
|
});
|
|
41
41
|
const signalHandlers = new Map();
|
|
42
|
-
let forwardedShutdownSignal;
|
|
43
42
|
child.on('error', (error) => {
|
|
44
43
|
console.error(error);
|
|
45
44
|
process.exit(1);
|
|
46
45
|
});
|
|
47
46
|
for (const signal of shutdownSignals) {
|
|
48
47
|
const signalHandler = () => {
|
|
49
|
-
forwardedShutdownSignal = signal;
|
|
50
48
|
child.kill(signal);
|
|
51
49
|
};
|
|
52
50
|
signalHandlers.set(signal, signalHandler);
|
|
@@ -56,10 +54,9 @@ export function runDotenvCommand(args) {
|
|
|
56
54
|
for (const [shutdownSignal, signalHandler] of signalHandlers) {
|
|
57
55
|
process.off(shutdownSignal, signalHandler);
|
|
58
56
|
}
|
|
59
|
-
if (signal && signal === forwardedShutdownSignal) {
|
|
60
|
-
process.exit(0);
|
|
61
|
-
}
|
|
62
57
|
if (signal) {
|
|
58
|
+
// Re-raise even for forwarded shutdown signals so callers observe the conventional
|
|
59
|
+
// signal exit status (e.g. 130 for SIGINT) instead of a misleading success.
|
|
63
60
|
process.kill(process.pid, signal);
|
|
64
61
|
return;
|
|
65
62
|
}
|
|
@@ -67,59 +64,172 @@ export function runDotenvCommand(args) {
|
|
|
67
64
|
});
|
|
68
65
|
}
|
|
69
66
|
|
|
67
|
+
// Mirrors src/commands/dotenv.ts (validateStandardWbEnv) for this startup fast path.
|
|
68
|
+
function validateStandardWbEnv(value, fixTarget) {
|
|
69
|
+
if (
|
|
70
|
+
!value ||
|
|
71
|
+
['development', 'test', 'staging', 'production'].includes(value) ||
|
|
72
|
+
process.env.WB_SKIP_ENV_CHECK === '1' ||
|
|
73
|
+
process.env.WB_SKIP_ENV_CHECK === 'true'
|
|
74
|
+
) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.error(
|
|
78
|
+
`WB_ENV must be one of development, test, staging, or production, but is "${value}". ` +
|
|
79
|
+
`Fix ${fixTarget}, or set WB_SKIP_ENV_CHECK=1 to skip this check.`
|
|
80
|
+
);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Mirrors src/commands/dotenv.ts (readAndApplyEnvironmentVariables) for this startup fast path.
|
|
70
85
|
function readAndApplyEnvironmentVariables(cwd) {
|
|
86
|
+
const mode = process.env.WB_ENV;
|
|
87
|
+
// Mode-specific sources drive the forced-mode override below.
|
|
88
|
+
const modeVars = mode
|
|
89
|
+
? { ...readEnvFile(path.join(cwd, `.env.${mode}`)), ...readEnvFile(path.join(cwd, `.env.${mode}.local`)) }
|
|
90
|
+
: {};
|
|
91
|
+
// Mirror the shared cascade's precedence: .env.<mode>.local > .env.local > .env.<mode> > .env.
|
|
71
92
|
const dotenvVars = {
|
|
72
93
|
...readEnvFile(path.join(cwd, '.env')),
|
|
73
|
-
...(
|
|
94
|
+
...(mode ? readEnvFile(path.join(cwd, `.env.${mode}`)) : {}),
|
|
95
|
+
...readEnvFile(path.join(cwd, '.env.local')),
|
|
96
|
+
...(mode ? readEnvFile(path.join(cwd, `.env.${mode}.local`)) : {}),
|
|
74
97
|
};
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
// WB_ENV in process.env means the mode is explicitly forced, so values from the mode's own
|
|
99
|
+
// sources (.env.<mode>[.local] and the fnox profile) win over variables inherited from the
|
|
100
|
+
// parent shell — except on CI, where injected env vars must keep overriding committed files
|
|
101
|
+
// (see https://github.com/WillBooster/shared/issues/930).
|
|
102
|
+
const modeFileOverridesProcessEnv = !!mode && !isCI(process.env.CI);
|
|
103
|
+
// fnox.toml is the committed, encrypted equivalent of .env files; local .env files still win over it.
|
|
104
|
+
const fnoxVars = readFnoxEnvironmentVariables(cwd, mode, dotenvVars, modeFileOverridesProcessEnv);
|
|
105
|
+
const parsed = { ...fnoxVars, ...dotenvVars };
|
|
106
|
+
// Expand ${...} references against exported variables whose FILE value loses to the shell
|
|
107
|
+
// (mirroring readEnvironmentVariables' effective-value semantics): a reference must resolve to
|
|
108
|
+
// the value the child will actually see, so a shell-shadowed key expands to the shell value
|
|
109
|
+
// while a winning file/override value expands to the file value.
|
|
110
|
+
const fileValueWins = (key) =>
|
|
111
|
+
!(key in process.env) || (modeFileOverridesProcessEnv && (key in modeVars || key in fnoxVars));
|
|
112
|
+
const referenceEnv = {};
|
|
113
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
114
|
+
// Escape dollar signs so exported values substitute literally (pa$word stays pa$word).
|
|
115
|
+
if (value !== undefined && !(key in parsed && fileValueWins(key))) {
|
|
116
|
+
referenceEnv[key] = value.replaceAll('$', String.raw`\$`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const envVars = expand({ parsed, processEnv: referenceEnv }).parsed ?? parsed;
|
|
79
120
|
for (const [key, value] of Object.entries(envVars)) {
|
|
80
121
|
if (!(key in process.env)) {
|
|
81
122
|
process.env[key] = value;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// readFnoxEnvironmentVariables returns a process.env-shadowed key only when the forced
|
|
126
|
+
// profile overrides it, so fnox-provided keys count as mode-specific here.
|
|
127
|
+
if (!(key in modeVars || key in fnoxVars) || process.env[key] === value) continue;
|
|
128
|
+
|
|
129
|
+
if (modeFileOverridesProcessEnv) {
|
|
130
|
+
console.warn(
|
|
131
|
+
`Warning: ${key} in the "${mode}" mode's env sources overrides the value inherited from the parent environment because WB_ENV is explicitly set.`
|
|
132
|
+
);
|
|
133
|
+
process.env[key] = value;
|
|
82
134
|
}
|
|
135
|
+
// On CI, inherited variables intentionally win; no warning is emitted.
|
|
136
|
+
}
|
|
137
|
+
// Validate only AFTER applying the sources, so a WB_SKIP_ENV_CHECK defined in an env FILE is
|
|
138
|
+
// honored: both the captured exported mode (it selected the cascade) and the FINAL value the
|
|
139
|
+
// child will see.
|
|
140
|
+
validateStandardWbEnv(mode, 'the exported variable');
|
|
141
|
+
validateStandardWbEnv(process.env.WB_ENV, 'the env source or the exported variable');
|
|
142
|
+
// The exported mode selected the cascade, so a mode file silently replacing it with a DIFFERENT
|
|
143
|
+
// valid mode (e.g. `.env.production` containing `WB_ENV=development`) must be rejected as well.
|
|
144
|
+
if (
|
|
145
|
+
mode &&
|
|
146
|
+
process.env.WB_ENV &&
|
|
147
|
+
process.env.WB_ENV !== mode &&
|
|
148
|
+
process.env.WB_SKIP_ENV_CHECK !== '1' &&
|
|
149
|
+
process.env.WB_SKIP_ENV_CHECK !== 'true'
|
|
150
|
+
) {
|
|
151
|
+
console.error(
|
|
152
|
+
`WB_ENV resolves to "${process.env.WB_ENV}" although the "${mode}" environment was selected. ` +
|
|
153
|
+
`Fix the WB_ENV defined in the mode's env sources, or set WB_SKIP_ENV_CHECK=1 to skip this check.`
|
|
154
|
+
);
|
|
155
|
+
process.exit(1);
|
|
83
156
|
}
|
|
84
157
|
}
|
|
85
158
|
|
|
86
159
|
// Mirrors readFnoxEnvironmentVariables in @willbooster/shared-lib-node for this startup fast path.
|
|
87
|
-
function readFnoxEnvironmentVariables(cwd, cascade, currentEnvVars) {
|
|
160
|
+
function readFnoxEnvironmentVariables(cwd, cascade, currentEnvVars, modeFileOverridesProcessEnv) {
|
|
88
161
|
if (!hasProjectFnoxConfig(cwd)) return {};
|
|
89
162
|
|
|
163
|
+
const secrets = runFnoxExport(cwd, cascade, { quiet: false });
|
|
164
|
+
if (!secrets) return {};
|
|
165
|
+
// A key is profile-specific (and may override process.env off CI) when the profile export's
|
|
166
|
+
// value differs from the base export's; when the base export fails, no override is applied.
|
|
167
|
+
// The base export runs lazily, only when a process.env collision needs adjudicating.
|
|
168
|
+
let cachedBaseSecrets = false;
|
|
169
|
+
const getBaseSecrets = () => {
|
|
170
|
+
if (cachedBaseSecrets === false) {
|
|
171
|
+
cachedBaseSecrets = runFnoxExport(cwd, undefined, { quiet: true, ignoreProfileEnvVar: true });
|
|
172
|
+
}
|
|
173
|
+
return cachedBaseSecrets;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const envVars = {};
|
|
177
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
178
|
+
if (typeof value !== 'string' || key in currentEnvVars) continue;
|
|
179
|
+
if (key in process.env) {
|
|
180
|
+
const baseSecrets = modeFileOverridesProcessEnv && cascade ? getBaseSecrets() : undefined;
|
|
181
|
+
const overridesProcessEnv = baseSecrets !== undefined && baseSecrets[key] !== value;
|
|
182
|
+
if (!overridesProcessEnv) continue;
|
|
183
|
+
}
|
|
184
|
+
envVars[key] = value;
|
|
185
|
+
}
|
|
186
|
+
return envVars;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function runFnoxExport(cwd, cascade, options) {
|
|
90
190
|
// `--if-missing error`: fnox otherwise exits 0 and silently omits secrets it fails to resolve.
|
|
91
191
|
// `--non-interactive`: prompts or browser auth flows would hang forever because stdin is ignored.
|
|
92
192
|
const args = ['export', '--format', 'json', '--no-color', '--if-missing', 'error', '--non-interactive'];
|
|
193
|
+
const env = { ...process.env };
|
|
93
194
|
if (cascade) {
|
|
94
195
|
args.push('--profile', cascade);
|
|
95
196
|
}
|
|
197
|
+
if (options.ignoreProfileEnvVar) {
|
|
198
|
+
// Without `--profile`, fnox falls back to FNOX_PROFILE; the base-adjudication export must
|
|
199
|
+
// read the BASE secrets, so the inherited profile selection is cleared for it — and only for
|
|
200
|
+
// it: a profile-less PRIMARY export (e.g. `wb dotenv` without WB_ENV) keeps honoring
|
|
201
|
+
// FNOX_PROFILE.
|
|
202
|
+
delete env.FNOX_PROFILE;
|
|
203
|
+
}
|
|
96
204
|
const result = childProcess.spawnSync('fnox', args, {
|
|
97
205
|
cwd,
|
|
206
|
+
env,
|
|
98
207
|
encoding: 'utf8',
|
|
99
208
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
209
|
});
|
|
101
210
|
if (result.error || result.status !== 0 || !result.stdout?.trim()) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
211
|
+
if (!options.quiet) {
|
|
212
|
+
console.warn(
|
|
213
|
+
`Failed to read fnox secrets: ${result.error?.message || result.stderr?.trim() || `fnox exited with status ${result.status}`}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
106
217
|
}
|
|
107
218
|
|
|
108
219
|
let parsed;
|
|
109
220
|
try {
|
|
110
221
|
parsed = JSON.parse(result.stdout);
|
|
111
222
|
} catch {
|
|
112
|
-
return
|
|
223
|
+
return;
|
|
113
224
|
}
|
|
114
225
|
const secrets = parsed?.secrets;
|
|
115
|
-
if (!secrets || typeof secrets !== 'object' || Array.isArray(secrets)) return
|
|
226
|
+
if (!secrets || typeof secrets !== 'object' || Array.isArray(secrets)) return;
|
|
227
|
+
return secrets;
|
|
228
|
+
}
|
|
116
229
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
envVars[key] = value;
|
|
121
|
-
}
|
|
122
|
-
return envVars;
|
|
230
|
+
// Mirrors src/utils/ci.ts for this startup fast path.
|
|
231
|
+
function isCI(ciEnv) {
|
|
232
|
+
return !!ciEnv && ciEnv !== '0' && ciEnv !== 'false';
|
|
123
233
|
}
|
|
124
234
|
|
|
125
235
|
function hasProjectFnoxConfig(cwd) {
|