@karmaniverous/get-dotenv 5.2.5 → 6.0.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/README.md +63 -67
- package/dist/cliHost.cjs +765 -549
- package/dist/cliHost.d.cts +128 -84
- package/dist/cliHost.d.mts +128 -84
- package/dist/cliHost.d.ts +128 -84
- package/dist/cliHost.mjs +765 -549
- package/dist/getdotenv.cli.mjs +915 -685
- package/dist/index.cjs +959 -1006
- package/dist/index.d.cts +18 -178
- package/dist/index.d.mts +18 -178
- package/dist/index.d.ts +18 -178
- package/dist/index.mjs +960 -1006
- package/dist/plugins-aws.cjs +0 -1
- package/dist/plugins-aws.d.cts +8 -78
- package/dist/plugins-aws.d.mts +8 -78
- package/dist/plugins-aws.d.ts +8 -78
- package/dist/plugins-aws.mjs +0 -1
- package/dist/plugins-batch.cjs +53 -11
- package/dist/plugins-batch.d.cts +10 -79
- package/dist/plugins-batch.d.mts +10 -79
- package/dist/plugins-batch.d.ts +10 -79
- package/dist/plugins-batch.mjs +53 -11
- package/dist/plugins-cmd.cjs +162 -1555
- package/dist/plugins-cmd.d.cts +8 -78
- package/dist/plugins-cmd.d.mts +8 -78
- package/dist/plugins-cmd.d.ts +8 -78
- package/dist/plugins-cmd.mjs +162 -1554
- package/dist/plugins-demo.cjs +52 -7
- package/dist/plugins-demo.d.cts +8 -78
- package/dist/plugins-demo.d.mts +8 -78
- package/dist/plugins-demo.d.ts +8 -78
- package/dist/plugins-demo.mjs +52 -7
- package/dist/plugins-init.d.cts +8 -78
- package/dist/plugins-init.d.mts +8 -78
- package/dist/plugins-init.d.ts +8 -78
- package/dist/plugins.cjs +283 -1630
- package/dist/plugins.d.cts +10 -79
- package/dist/plugins.d.mts +10 -79
- package/dist/plugins.d.ts +10 -79
- package/dist/plugins.mjs +285 -1631
- package/package.json +4 -2
package/dist/plugins.cjs
CHANGED
|
@@ -7,15 +7,9 @@ var globby = require('globby');
|
|
|
7
7
|
var packageDirectory = require('package-directory');
|
|
8
8
|
var path = require('path');
|
|
9
9
|
var fs = require('fs-extra');
|
|
10
|
-
var url = require('url');
|
|
11
|
-
var YAML = require('yaml');
|
|
12
|
-
var nanoid = require('nanoid');
|
|
13
|
-
var dotenv = require('dotenv');
|
|
14
|
-
var crypto = require('crypto');
|
|
15
10
|
var node_process = require('node:process');
|
|
16
11
|
var promises = require('readline/promises');
|
|
17
12
|
|
|
18
|
-
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
19
13
|
// Minimal tokenizer for shell-off execution:
|
|
20
14
|
// Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
21
15
|
const tokenize = (command) => {
|
|
@@ -512,7 +506,6 @@ const awsPlugin = () => definePlugin({
|
|
|
512
506
|
cli
|
|
513
507
|
.ns('aws')
|
|
514
508
|
.description('Establish an AWS session and optionally forward to the AWS CLI')
|
|
515
|
-
.configureHelp({ showGlobalOptions: true })
|
|
516
509
|
.enablePositionalOptions()
|
|
517
510
|
.passThroughOptions()
|
|
518
511
|
.allowUnknownOption(true)
|
|
@@ -703,9 +696,10 @@ const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
|
703
696
|
}
|
|
704
697
|
return { absRootPath, paths };
|
|
705
698
|
};
|
|
706
|
-
const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
699
|
+
const execShellCommandBatch = async ({ command, getDotenvCliOptions, dotenvEnv, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
707
700
|
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
708
|
-
Boolean(getDotenvCliOptions?.capture);
|
|
701
|
+
Boolean(getDotenvCliOptions?.capture);
|
|
702
|
+
// Require a command only when not listing. In list mode, a command is optional.
|
|
709
703
|
if (!command && !list) {
|
|
710
704
|
logger.error(`No command provided. Use --command or --list.`);
|
|
711
705
|
process.exit(0);
|
|
@@ -752,12 +746,25 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
752
746
|
const hasCmd = (typeof command === 'string' && command.length > 0) ||
|
|
753
747
|
(Array.isArray(command) && command.length > 0);
|
|
754
748
|
if (hasCmd) {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
749
|
+
// Compose child env overlay from dotenv (drop undefined) and merged options
|
|
750
|
+
const overlay = {};
|
|
751
|
+
if (dotenvEnv) {
|
|
752
|
+
for (const [k, v] of Object.entries(dotenvEnv)) {
|
|
753
|
+
if (typeof v === 'string')
|
|
754
|
+
overlay[k] = v;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (getDotenvCliOptions !== undefined) {
|
|
758
|
+
try {
|
|
759
|
+
overlay.getDotenvCliOptions = JSON.stringify(getDotenvCliOptions);
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
// best-effort: omit if serialization fails
|
|
763
|
+
}
|
|
764
|
+
}
|
|
758
765
|
await runCommand(command, shell, {
|
|
759
766
|
cwd: path,
|
|
760
|
-
env: buildSpawnEnv(process.env,
|
|
767
|
+
env: buildSpawnEnv(process.env, overlay),
|
|
761
768
|
stdio: capture ? 'pipe' : 'inherit',
|
|
762
769
|
});
|
|
763
770
|
}
|
|
@@ -795,6 +802,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
795
802
|
const ctx = cli.getCtx();
|
|
796
803
|
const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
|
|
797
804
|
const cfg = (cfgRaw || {});
|
|
805
|
+
const dotenvEnv = (ctx?.dotenv ?? {});
|
|
798
806
|
// Resolve batch flags from the captured parent (batch) command.
|
|
799
807
|
const raw = batchCmd.opts();
|
|
800
808
|
const listFromParent = !!raw.list;
|
|
@@ -813,6 +821,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
813
821
|
if (typeof commandOpt === 'string') {
|
|
814
822
|
await execShellCommandBatch({
|
|
815
823
|
command: resolveCommand(scripts, commandOpt),
|
|
824
|
+
dotenvEnv,
|
|
816
825
|
globs,
|
|
817
826
|
ignoreErrors,
|
|
818
827
|
list: false,
|
|
@@ -824,6 +833,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
824
833
|
return;
|
|
825
834
|
}
|
|
826
835
|
if (raw.list || localList) {
|
|
836
|
+
const shellBag = ((batchCmd.parent ?? undefined)?.getDotenvCliOptions ?? {});
|
|
827
837
|
await execShellCommandBatch({
|
|
828
838
|
globs,
|
|
829
839
|
ignoreErrors,
|
|
@@ -831,7 +841,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
831
841
|
logger: loggerLocal,
|
|
832
842
|
...(pkgCwd ? { pkgCwd } : {}),
|
|
833
843
|
rootPath,
|
|
834
|
-
shell: (shell ?? false),
|
|
844
|
+
shell: (shell ?? shellBag.shell ?? false),
|
|
835
845
|
});
|
|
836
846
|
return;
|
|
837
847
|
}
|
|
@@ -898,6 +908,7 @@ const buildDefaultCmdAction = (cli, batchCmd, opts) => async (commandParts, _sub
|
|
|
898
908
|
}
|
|
899
909
|
await execShellCommandBatch({
|
|
900
910
|
command: commandArg,
|
|
911
|
+
dotenvEnv,
|
|
901
912
|
...(envBag ? { getDotenvCliOptions: envBag } : {}),
|
|
902
913
|
globs,
|
|
903
914
|
ignoreErrors,
|
|
@@ -916,6 +927,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
916
927
|
const logger = opts.logger ?? console;
|
|
917
928
|
// Ensure context exists (host preSubcommand on root creates if missing).
|
|
918
929
|
const ctx = cli.getCtx();
|
|
930
|
+
const dotenvEnv = (ctx?.dotenv ?? {});
|
|
919
931
|
const cfgRaw = (ctx?.pluginConfigs?.['batch'] ?? {});
|
|
920
932
|
const cfg = (cfgRaw || {});
|
|
921
933
|
const raw = thisCommand.opts();
|
|
@@ -938,6 +950,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
938
950
|
const commandArg = resolved;
|
|
939
951
|
await execShellCommandBatch({
|
|
940
952
|
command: commandArg,
|
|
953
|
+
dotenvEnv,
|
|
941
954
|
globs,
|
|
942
955
|
ignoreErrors,
|
|
943
956
|
list: false,
|
|
@@ -975,6 +988,7 @@ const buildParentAction = (cli, opts) => async (commandParts, thisCommand) => {
|
|
|
975
988
|
const shellOpt = opts.shell ?? cfg.shell ?? mergedBag.shell;
|
|
976
989
|
await execShellCommandBatch({
|
|
977
990
|
command: resolveCommand(scriptsOpt, commandOpt),
|
|
991
|
+
dotenvEnv,
|
|
978
992
|
globs,
|
|
979
993
|
ignoreErrors,
|
|
980
994
|
list,
|
|
@@ -1018,7 +1032,8 @@ const BatchConfigSchema = zod.z.object({
|
|
|
1018
1032
|
/**
|
|
1019
1033
|
* Batch plugin for the GetDotenv CLI host.
|
|
1020
1034
|
*
|
|
1021
|
-
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
1035
|
+
* Mirrors the legacy batch subcommand behavior without altering the shipped CLI.
|
|
1036
|
+
* Options:
|
|
1022
1037
|
* - scripts/shell: used to resolve command and shell behavior per script or global default.
|
|
1023
1038
|
* - logger: defaults to console.
|
|
1024
1039
|
*/
|
|
@@ -1030,12 +1045,32 @@ const batchPlugin = (opts = {}) => definePlugin({
|
|
|
1030
1045
|
setup(cli) {
|
|
1031
1046
|
const ns = cli.ns('batch');
|
|
1032
1047
|
const batchCmd = ns; // capture the parent "batch" command for default-subcommand context
|
|
1048
|
+
const host = cli;
|
|
1049
|
+
const pluginId = 'batch';
|
|
1050
|
+
const GROUP = `plugin:${pluginId}`;
|
|
1033
1051
|
ns.description('Batch command execution across multiple working directories.')
|
|
1034
1052
|
.enablePositionalOptions()
|
|
1035
1053
|
.passThroughOptions()
|
|
1036
|
-
|
|
1037
|
-
.
|
|
1038
|
-
.
|
|
1054
|
+
// Dynamic help: show effective defaults from the merged/interpolated plugin config slice.
|
|
1055
|
+
.addOption((() => {
|
|
1056
|
+
const opt = host.createDynamicOption('-p, --pkg-cwd', (cfg) => {
|
|
1057
|
+
const slice = cfg.plugins.batch ?? {};
|
|
1058
|
+
const on = !!slice.pkgCwd;
|
|
1059
|
+
return `use nearest package directory as current working directory${on ? ' (default)' : ''}`;
|
|
1060
|
+
});
|
|
1061
|
+
opt.__group = GROUP;
|
|
1062
|
+
return opt;
|
|
1063
|
+
})())
|
|
1064
|
+
.addOption((() => {
|
|
1065
|
+
const opt = host.createDynamicOption('-r, --root-path <string>', (cfg) => `path to batch root directory from current working directory (default: ${JSON.stringify(cfg.plugins.batch?.rootPath || './')})`);
|
|
1066
|
+
opt.__group = GROUP;
|
|
1067
|
+
return opt;
|
|
1068
|
+
})())
|
|
1069
|
+
.addOption((() => {
|
|
1070
|
+
const opt = host.createDynamicOption('-g, --globs <string>', (cfg) => `space-delimited globs from root path (default: ${JSON.stringify(cfg.plugins.batch?.globs || '*')})`);
|
|
1071
|
+
opt.__group = GROUP;
|
|
1072
|
+
return opt;
|
|
1073
|
+
})())
|
|
1039
1074
|
.option('-c, --command <string>', 'command executed according to the base shell resolution')
|
|
1040
1075
|
.option('-l, --list', 'list working directories without executing command')
|
|
1041
1076
|
.option('-e, --ignore-errors', 'ignore errors and continue with next path')
|
|
@@ -1109,19 +1144,6 @@ const DEFAULT_PATTERNS = [
|
|
|
1109
1144
|
const compile = (patterns) => (patterns && patterns.length > 0 ? patterns : DEFAULT_PATTERNS).map((p) => new RegExp(p, 'i'));
|
|
1110
1145
|
const shouldRedactKey = (key, regs) => regs.some((re) => re.test(key));
|
|
1111
1146
|
const MASK = '[redacted]';
|
|
1112
|
-
/**
|
|
1113
|
-
* Produce a shallow redacted copy of an env-like object for display.
|
|
1114
|
-
*/
|
|
1115
|
-
const redactObject = (obj, opts) => {
|
|
1116
|
-
if (!opts?.redact)
|
|
1117
|
-
return { ...obj };
|
|
1118
|
-
const regs = compile(opts.redactPatterns);
|
|
1119
|
-
const out = {};
|
|
1120
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
1121
|
-
out[k] = v && shouldRedactKey(k, regs) ? MASK : v;
|
|
1122
|
-
}
|
|
1123
|
-
return out;
|
|
1124
|
-
};
|
|
1125
1147
|
/**
|
|
1126
1148
|
* Utility to redact three related displayed values (parent/dotenv/final)
|
|
1127
1149
|
* consistently for trace lines.
|
|
@@ -1144,1526 +1166,73 @@ const redactTriple = (key, triple, opts) => {
|
|
|
1144
1166
|
return out;
|
|
1145
1167
|
};
|
|
1146
1168
|
|
|
1147
|
-
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
1148
|
-
const baseRootOptionDefaults = {
|
|
1149
|
-
dotenvToken: '.env',
|
|
1150
|
-
loadProcess: true,
|
|
1151
|
-
logger: console,
|
|
1152
|
-
// Diagnostics defaults
|
|
1153
|
-
warnEntropy: true,
|
|
1154
|
-
entropyThreshold: 3.8,
|
|
1155
|
-
entropyMinLength: 16,
|
|
1156
|
-
entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
|
|
1157
|
-
paths: './',
|
|
1158
|
-
pathsDelimiter: ' ',
|
|
1159
|
-
privateToken: 'local',
|
|
1160
|
-
scripts: {
|
|
1161
|
-
'git-status': {
|
|
1162
|
-
cmd: 'git branch --show-current && git status -s -u',
|
|
1163
|
-
shell: true,
|
|
1164
|
-
},
|
|
1165
|
-
},
|
|
1166
|
-
shell: true,
|
|
1167
|
-
vars: '',
|
|
1168
|
-
varsAssignor: '=',
|
|
1169
|
-
varsDelimiter: ' ',
|
|
1170
|
-
// tri-state flags default to unset unless explicitly provided
|
|
1171
|
-
// (debug/log/exclude* resolved via flag utils)
|
|
1172
|
-
};
|
|
1173
|
-
|
|
1174
|
-
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
1175
|
-
|
|
1176
|
-
/** @internal */
|
|
1177
|
-
const isPlainObject$1 = (value) => value !== null &&
|
|
1178
|
-
typeof value === 'object' &&
|
|
1179
|
-
Object.getPrototypeOf(value) === Object.prototype;
|
|
1180
|
-
const mergeInto = (target, source) => {
|
|
1181
|
-
for (const [key, sVal] of Object.entries(source)) {
|
|
1182
|
-
if (sVal === undefined)
|
|
1183
|
-
continue; // do not overwrite with undefined
|
|
1184
|
-
const tVal = target[key];
|
|
1185
|
-
if (isPlainObject$1(tVal) && isPlainObject$1(sVal)) {
|
|
1186
|
-
target[key] = mergeInto({ ...tVal }, sVal);
|
|
1187
|
-
}
|
|
1188
|
-
else if (isPlainObject$1(sVal)) {
|
|
1189
|
-
target[key] = mergeInto({}, sVal);
|
|
1190
|
-
}
|
|
1191
|
-
else {
|
|
1192
|
-
target[key] = sVal;
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
return target;
|
|
1196
|
-
};
|
|
1197
|
-
/**
|
|
1198
|
-
* Perform a deep defaults-style merge across plain objects. *
|
|
1199
|
-
* - Only merges plain objects (prototype === Object.prototype).
|
|
1200
|
-
* - Arrays and non-objects are replaced, not merged.
|
|
1201
|
-
* - `undefined` values are ignored and do not overwrite prior values.
|
|
1202
|
-
*
|
|
1203
|
-
* @typeParam T - The resulting shape after merging all layers.
|
|
1204
|
-
* @param layers - Zero or more partial layers in ascending precedence order.
|
|
1205
|
-
* @returns The merged object typed as {@link T}.
|
|
1206
|
-
*
|
|
1207
|
-
* @example
|
|
1208
|
-
* defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
|
|
1209
|
-
* =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
|
|
1210
|
-
*/
|
|
1211
|
-
const defaultsDeep = (...layers) => {
|
|
1212
|
-
const result = layers
|
|
1213
|
-
.filter(Boolean)
|
|
1214
|
-
.reduce((acc, layer) => mergeInto(acc, layer), {});
|
|
1215
|
-
return result;
|
|
1216
|
-
};
|
|
1217
|
-
|
|
1218
|
-
// src/GetDotenvOptions.ts
|
|
1219
|
-
const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
1220
|
-
/**
|
|
1221
|
-
* Converts programmatic CLI options to `getDotenv` options. *
|
|
1222
|
-
* @param cliOptions - CLI options. Defaults to `{}`.
|
|
1223
|
-
*
|
|
1224
|
-
* @returns `getDotenv` options.
|
|
1225
|
-
*/
|
|
1226
|
-
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
|
|
1227
|
-
/**
|
|
1228
|
-
* Convert CLI-facing string options into {@link GetDotenvOptions}.
|
|
1229
|
-
*
|
|
1230
|
-
* - Splits {@link GetDotenvCliOptions.paths} using either a delimiter * or a regular expression pattern into a string array. * - Parses {@link GetDotenvCliOptions.vars} as space-separated `KEY=VALUE`
|
|
1231
|
-
* pairs (configurable delimiters) into a {@link ProcessEnv}.
|
|
1232
|
-
* - Drops CLI-only keys that have no programmatic equivalent.
|
|
1233
|
-
*
|
|
1234
|
-
* @remarks
|
|
1235
|
-
* Follows exact-optional semantics by not emitting undefined-valued entries.
|
|
1236
|
-
*/
|
|
1237
|
-
// Drop CLI-only keys (debug/scripts) without relying on Record casts.
|
|
1238
|
-
// Create a shallow copy then delete optional CLI-only keys if present.
|
|
1239
|
-
const restObj = { ...rest };
|
|
1240
|
-
delete restObj.debug;
|
|
1241
|
-
delete restObj.scripts;
|
|
1242
|
-
const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
|
|
1243
|
-
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
1244
|
-
let parsedVars;
|
|
1245
|
-
if (typeof vars === 'string') {
|
|
1246
|
-
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
1247
|
-
? RegExp(varsAssignorPattern)
|
|
1248
|
-
: (varsAssignor ?? '=')));
|
|
1249
|
-
parsedVars = Object.fromEntries(kvPairs);
|
|
1250
|
-
}
|
|
1251
|
-
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
1252
|
-
// Keep only string or undefined values to match ProcessEnv.
|
|
1253
|
-
const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
|
|
1254
|
-
parsedVars = Object.fromEntries(entries);
|
|
1255
|
-
}
|
|
1256
|
-
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
1257
|
-
// expectations and the compat test assertions.
|
|
1258
|
-
if (parsedVars) {
|
|
1259
|
-
parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
|
|
1260
|
-
}
|
|
1261
|
-
// Tolerate paths as either a delimited string or string[]
|
|
1262
|
-
// Use a locally cast union type to avoid lint warnings about always-falsy conditions
|
|
1263
|
-
// under the RootOptionsShape (which declares paths as string | undefined).
|
|
1264
|
-
const pathsAny = paths;
|
|
1265
|
-
const pathsOut = Array.isArray(pathsAny)
|
|
1266
|
-
? pathsAny.filter((p) => typeof p === 'string')
|
|
1267
|
-
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
1268
|
-
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
1269
|
-
return {
|
|
1270
|
-
...restObj,
|
|
1271
|
-
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
1272
|
-
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
1273
|
-
};
|
|
1274
|
-
};
|
|
1275
|
-
const resolveGetDotenvOptions = async (customOptions) => {
|
|
1276
|
-
/**
|
|
1277
|
-
* Resolve {@link GetDotenvOptions} by layering defaults in ascending precedence:
|
|
1278
|
-
*
|
|
1279
|
-
* 1. Base defaults derived from the CLI generator defaults
|
|
1280
|
-
* ({@link baseGetDotenvCliOptions}).
|
|
1281
|
-
* 2. Local project overrides from a `getdotenv.config.json` in the nearest
|
|
1282
|
-
* package root (if present).
|
|
1283
|
-
* 3. The provided {@link customOptions}.
|
|
1284
|
-
*
|
|
1285
|
-
* The result preserves explicit empty values and drops only `undefined`.
|
|
1286
|
-
*
|
|
1287
|
-
* @returns Fully-resolved {@link GetDotenvOptions}.
|
|
1288
|
-
*
|
|
1289
|
-
* @example
|
|
1290
|
-
* ```ts
|
|
1291
|
-
* const options = await resolveGetDotenvOptions({ env: 'dev' });
|
|
1292
|
-
* ```
|
|
1293
|
-
*/
|
|
1294
|
-
const localPkgDir = await packageDirectory.packageDirectory();
|
|
1295
|
-
const localOptionsPath = localPkgDir
|
|
1296
|
-
? path.join(localPkgDir, getDotenvOptionsFilename)
|
|
1297
|
-
: undefined;
|
|
1298
|
-
const localOptions = (localOptionsPath && (await fs.exists(localOptionsPath))
|
|
1299
|
-
? JSON.parse((await fs.readFile(localOptionsPath)).toString())
|
|
1300
|
-
: {});
|
|
1301
|
-
// Merge order: base < local < custom (custom has highest precedence)
|
|
1302
|
-
const mergedCli = defaultsDeep(baseGetDotenvCliOptions, localOptions);
|
|
1303
|
-
const defaultsFromCli = getDotenvCliOptions2Options(mergedCli);
|
|
1304
|
-
const result = defaultsDeep(defaultsFromCli, customOptions);
|
|
1305
|
-
return {
|
|
1306
|
-
...result, // Keep explicit empty strings/zeros; drop only undefined
|
|
1307
|
-
vars: Object.fromEntries(Object.entries(result.vars ?? {}).filter(([, v]) => v !== undefined)),
|
|
1308
|
-
};
|
|
1309
|
-
};
|
|
1310
|
-
|
|
1311
|
-
/**
|
|
1312
|
-
* Zod schemas for programmatic GetDotenv options.
|
|
1313
|
-
*
|
|
1314
|
-
* NOTE: These schemas are introduced without wiring to avoid behavior changes.
|
|
1315
|
-
* Legacy paths continue to use existing types/logic. The new plugin host will
|
|
1316
|
-
* use these schemas in strict mode; legacy paths will adopt them in warn mode
|
|
1317
|
-
* later per the staged plan.
|
|
1318
|
-
*/
|
|
1319
|
-
// Minimal process env representation: string values or undefined to indicate "unset".
|
|
1320
|
-
const processEnvSchema = zod.z.record(zod.z.string(), zod.z.string().optional());
|
|
1321
|
-
// RAW: all fields optional — undefined means "inherit" from lower layers.
|
|
1322
|
-
const getDotenvOptionsSchemaRaw = zod.z.object({
|
|
1323
|
-
defaultEnv: zod.z.string().optional(),
|
|
1324
|
-
dotenvToken: zod.z.string().optional(),
|
|
1325
|
-
dynamicPath: zod.z.string().optional(),
|
|
1326
|
-
// Dynamic map is intentionally wide for now; refine once sources are normalized.
|
|
1327
|
-
dynamic: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
1328
|
-
env: zod.z.string().optional(),
|
|
1329
|
-
excludeDynamic: zod.z.boolean().optional(),
|
|
1330
|
-
excludeEnv: zod.z.boolean().optional(),
|
|
1331
|
-
excludeGlobal: zod.z.boolean().optional(),
|
|
1332
|
-
excludePrivate: zod.z.boolean().optional(),
|
|
1333
|
-
excludePublic: zod.z.boolean().optional(),
|
|
1334
|
-
loadProcess: zod.z.boolean().optional(),
|
|
1335
|
-
log: zod.z.boolean().optional(),
|
|
1336
|
-
outputPath: zod.z.string().optional(),
|
|
1337
|
-
paths: zod.z.array(zod.z.string()).optional(),
|
|
1338
|
-
privateToken: zod.z.string().optional(),
|
|
1339
|
-
vars: processEnvSchema.optional(),
|
|
1340
|
-
// Host-only feature flag: guarded integration of config loader/overlay
|
|
1341
|
-
useConfigLoader: zod.z.boolean().optional(),
|
|
1342
|
-
});
|
|
1343
|
-
// RESOLVED: service-boundary contract (post-inheritance).
|
|
1344
|
-
// For Step A, keep identical to RAW (no behavior change). Later stages will// materialize required defaults and narrow shapes as resolution is wired.
|
|
1345
|
-
const getDotenvOptionsSchemaResolved = getDotenvOptionsSchemaRaw;
|
|
1346
|
-
|
|
1347
|
-
/**
|
|
1348
|
-
* Zod schemas for configuration files discovered by the new loader.
|
|
1349
|
-
*
|
|
1350
|
-
* Notes:
|
|
1351
|
-
* - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
|
|
1352
|
-
* - RESOLVED: normalized shapes (paths always string[]).
|
|
1353
|
-
* - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
|
|
1354
|
-
*/
|
|
1355
|
-
// String-only env value map
|
|
1356
|
-
const stringMap = zod.z.record(zod.z.string(), zod.z.string());
|
|
1357
|
-
const envStringMap = zod.z.record(zod.z.string(), stringMap);
|
|
1358
|
-
// Allow string[] or single string for "paths" in RAW; normalize later.
|
|
1359
|
-
const rawPathsSchema = zod.z.union([zod.z.array(zod.z.string()), zod.z.string()]).optional();
|
|
1360
|
-
const getDotenvConfigSchemaRaw = zod.z.object({
|
|
1361
|
-
dotenvToken: zod.z.string().optional(),
|
|
1362
|
-
privateToken: zod.z.string().optional(),
|
|
1363
|
-
paths: rawPathsSchema,
|
|
1364
|
-
loadProcess: zod.z.boolean().optional(),
|
|
1365
|
-
log: zod.z.boolean().optional(),
|
|
1366
|
-
shell: zod.z.union([zod.z.string(), zod.z.boolean()]).optional(),
|
|
1367
|
-
scripts: zod.z.record(zod.z.string(), zod.z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
1368
|
-
requiredKeys: zod.z.array(zod.z.string()).optional(),
|
|
1369
|
-
schema: zod.z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
|
|
1370
|
-
vars: stringMap.optional(), // public, global
|
|
1371
|
-
envVars: envStringMap.optional(), // public, per-env
|
|
1372
|
-
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
1373
|
-
dynamic: zod.z.unknown().optional(),
|
|
1374
|
-
// Per-plugin config bag; validated by plugins/host when used.
|
|
1375
|
-
plugins: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
1376
|
-
});
|
|
1377
|
-
// Normalize paths to string[]
|
|
1378
|
-
const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
|
|
1379
|
-
const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => ({
|
|
1380
|
-
...raw,
|
|
1381
|
-
paths: normalizePaths(raw.paths),
|
|
1382
|
-
}));
|
|
1383
|
-
|
|
1384
|
-
// Discovery candidates (first match wins per scope/privacy).
|
|
1385
|
-
// Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
|
|
1386
|
-
const PUBLIC_FILENAMES = [
|
|
1387
|
-
'getdotenv.config.json',
|
|
1388
|
-
'getdotenv.config.yaml',
|
|
1389
|
-
'getdotenv.config.yml',
|
|
1390
|
-
'getdotenv.config.js',
|
|
1391
|
-
'getdotenv.config.mjs',
|
|
1392
|
-
'getdotenv.config.cjs',
|
|
1393
|
-
'getdotenv.config.ts',
|
|
1394
|
-
'getdotenv.config.mts',
|
|
1395
|
-
'getdotenv.config.cts',
|
|
1396
|
-
];
|
|
1397
|
-
const LOCAL_FILENAMES = [
|
|
1398
|
-
'getdotenv.config.local.json',
|
|
1399
|
-
'getdotenv.config.local.yaml',
|
|
1400
|
-
'getdotenv.config.local.yml',
|
|
1401
|
-
'getdotenv.config.local.js',
|
|
1402
|
-
'getdotenv.config.local.mjs',
|
|
1403
|
-
'getdotenv.config.local.cjs',
|
|
1404
|
-
'getdotenv.config.local.ts',
|
|
1405
|
-
'getdotenv.config.local.mts',
|
|
1406
|
-
'getdotenv.config.local.cts',
|
|
1407
|
-
];
|
|
1408
|
-
const isYaml = (p) => ['.yaml', '.yml'].includes(path.extname(p).toLowerCase());
|
|
1409
|
-
const isJson = (p) => path.extname(p).toLowerCase() === '.json';
|
|
1410
|
-
const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(path.extname(p).toLowerCase());
|
|
1411
|
-
// --- Internal JS/TS module loader helpers (default export) ---
|
|
1412
|
-
const importDefault$1 = async (fileUrl) => {
|
|
1413
|
-
const mod = (await import(fileUrl));
|
|
1414
|
-
return mod.default;
|
|
1415
|
-
};
|
|
1416
|
-
const cacheName = (absPath, suffix) => {
|
|
1417
|
-
// sanitized filename with suffix; recompile on mtime changes not tracked here (simplified)
|
|
1418
|
-
const base = path.basename(absPath).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1419
|
-
return `${base}.${suffix}.mjs`;
|
|
1420
|
-
};
|
|
1421
|
-
const ensureDir$1 = async (dir) => {
|
|
1422
|
-
await fs.ensureDir(dir);
|
|
1423
|
-
return dir;
|
|
1424
|
-
};
|
|
1425
|
-
const loadJsTsDefault = async (absPath) => {
|
|
1426
|
-
const fileUrl = url.pathToFileURL(absPath).toString();
|
|
1427
|
-
const ext = path.extname(absPath).toLowerCase();
|
|
1428
|
-
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
1429
|
-
return importDefault$1(fileUrl);
|
|
1430
|
-
}
|
|
1431
|
-
// Try direct import first in case a TS loader is active.
|
|
1432
|
-
try {
|
|
1433
|
-
const val = await importDefault$1(fileUrl);
|
|
1434
|
-
if (val)
|
|
1435
|
-
return val;
|
|
1436
|
-
}
|
|
1437
|
-
catch {
|
|
1438
|
-
/* fallthrough */
|
|
1439
|
-
}
|
|
1440
|
-
// esbuild bundle to a temp ESM file
|
|
1441
|
-
try {
|
|
1442
|
-
const esbuild = (await import('esbuild'));
|
|
1443
|
-
const outDir = await ensureDir$1(path.resolve('.tsbuild', 'getdotenv-config'));
|
|
1444
|
-
const outfile = path.join(outDir, cacheName(absPath, 'bundle'));
|
|
1445
|
-
await esbuild.build({
|
|
1446
|
-
entryPoints: [absPath],
|
|
1447
|
-
bundle: true,
|
|
1448
|
-
platform: 'node',
|
|
1449
|
-
format: 'esm',
|
|
1450
|
-
target: 'node20',
|
|
1451
|
-
outfile,
|
|
1452
|
-
sourcemap: false,
|
|
1453
|
-
logLevel: 'silent',
|
|
1454
|
-
});
|
|
1455
|
-
return await importDefault$1(url.pathToFileURL(outfile).toString());
|
|
1456
|
-
}
|
|
1457
|
-
catch {
|
|
1458
|
-
/* fallthrough to TS transpile */
|
|
1459
|
-
}
|
|
1460
|
-
// typescript.transpileModule simple transpile (single-file)
|
|
1461
|
-
try {
|
|
1462
|
-
const ts = (await import('typescript'));
|
|
1463
|
-
const src = await fs.readFile(absPath, 'utf-8');
|
|
1464
|
-
const out = ts.transpileModule(src, {
|
|
1465
|
-
compilerOptions: {
|
|
1466
|
-
module: 'ESNext',
|
|
1467
|
-
target: 'ES2022',
|
|
1468
|
-
moduleResolution: 'NodeNext',
|
|
1469
|
-
},
|
|
1470
|
-
}).outputText;
|
|
1471
|
-
const outDir = await ensureDir$1(path.resolve('.tsbuild', 'getdotenv-config'));
|
|
1472
|
-
const outfile = path.join(outDir, cacheName(absPath, 'ts'));
|
|
1473
|
-
await fs.writeFile(outfile, out, 'utf-8');
|
|
1474
|
-
return await importDefault$1(url.pathToFileURL(outfile).toString());
|
|
1475
|
-
}
|
|
1476
|
-
catch {
|
|
1477
|
-
throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
|
|
1478
|
-
}
|
|
1479
|
-
};
|
|
1480
|
-
/**
|
|
1481
|
-
* Discover JSON/YAML config files in the packaged root and project root.
|
|
1482
|
-
* Order: packaged public → project public → project local. */
|
|
1483
|
-
const discoverConfigFiles = async (importMetaUrl) => {
|
|
1484
|
-
const files = [];
|
|
1485
|
-
// Packaged root via importMetaUrl (optional)
|
|
1486
|
-
if (importMetaUrl) {
|
|
1487
|
-
const fromUrl = url.fileURLToPath(importMetaUrl);
|
|
1488
|
-
const packagedRoot = await packageDirectory.packageDirectory({ cwd: fromUrl });
|
|
1489
|
-
if (packagedRoot) {
|
|
1490
|
-
for (const name of PUBLIC_FILENAMES) {
|
|
1491
|
-
const p = path.join(packagedRoot, name);
|
|
1492
|
-
if (await fs.pathExists(p)) {
|
|
1493
|
-
files.push({ path: p, privacy: 'public', scope: 'packaged' });
|
|
1494
|
-
break; // only one public file expected per scope
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
// By policy, packaged .local is not expected; skip even if present.
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
// Project root (from current working directory)
|
|
1501
|
-
const projectRoot = await packageDirectory.packageDirectory();
|
|
1502
|
-
if (projectRoot) {
|
|
1503
|
-
for (const name of PUBLIC_FILENAMES) {
|
|
1504
|
-
const p = path.join(projectRoot, name);
|
|
1505
|
-
if (await fs.pathExists(p)) {
|
|
1506
|
-
files.push({ path: p, privacy: 'public', scope: 'project' });
|
|
1507
|
-
break;
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
for (const name of LOCAL_FILENAMES) {
|
|
1511
|
-
const p = path.join(projectRoot, name);
|
|
1512
|
-
if (await fs.pathExists(p)) {
|
|
1513
|
-
files.push({ path: p, privacy: 'local', scope: 'project' });
|
|
1514
|
-
break;
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
return files;
|
|
1519
|
-
};
|
|
1520
|
-
/**
|
|
1521
|
-
* Load a single config file (JSON/YAML). JS/TS is not supported in this step.
|
|
1522
|
-
* Validates with Zod RAW schema, then normalizes to RESOLVED.
|
|
1523
|
-
*
|
|
1524
|
-
* For JSON/YAML: if a "dynamic" property is present, throws with guidance.
|
|
1525
|
-
* For JS/TS: default export is loaded; "dynamic" is allowed.
|
|
1526
|
-
*/
|
|
1527
|
-
const loadConfigFile = async (filePath) => {
|
|
1528
|
-
let raw = {};
|
|
1529
|
-
try {
|
|
1530
|
-
const abs = path.resolve(filePath);
|
|
1531
|
-
if (isJsOrTs(abs)) {
|
|
1532
|
-
// JS/TS support: load default export via robust pipeline.
|
|
1533
|
-
const mod = await loadJsTsDefault(abs);
|
|
1534
|
-
raw = mod ?? {};
|
|
1535
|
-
}
|
|
1536
|
-
else {
|
|
1537
|
-
const txt = await fs.readFile(abs, 'utf-8');
|
|
1538
|
-
raw = isJson(abs) ? JSON.parse(txt) : isYaml(abs) ? YAML.parse(txt) : {};
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
catch (err) {
|
|
1542
|
-
throw new Error(`Failed to read/parse config: ${filePath}. ${String(err)}`);
|
|
1543
|
-
}
|
|
1544
|
-
// Validate RAW
|
|
1545
|
-
const parsed = getDotenvConfigSchemaRaw.safeParse(raw);
|
|
1546
|
-
if (!parsed.success) {
|
|
1547
|
-
const msgs = parsed.error.issues
|
|
1548
|
-
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
1549
|
-
.join('\n');
|
|
1550
|
-
throw new Error(`Invalid config ${filePath}:\n${msgs}`);
|
|
1551
|
-
}
|
|
1552
|
-
// Disallow dynamic and schema in JSON/YAML; allow both in JS/TS.
|
|
1553
|
-
if (!isJsOrTs(filePath) &&
|
|
1554
|
-
(parsed.data.dynamic !== undefined || parsed.data.schema !== undefined)) {
|
|
1555
|
-
throw new Error(`Config ${filePath} specifies unsupported keys for JSON/YAML. ` +
|
|
1556
|
-
`Use JS/TS config for "dynamic" or "schema".`);
|
|
1557
|
-
}
|
|
1558
|
-
return getDotenvConfigSchemaResolved.parse(parsed.data);
|
|
1559
|
-
};
|
|
1560
|
-
/**
|
|
1561
|
-
* Discover and load configs into resolved shapes, ordered by scope/privacy.
|
|
1562
|
-
* JSON/YAML/JS/TS supported; first match per scope/privacy applies.
|
|
1563
|
-
*/
|
|
1564
|
-
const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
1565
|
-
const discovered = await discoverConfigFiles(importMetaUrl);
|
|
1566
|
-
const result = {};
|
|
1567
|
-
for (const f of discovered) {
|
|
1568
|
-
const cfg = await loadConfigFile(f.path);
|
|
1569
|
-
if (f.scope === 'packaged') {
|
|
1570
|
-
// packaged public only
|
|
1571
|
-
result.packaged = cfg;
|
|
1572
|
-
}
|
|
1573
|
-
else {
|
|
1574
|
-
result.project ??= {};
|
|
1575
|
-
if (f.privacy === 'public')
|
|
1576
|
-
result.project.public = cfg;
|
|
1577
|
-
else
|
|
1578
|
-
result.project.local = cfg;
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
return result;
|
|
1582
|
-
};
|
|
1583
|
-
|
|
1584
|
-
/**
|
|
1585
|
-
* Dotenv expansion utilities.
|
|
1586
|
-
*
|
|
1587
|
-
* This module implements recursive expansion of environment-variable
|
|
1588
|
-
* references in strings and records. It supports both whitespace and
|
|
1589
|
-
* bracket syntaxes with optional defaults:
|
|
1590
|
-
*
|
|
1591
|
-
* - Whitespace: `$VAR[:default]`
|
|
1592
|
-
* - Bracketed: `${VAR[:default]}`
|
|
1593
|
-
*
|
|
1594
|
-
* Escaped dollar signs (`\$`) are preserved.
|
|
1595
|
-
* Unknown variables resolve to empty string unless a default is provided.
|
|
1596
|
-
*/
|
|
1597
|
-
/**
|
|
1598
|
-
* Like String.prototype.search but returns the last index.
|
|
1599
|
-
* @internal
|
|
1600
|
-
*/
|
|
1601
|
-
const searchLast = (str, rgx) => {
|
|
1602
|
-
const matches = Array.from(str.matchAll(rgx));
|
|
1603
|
-
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
1604
|
-
};
|
|
1605
|
-
const replaceMatch = (value, match, ref) => {
|
|
1606
|
-
/**
|
|
1607
|
-
* @internal
|
|
1608
|
-
*/
|
|
1609
|
-
const group = match[0];
|
|
1610
|
-
const key = match[1];
|
|
1611
|
-
const defaultValue = match[2];
|
|
1612
|
-
if (!key)
|
|
1613
|
-
return value;
|
|
1614
|
-
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
1615
|
-
return interpolate(replacement, ref);
|
|
1616
|
-
};
|
|
1617
|
-
const interpolate = (value = '', ref = {}) => {
|
|
1618
|
-
/**
|
|
1619
|
-
* @internal
|
|
1620
|
-
*/
|
|
1621
|
-
// if value is falsy, return it as is
|
|
1622
|
-
if (!value)
|
|
1623
|
-
return value;
|
|
1624
|
-
// get position of last unescaped dollar sign
|
|
1625
|
-
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
1626
|
-
// return value if none found
|
|
1627
|
-
if (lastUnescapedDollarSignIndex === -1)
|
|
1628
|
-
return value;
|
|
1629
|
-
// evaluate the value tail
|
|
1630
|
-
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
1631
|
-
// find whitespace pattern: $KEY:DEFAULT
|
|
1632
|
-
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
1633
|
-
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
1634
|
-
if (whitespaceMatch != null)
|
|
1635
|
-
return replaceMatch(value, whitespaceMatch, ref);
|
|
1636
|
-
else {
|
|
1637
|
-
// find bracket pattern: ${KEY:DEFAULT}
|
|
1638
|
-
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
1639
|
-
const bracketMatch = bracketPattern.exec(tail);
|
|
1640
|
-
if (bracketMatch != null)
|
|
1641
|
-
return replaceMatch(value, bracketMatch, ref);
|
|
1642
|
-
}
|
|
1643
|
-
return value;
|
|
1644
|
-
};
|
|
1645
|
-
/**
|
|
1646
|
-
* Recursively expands environment variables in a string. Variables may be
|
|
1647
|
-
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
1648
|
-
* Unknown variables will expand to an empty string.
|
|
1649
|
-
*
|
|
1650
|
-
* @param value - The string to expand.
|
|
1651
|
-
* @param ref - The reference object to use for variable expansion.
|
|
1652
|
-
* @returns The expanded string.
|
|
1653
|
-
*
|
|
1654
|
-
* @example
|
|
1655
|
-
* ```ts
|
|
1656
|
-
* process.env.FOO = 'bar';
|
|
1657
|
-
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
1658
|
-
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
1659
|
-
* ```
|
|
1660
|
-
*
|
|
1661
|
-
* @remarks
|
|
1662
|
-
* The expansion is recursive. If a referenced variable itself contains
|
|
1663
|
-
* references, those will also be expanded until a stable value is reached.
|
|
1664
|
-
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
1665
|
-
*/
|
|
1666
|
-
const dotenvExpand = (value, ref = process.env) => {
|
|
1667
|
-
const result = interpolate(value, ref);
|
|
1668
|
-
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
1669
|
-
};
|
|
1670
|
-
/**
|
|
1671
|
-
* Recursively expands environment variables in the values of a JSON object.
|
|
1672
|
-
* Variables may be presented with optional default as `$VAR[:default]` or
|
|
1673
|
-
* `${VAR[:default]}`. Unknown variables will expand to an empty string.
|
|
1674
|
-
*
|
|
1675
|
-
* @param values - The values object to expand.
|
|
1676
|
-
* @param options - Expansion options.
|
|
1677
|
-
* @returns The value object with expanded string values.
|
|
1678
|
-
*
|
|
1679
|
-
* @example
|
|
1680
|
-
* ```ts
|
|
1681
|
-
* process.env.FOO = 'bar';
|
|
1682
|
-
* dotenvExpandAll({ A: '$FOO', B: 'x${FOO}y' });
|
|
1683
|
-
* // => { A: "bar", B: "xbary" }
|
|
1684
|
-
* ```
|
|
1685
|
-
*
|
|
1686
|
-
* @remarks
|
|
1687
|
-
* Options:
|
|
1688
|
-
* - ref: The reference object to use for expansion (defaults to process.env).
|
|
1689
|
-
* - progressive: Whether to progressively add expanded values to the set of
|
|
1690
|
-
* reference keys.
|
|
1691
|
-
*
|
|
1692
|
-
* When `progressive` is true, each expanded key becomes available for
|
|
1693
|
-
* subsequent expansions in the same object (left-to-right by object key order).
|
|
1694
|
-
*/
|
|
1695
|
-
const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
|
|
1696
|
-
const { ref = process.env, progressive = false } = options;
|
|
1697
|
-
acc[key] = dotenvExpand(values[key], {
|
|
1698
|
-
...ref,
|
|
1699
|
-
...(progressive ? acc : {}),
|
|
1700
|
-
});
|
|
1701
|
-
return acc;
|
|
1702
|
-
}, {});
|
|
1703
|
-
/**
|
|
1704
|
-
* Recursively expands environment variables in a string using `process.env` as
|
|
1705
|
-
* the expansion reference. Variables may be presented with optional default as
|
|
1706
|
-
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
1707
|
-
* empty string.
|
|
1708
|
-
*
|
|
1709
|
-
* @param value - The string to expand.
|
|
1710
|
-
* @returns The expanded string.
|
|
1711
|
-
*
|
|
1712
|
-
* @example
|
|
1713
|
-
* ```ts
|
|
1714
|
-
* process.env.FOO = 'bar';
|
|
1715
|
-
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
1716
|
-
* ```
|
|
1717
|
-
*/
|
|
1718
|
-
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
1719
|
-
|
|
1720
|
-
const applyKv = (current, kv) => {
|
|
1721
|
-
if (!kv || Object.keys(kv).length === 0)
|
|
1722
|
-
return current;
|
|
1723
|
-
const expanded = dotenvExpandAll(kv, { ref: current, progressive: true });
|
|
1724
|
-
return { ...current, ...expanded };
|
|
1725
|
-
};
|
|
1726
|
-
const applyConfigSlice = (current, cfg, env) => {
|
|
1727
|
-
if (!cfg)
|
|
1728
|
-
return current;
|
|
1729
|
-
// kind axis: global then env (env overrides global)
|
|
1730
|
-
const afterGlobal = applyKv(current, cfg.vars);
|
|
1731
|
-
const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
|
|
1732
|
-
return applyKv(afterGlobal, envKv);
|
|
1733
|
-
};
|
|
1734
|
-
/**
|
|
1735
|
-
* Overlay config-provided values onto a base ProcessEnv using precedence axes:
|
|
1736
|
-
* - kind: env \> global
|
|
1737
|
-
* - privacy: local \> public
|
|
1738
|
-
* - source: project \> packaged \> base
|
|
1739
|
-
*
|
|
1740
|
-
* Programmatic explicit vars (if provided) override all config slices.
|
|
1741
|
-
* Progressive expansion is applied within each slice.
|
|
1742
|
-
*/
|
|
1743
|
-
const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
1744
|
-
let current = { ...base };
|
|
1745
|
-
// Source: packaged (public -> local)
|
|
1746
|
-
current = applyConfigSlice(current, configs.packaged, env);
|
|
1747
|
-
// Packaged "local" is not expected by policy; if present, honor it.
|
|
1748
|
-
// We do not have a separate object for packaged.local in sources, keep as-is.
|
|
1749
|
-
// Source: project (public -> local)
|
|
1750
|
-
current = applyConfigSlice(current, configs.project?.public, env);
|
|
1751
|
-
current = applyConfigSlice(current, configs.project?.local, env);
|
|
1752
|
-
// Programmatic explicit vars (top of static tier)
|
|
1753
|
-
if (programmaticVars) {
|
|
1754
|
-
const toApply = Object.fromEntries(Object.entries(programmaticVars).filter(([_k, v]) => typeof v === 'string'));
|
|
1755
|
-
current = applyKv(current, toApply);
|
|
1756
|
-
}
|
|
1757
|
-
return current;
|
|
1758
|
-
};
|
|
1759
|
-
|
|
1760
|
-
/**
|
|
1761
|
-
* Asynchronously read a dotenv file & parse it into an object.
|
|
1762
|
-
*
|
|
1763
|
-
* @param path - Path to dotenv file.
|
|
1764
|
-
* @returns The parsed dotenv object.
|
|
1765
|
-
*/
|
|
1766
|
-
const readDotenv = async (path) => {
|
|
1767
|
-
try {
|
|
1768
|
-
return (await fs.exists(path)) ? dotenv.parse(await fs.readFile(path)) : {};
|
|
1769
|
-
}
|
|
1770
|
-
catch {
|
|
1771
|
-
return {};
|
|
1772
|
-
}
|
|
1773
|
-
};
|
|
1774
|
-
|
|
1775
|
-
const importDefault = async (fileUrl) => {
|
|
1776
|
-
const mod = (await import(fileUrl));
|
|
1777
|
-
return mod.default;
|
|
1778
|
-
};
|
|
1779
|
-
const cacheHash = (absPath, mtimeMs) => crypto.createHash('sha1')
|
|
1780
|
-
.update(absPath)
|
|
1781
|
-
.update(String(mtimeMs))
|
|
1782
|
-
.digest('hex')
|
|
1783
|
-
.slice(0, 12);
|
|
1784
|
-
/**
|
|
1785
|
-
* Remove older compiled cache files for a given source base name, keeping
|
|
1786
|
-
* at most `keep` most-recent files. Errors are ignored by design.
|
|
1787
|
-
*/
|
|
1788
|
-
const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Number.parseInt(process.env.GETDOTENV_CACHE_KEEP ?? '2'))) => {
|
|
1789
|
-
try {
|
|
1790
|
-
const entries = await fs.readdir(cacheDir);
|
|
1791
|
-
const mine = entries
|
|
1792
|
-
.filter((f) => f.startsWith(`${baseName}.`) && f.endsWith('.mjs'))
|
|
1793
|
-
.map((f) => path.join(cacheDir, f));
|
|
1794
|
-
if (mine.length <= keep)
|
|
1795
|
-
return;
|
|
1796
|
-
const stats = await Promise.all(mine.map(async (p) => ({ p, mtimeMs: (await fs.stat(p)).mtimeMs })));
|
|
1797
|
-
stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1798
|
-
const toDelete = stats.slice(keep).map((s) => s.p);
|
|
1799
|
-
await Promise.all(toDelete.map(async (p) => {
|
|
1800
|
-
try {
|
|
1801
|
-
await fs.remove(p);
|
|
1802
|
-
}
|
|
1803
|
-
catch {
|
|
1804
|
-
// best-effort cleanup
|
|
1805
|
-
}
|
|
1806
|
-
}));
|
|
1807
|
-
}
|
|
1808
|
-
catch {
|
|
1809
|
-
// best-effort cleanup
|
|
1810
|
-
}
|
|
1811
|
-
};
|
|
1812
|
-
/**
|
|
1813
|
-
* Load a module default export from a JS/TS file with robust fallbacks:
|
|
1814
|
-
* - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
|
|
1815
|
-
* 1) try direct import (if a TS loader is active),
|
|
1816
|
-
* 2) esbuild bundle to a temp ESM file,
|
|
1817
|
-
* 3) typescript.transpileModule fallback for simple modules.
|
|
1818
|
-
*
|
|
1819
|
-
* @param absPath - absolute path to source file
|
|
1820
|
-
* @param cacheDirName - cache subfolder under .tsbuild
|
|
1821
|
-
*/
|
|
1822
|
-
const loadModuleDefault = async (absPath, cacheDirName) => {
|
|
1823
|
-
const ext = path.extname(absPath).toLowerCase();
|
|
1824
|
-
const fileUrl = url.pathToFileURL(absPath).toString();
|
|
1825
|
-
if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
|
|
1826
|
-
return importDefault(fileUrl);
|
|
1827
|
-
}
|
|
1828
|
-
// Try direct import first (TS loader active)
|
|
1829
|
-
try {
|
|
1830
|
-
const dyn = await importDefault(fileUrl);
|
|
1831
|
-
if (dyn)
|
|
1832
|
-
return dyn;
|
|
1833
|
-
}
|
|
1834
|
-
catch {
|
|
1835
|
-
/* fall through */
|
|
1836
|
-
}
|
|
1837
|
-
const stat = await fs.stat(absPath);
|
|
1838
|
-
const hash = cacheHash(absPath, stat.mtimeMs);
|
|
1839
|
-
const cacheDir = path.resolve('.tsbuild', cacheDirName);
|
|
1840
|
-
await fs.ensureDir(cacheDir);
|
|
1841
|
-
const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
|
|
1842
|
-
// Try esbuild
|
|
1843
|
-
try {
|
|
1844
|
-
const esbuild = (await import('esbuild'));
|
|
1845
|
-
await esbuild.build({
|
|
1846
|
-
entryPoints: [absPath],
|
|
1847
|
-
bundle: true,
|
|
1848
|
-
platform: 'node',
|
|
1849
|
-
format: 'esm',
|
|
1850
|
-
target: 'node20',
|
|
1851
|
-
outfile: cacheFile,
|
|
1852
|
-
sourcemap: false,
|
|
1853
|
-
logLevel: 'silent',
|
|
1854
|
-
});
|
|
1855
|
-
const result = await importDefault(url.pathToFileURL(cacheFile).toString());
|
|
1856
|
-
// Best-effort: trim older cache files for this source.
|
|
1857
|
-
await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
|
|
1858
|
-
return result;
|
|
1859
|
-
}
|
|
1860
|
-
catch {
|
|
1861
|
-
/* fall through to TS transpile */
|
|
1862
|
-
}
|
|
1863
|
-
// TypeScript transpile fallback
|
|
1864
|
-
try {
|
|
1865
|
-
const ts = (await import('typescript'));
|
|
1866
|
-
const code = await fs.readFile(absPath, 'utf-8');
|
|
1867
|
-
const out = ts.transpileModule(code, {
|
|
1868
|
-
compilerOptions: {
|
|
1869
|
-
module: 'ESNext',
|
|
1870
|
-
target: 'ES2022',
|
|
1871
|
-
moduleResolution: 'NodeNext',
|
|
1872
|
-
},
|
|
1873
|
-
}).outputText;
|
|
1874
|
-
await fs.writeFile(cacheFile, out, 'utf-8');
|
|
1875
|
-
const result = await importDefault(url.pathToFileURL(cacheFile).toString());
|
|
1876
|
-
// Best-effort: trim older cache files for this source.
|
|
1877
|
-
await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
|
|
1878
|
-
return result;
|
|
1879
|
-
}
|
|
1880
|
-
catch {
|
|
1881
|
-
// Caller decides final error wording; rethrow for upstream mapping.
|
|
1882
|
-
throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
|
|
1883
|
-
}
|
|
1884
|
-
};
|
|
1885
|
-
|
|
1886
|
-
/**
|
|
1887
|
-
* Asynchronously process dotenv files of the form `.env[.<ENV>][.<PRIVATE_TOKEN>]`
|
|
1888
|
-
*
|
|
1889
|
-
* @param options - `GetDotenvOptions` object
|
|
1890
|
-
* @returns The combined parsed dotenv object.
|
|
1891
|
-
* * @example Load from the project root with default tokens
|
|
1892
|
-
* ```ts
|
|
1893
|
-
* const vars = await getDotenv();
|
|
1894
|
-
* console.log(vars.MY_SETTING);
|
|
1895
|
-
* ```
|
|
1896
|
-
*
|
|
1897
|
-
* @example Load from multiple paths and a specific environment
|
|
1898
|
-
* ```ts
|
|
1899
|
-
* const vars = await getDotenv({
|
|
1900
|
-
* env: 'dev',
|
|
1901
|
-
* dotenvToken: '.testenv',
|
|
1902
|
-
* privateToken: 'secret',
|
|
1903
|
-
* paths: ['./', './packages/app'],
|
|
1904
|
-
* });
|
|
1905
|
-
* ```
|
|
1906
|
-
*
|
|
1907
|
-
* @example Use dynamic variables
|
|
1908
|
-
* ```ts
|
|
1909
|
-
* // .env.js default-exports: { DYNAMIC: ({ PREV }) => `${PREV}-suffix` }
|
|
1910
|
-
* const vars = await getDotenv({ dynamicPath: '.env.js' });
|
|
1911
|
-
* ```
|
|
1912
|
-
*
|
|
1913
|
-
* @remarks
|
|
1914
|
-
* - When {@link GetDotenvOptions.loadProcess} is true, the resulting variables are merged
|
|
1915
|
-
* into `process.env` as a side effect.
|
|
1916
|
-
* - When {@link GetDotenvOptions.outputPath} is provided, a consolidated dotenv file is written.
|
|
1917
|
-
* The path is resolved after expansion, so it may reference previously loaded vars.
|
|
1918
|
-
*
|
|
1919
|
-
* @throws Error when a dynamic module is present but cannot be imported.
|
|
1920
|
-
* @throws Error when an output path was requested but could not be resolved.
|
|
1921
|
-
*/
|
|
1922
|
-
const getDotenv = async (options = {}) => {
|
|
1923
|
-
// Apply defaults.
|
|
1924
|
-
const { defaultEnv, dotenvToken = '.env', dynamicPath, env, excludeDynamic = false, excludeEnv = false, excludeGlobal = false, excludePrivate = false, excludePublic = false, loadProcess = false, log = false, logger = console, outputPath, paths = [], privateToken = 'local', vars = {}, } = await resolveGetDotenvOptions(options);
|
|
1925
|
-
// Read .env files.
|
|
1926
|
-
const loaded = paths.length
|
|
1927
|
-
? await paths.reduce(async (e, p) => {
|
|
1928
|
-
const publicGlobal = excludePublic || excludeGlobal
|
|
1929
|
-
? Promise.resolve({})
|
|
1930
|
-
: readDotenv(path.resolve(p, dotenvToken));
|
|
1931
|
-
const publicEnv = excludePublic || excludeEnv || (!env && !defaultEnv)
|
|
1932
|
-
? Promise.resolve({})
|
|
1933
|
-
: readDotenv(path.resolve(p, `${dotenvToken}.${env ?? defaultEnv ?? ''}`));
|
|
1934
|
-
const privateGlobal = excludePrivate || excludeGlobal
|
|
1935
|
-
? Promise.resolve({})
|
|
1936
|
-
: readDotenv(path.resolve(p, `${dotenvToken}.${privateToken}`));
|
|
1937
|
-
const privateEnv = excludePrivate || excludeEnv || (!env && !defaultEnv)
|
|
1938
|
-
? Promise.resolve({})
|
|
1939
|
-
: readDotenv(path.resolve(p, `${dotenvToken}.${env ?? defaultEnv ?? ''}.${privateToken}`));
|
|
1940
|
-
const [eResolved, publicGlobalResolved, publicEnvResolved, privateGlobalResolved, privateEnvResolved,] = await Promise.all([
|
|
1941
|
-
e,
|
|
1942
|
-
publicGlobal,
|
|
1943
|
-
publicEnv,
|
|
1944
|
-
privateGlobal,
|
|
1945
|
-
privateEnv,
|
|
1946
|
-
]);
|
|
1947
|
-
return {
|
|
1948
|
-
...eResolved,
|
|
1949
|
-
...publicGlobalResolved,
|
|
1950
|
-
...publicEnvResolved,
|
|
1951
|
-
...privateGlobalResolved,
|
|
1952
|
-
...privateEnvResolved,
|
|
1953
|
-
};
|
|
1954
|
-
}, Promise.resolve({}))
|
|
1955
|
-
: {};
|
|
1956
|
-
const outputKey = nanoid.nanoid();
|
|
1957
|
-
const dotenv = dotenvExpandAll({
|
|
1958
|
-
...loaded,
|
|
1959
|
-
...vars,
|
|
1960
|
-
...(outputPath ? { [outputKey]: outputPath } : {}),
|
|
1961
|
-
}, { progressive: true });
|
|
1962
|
-
// Process dynamic variables. Programmatic option takes precedence over path.
|
|
1963
|
-
if (!excludeDynamic) {
|
|
1964
|
-
let dynamic = undefined;
|
|
1965
|
-
if (options.dynamic && Object.keys(options.dynamic).length > 0) {
|
|
1966
|
-
dynamic = options.dynamic;
|
|
1967
|
-
}
|
|
1968
|
-
else if (dynamicPath) {
|
|
1969
|
-
const absDynamicPath = path.resolve(dynamicPath);
|
|
1970
|
-
if (await fs.exists(absDynamicPath)) {
|
|
1971
|
-
try {
|
|
1972
|
-
dynamic = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic');
|
|
1973
|
-
}
|
|
1974
|
-
catch {
|
|
1975
|
-
// Preserve legacy error text for compatibility with tests/docs.
|
|
1976
|
-
throw new Error(`Unable to load dynamic TypeScript file: ${absDynamicPath}. ` +
|
|
1977
|
-
`Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
if (dynamic) {
|
|
1982
|
-
try {
|
|
1983
|
-
for (const key in dynamic)
|
|
1984
|
-
Object.assign(dotenv, {
|
|
1985
|
-
[key]: typeof dynamic[key] === 'function'
|
|
1986
|
-
? dynamic[key](dotenv, env ?? defaultEnv)
|
|
1987
|
-
: dynamic[key],
|
|
1988
|
-
});
|
|
1989
|
-
}
|
|
1990
|
-
catch {
|
|
1991
|
-
throw new Error(`Unable to evaluate dynamic variables.`);
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
// Write output file.
|
|
1996
|
-
let resultDotenv = dotenv;
|
|
1997
|
-
if (outputPath) {
|
|
1998
|
-
const outputPathResolved = dotenv[outputKey];
|
|
1999
|
-
if (!outputPathResolved)
|
|
2000
|
-
throw new Error('Output path not found.');
|
|
2001
|
-
const { [outputKey]: _omitted, ...dotenvForOutput } = dotenv;
|
|
2002
|
-
await fs.writeFile(outputPathResolved, Object.keys(dotenvForOutput).reduce((contents, key) => {
|
|
2003
|
-
const value = dotenvForOutput[key] ?? '';
|
|
2004
|
-
return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
|
|
2005
|
-
}, ''), { encoding: 'utf-8' });
|
|
2006
|
-
resultDotenv = dotenvForOutput;
|
|
2007
|
-
}
|
|
2008
|
-
// Log result.
|
|
2009
|
-
if (log) {
|
|
2010
|
-
const redactFlag = options.redact ?? false;
|
|
2011
|
-
const redactPatterns = options.redactPatterns ?? undefined;
|
|
2012
|
-
const redOpts = {};
|
|
2013
|
-
if (redactFlag)
|
|
2014
|
-
redOpts.redact = true;
|
|
2015
|
-
if (redactFlag && Array.isArray(redactPatterns))
|
|
2016
|
-
redOpts.redactPatterns = redactPatterns;
|
|
2017
|
-
const bag = redactFlag
|
|
2018
|
-
? redactObject(resultDotenv, redOpts)
|
|
2019
|
-
: { ...resultDotenv };
|
|
2020
|
-
logger.log(bag);
|
|
2021
|
-
// Entropy warnings: once-per-key-per-run (presentation only)
|
|
2022
|
-
const warnEntropyVal = options.warnEntropy ?? true;
|
|
2023
|
-
const entropyThresholdVal = options
|
|
2024
|
-
.entropyThreshold;
|
|
2025
|
-
const entropyMinLengthVal = options
|
|
2026
|
-
.entropyMinLength;
|
|
2027
|
-
const entropyWhitelistVal = options
|
|
2028
|
-
.entropyWhitelist;
|
|
2029
|
-
const entOpts = {};
|
|
2030
|
-
if (typeof warnEntropyVal === 'boolean')
|
|
2031
|
-
entOpts.warnEntropy = warnEntropyVal;
|
|
2032
|
-
if (typeof entropyThresholdVal === 'number')
|
|
2033
|
-
entOpts.entropyThreshold = entropyThresholdVal;
|
|
2034
|
-
if (typeof entropyMinLengthVal === 'number')
|
|
2035
|
-
entOpts.entropyMinLength = entropyMinLengthVal;
|
|
2036
|
-
if (Array.isArray(entropyWhitelistVal))
|
|
2037
|
-
entOpts.entropyWhitelist = entropyWhitelistVal;
|
|
2038
|
-
for (const [k, v] of Object.entries(resultDotenv)) {
|
|
2039
|
-
maybeWarnEntropy(k, v, v !== undefined ? 'dotenv' : 'unset', entOpts, (line) => {
|
|
2040
|
-
logger.log(line);
|
|
2041
|
-
});
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
// Load process.env.
|
|
2045
|
-
if (loadProcess)
|
|
2046
|
-
Object.assign(process.env, resultDotenv);
|
|
2047
|
-
return resultDotenv;
|
|
2048
|
-
};
|
|
2049
|
-
|
|
2050
|
-
/**
|
|
2051
|
-
* Deep interpolation utility for string leaves.
|
|
2052
|
-
* - Expands string values using dotenv-style expansion against the provided envRef.
|
|
2053
|
-
* - Preserves non-strings as-is.
|
|
2054
|
-
* - Does not recurse into arrays (arrays are returned unchanged).
|
|
2055
|
-
*
|
|
2056
|
-
* Intended for:
|
|
2057
|
-
* - Phase C option/config interpolation after composing ctx.dotenv.
|
|
2058
|
-
* - Per-plugin config slice interpolation before afterResolve.
|
|
2059
|
-
*/
|
|
2060
|
-
/** @internal */
|
|
2061
|
-
const isPlainObject = (v) => v !== null &&
|
|
2062
|
-
typeof v === 'object' &&
|
|
2063
|
-
!Array.isArray(v) &&
|
|
2064
|
-
Object.getPrototypeOf(v) === Object.prototype;
|
|
2065
|
-
/**
|
|
2066
|
-
* Deeply interpolate string leaves against envRef.
|
|
2067
|
-
* Arrays are not recursed into; they are returned unchanged.
|
|
2068
|
-
*
|
|
2069
|
-
* @typeParam T - Shape of the input value.
|
|
2070
|
-
* @param value - Input value (object/array/primitive).
|
|
2071
|
-
* @param envRef - Reference environment for interpolation.
|
|
2072
|
-
* @returns A new value with string leaves interpolated.
|
|
2073
|
-
*/
|
|
2074
|
-
const interpolateDeep = (value, envRef) => {
|
|
2075
|
-
// Strings: expand and return
|
|
2076
|
-
if (typeof value === 'string') {
|
|
2077
|
-
const out = dotenvExpand(value, envRef);
|
|
2078
|
-
// dotenvExpand returns string | undefined; preserve original on undefined
|
|
2079
|
-
return (out ?? value);
|
|
2080
|
-
}
|
|
2081
|
-
// Arrays: return as-is (no recursion)
|
|
2082
|
-
if (Array.isArray(value)) {
|
|
2083
|
-
return value;
|
|
2084
|
-
}
|
|
2085
|
-
// Plain objects: shallow clone and recurse into values
|
|
2086
|
-
if (isPlainObject(value)) {
|
|
2087
|
-
const src = value;
|
|
2088
|
-
const out = {};
|
|
2089
|
-
for (const [k, v] of Object.entries(src)) {
|
|
2090
|
-
// Recurse for strings/objects; keep arrays as-is; preserve other scalars
|
|
2091
|
-
if (typeof v === 'string')
|
|
2092
|
-
out[k] = dotenvExpand(v, envRef) ?? v;
|
|
2093
|
-
else if (Array.isArray(v))
|
|
2094
|
-
out[k] = v;
|
|
2095
|
-
else if (isPlainObject(v))
|
|
2096
|
-
out[k] = interpolateDeep(v, envRef);
|
|
2097
|
-
else
|
|
2098
|
-
out[k] = v;
|
|
2099
|
-
}
|
|
2100
|
-
return out;
|
|
2101
|
-
}
|
|
2102
|
-
// Other primitives/types: return as-is
|
|
2103
|
-
return value;
|
|
2104
|
-
};
|
|
2105
|
-
|
|
2106
|
-
/**
|
|
2107
|
-
* Compute the dotenv context for the host (uses the config loader/overlay path).
|
|
2108
|
-
* - Resolves and validates options strictly (host-only).
|
|
2109
|
-
* - Applies file cascade, overlays, dynamics, and optional effects.
|
|
2110
|
-
* - Merges and validates per-plugin config slices (when provided).
|
|
2111
|
-
*
|
|
2112
|
-
* @param customOptions - Partial options from the current invocation.
|
|
2113
|
-
* @param plugins - Installed plugins (for config validation).
|
|
2114
|
-
* @param hostMetaUrl - import.meta.url of the host module (for packaged root discovery). */
|
|
2115
|
-
const computeContext = async (customOptions, plugins, hostMetaUrl) => {
|
|
2116
|
-
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
2117
|
-
const validated = getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
2118
|
-
// Always-on loader path
|
|
2119
|
-
// 1) Base from files only (no dynamic, no programmatic vars)
|
|
2120
|
-
const base = await getDotenv({
|
|
2121
|
-
...validated,
|
|
2122
|
-
// Build a pure base without side effects or logging.
|
|
2123
|
-
excludeDynamic: true,
|
|
2124
|
-
vars: {},
|
|
2125
|
-
log: false,
|
|
2126
|
-
loadProcess: false,
|
|
2127
|
-
outputPath: undefined,
|
|
2128
|
-
});
|
|
2129
|
-
// 2) Discover config sources and overlay
|
|
2130
|
-
const sources = await resolveGetDotenvConfigSources(hostMetaUrl);
|
|
2131
|
-
const dotenvOverlaid = overlayEnv({
|
|
2132
|
-
base,
|
|
2133
|
-
env: validated.env ?? validated.defaultEnv,
|
|
2134
|
-
configs: sources,
|
|
2135
|
-
...(validated.vars ? { programmaticVars: validated.vars } : {}),
|
|
2136
|
-
});
|
|
2137
|
-
// Helper to apply a dynamic map progressively.
|
|
2138
|
-
const applyDynamic = (target, dynamic, env) => {
|
|
2139
|
-
if (!dynamic)
|
|
2140
|
-
return;
|
|
2141
|
-
for (const key of Object.keys(dynamic)) {
|
|
2142
|
-
const value = typeof dynamic[key] === 'function'
|
|
2143
|
-
? dynamic[key](target, env)
|
|
2144
|
-
: dynamic[key];
|
|
2145
|
-
Object.assign(target, { [key]: value });
|
|
2146
|
-
}
|
|
2147
|
-
};
|
|
2148
|
-
// 3) Apply dynamics in order
|
|
2149
|
-
const dotenv = { ...dotenvOverlaid };
|
|
2150
|
-
applyDynamic(dotenv, validated.dynamic, validated.env ?? validated.defaultEnv);
|
|
2151
|
-
applyDynamic(dotenv, (sources.packaged?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
2152
|
-
applyDynamic(dotenv, (sources.project?.public?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
2153
|
-
applyDynamic(dotenv, (sources.project?.local?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
2154
|
-
// file dynamicPath (lowest)
|
|
2155
|
-
if (validated.dynamicPath) {
|
|
2156
|
-
const absDynamicPath = path.resolve(validated.dynamicPath);
|
|
2157
|
-
try {
|
|
2158
|
-
const dyn = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic-host');
|
|
2159
|
-
applyDynamic(dotenv, dyn, validated.env ?? validated.defaultEnv);
|
|
2160
|
-
}
|
|
2161
|
-
catch {
|
|
2162
|
-
throw new Error(`Unable to load dynamic from ${validated.dynamicPath}`);
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
// 4) Output/log/process merge
|
|
2166
|
-
if (validated.outputPath) {
|
|
2167
|
-
await fs.writeFile(validated.outputPath, Object.keys(dotenv).reduce((contents, key) => {
|
|
2168
|
-
const value = dotenv[key] ?? '';
|
|
2169
|
-
return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
|
|
2170
|
-
}, ''), { encoding: 'utf-8' });
|
|
2171
|
-
}
|
|
2172
|
-
const logger = validated.logger ?? console;
|
|
2173
|
-
if (validated.log)
|
|
2174
|
-
logger.log(dotenv);
|
|
2175
|
-
if (validated.loadProcess)
|
|
2176
|
-
Object.assign(process.env, dotenv);
|
|
2177
|
-
// 5) Merge and validate per-plugin config (packaged < project.public < project.local)
|
|
2178
|
-
const packagedPlugins = (sources.packaged &&
|
|
2179
|
-
sources.packaged.plugins) ??
|
|
2180
|
-
{};
|
|
2181
|
-
const publicPlugins = (sources.project?.public &&
|
|
2182
|
-
sources.project.public.plugins) ??
|
|
2183
|
-
{};
|
|
2184
|
-
const localPlugins = (sources.project?.local &&
|
|
2185
|
-
sources.project.local.plugins) ??
|
|
2186
|
-
{};
|
|
2187
|
-
const mergedPluginConfigs = defaultsDeep({}, packagedPlugins, publicPlugins, localPlugins);
|
|
2188
|
-
for (const p of plugins) {
|
|
2189
|
-
if (!p.id)
|
|
2190
|
-
continue;
|
|
2191
|
-
const slice = mergedPluginConfigs[p.id];
|
|
2192
|
-
if (slice === undefined)
|
|
2193
|
-
continue;
|
|
2194
|
-
// Per-plugin interpolation just before validation/afterResolve:
|
|
2195
|
-
// precedence: process.env wins over ctx.dotenv for slice defaults.
|
|
2196
|
-
const envRef = {
|
|
2197
|
-
...dotenv,
|
|
2198
|
-
...process.env,
|
|
2199
|
-
};
|
|
2200
|
-
const interpolated = interpolateDeep(slice, envRef);
|
|
2201
|
-
// Validate if a schema is provided; otherwise accept interpolated slice as-is.
|
|
2202
|
-
if (p.configSchema) {
|
|
2203
|
-
const parsed = p.configSchema.safeParse(interpolated);
|
|
2204
|
-
if (!parsed.success) {
|
|
2205
|
-
const msgs = parsed.error.issues
|
|
2206
|
-
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
2207
|
-
.join('\n');
|
|
2208
|
-
throw new Error(`Invalid config for plugin '${p.id}':\n${msgs}`);
|
|
2209
|
-
}
|
|
2210
|
-
mergedPluginConfigs[p.id] = parsed.data;
|
|
2211
|
-
}
|
|
2212
|
-
else {
|
|
2213
|
-
mergedPluginConfigs[p.id] = interpolated;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
return {
|
|
2217
|
-
optionsResolved: validated,
|
|
2218
|
-
dotenv: dotenv,
|
|
2219
|
-
plugins: {},
|
|
2220
|
-
pluginConfigs: mergedPluginConfigs,
|
|
2221
|
-
};
|
|
2222
|
-
};
|
|
2223
|
-
|
|
2224
|
-
const HOST_META_URL = (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('plugins.cjs', document.baseURI).href));
|
|
2225
|
-
const CTX_SYMBOL = Symbol('GetDotenvCli.ctx');
|
|
2226
|
-
const OPTS_SYMBOL = Symbol('GetDotenvCli.options');
|
|
2227
|
-
const HELP_HEADER_SYMBOL = Symbol('GetDotenvCli.helpHeader');
|
|
2228
|
-
/**
|
|
2229
|
-
* Plugin-first CLI host for get-dotenv. Extends Commander.Command.
|
|
2230
|
-
*
|
|
2231
|
-
* Responsibilities:
|
|
2232
|
-
* - Resolve options strictly and compute dotenv context (resolveAndLoad).
|
|
2233
|
-
* - Expose a stable accessor for the current context (getCtx).
|
|
2234
|
-
* - Provide a namespacing helper (ns).
|
|
2235
|
-
* - Support composable plugins with parent → children install and afterResolve.
|
|
2236
|
-
*
|
|
2237
|
-
* NOTE: This host is additive and does not alter the legacy CLI.
|
|
2238
|
-
*/
|
|
2239
|
-
class GetDotenvCli extends commander.Command {
|
|
2240
|
-
/** Registered top-level plugins (composition happens via .use()) */
|
|
2241
|
-
_plugins = [];
|
|
2242
|
-
/** One-time installation guard */
|
|
2243
|
-
_installed = false;
|
|
2244
|
-
/** Optional header line to prepend in help output */
|
|
2245
|
-
[HELP_HEADER_SYMBOL];
|
|
2246
|
-
constructor(alias = 'getdotenv') {
|
|
2247
|
-
super(alias);
|
|
2248
|
-
// Ensure subcommands that use passThroughOptions can be attached safely.
|
|
2249
|
-
// Commander requires parent commands to enable positional options when a
|
|
2250
|
-
// child uses passThroughOptions.
|
|
2251
|
-
this.enablePositionalOptions();
|
|
2252
|
-
// Configure grouped help: show only base options in default "Options";
|
|
2253
|
-
// append App/Plugin sections after default help.
|
|
2254
|
-
this.configureHelp({
|
|
2255
|
-
visibleOptions: (cmd) => {
|
|
2256
|
-
const all = cmd.options ??
|
|
2257
|
-
[];
|
|
2258
|
-
const base = all.filter((opt) => {
|
|
2259
|
-
const group = opt.__group;
|
|
2260
|
-
return group === 'base';
|
|
2261
|
-
});
|
|
2262
|
-
// Sort: short-aliased options first, then long-only; stable by flags.
|
|
2263
|
-
const hasShort = (opt) => {
|
|
2264
|
-
const flags = opt.flags ?? '';
|
|
2265
|
-
// Matches "-x," or starting "-x " before any long
|
|
2266
|
-
return /(^|\s|,)-[A-Za-z]/.test(flags);
|
|
2267
|
-
};
|
|
2268
|
-
const byFlags = (opt) => opt.flags ?? '';
|
|
2269
|
-
base.sort((a, b) => {
|
|
2270
|
-
const aS = hasShort(a) ? 1 : 0;
|
|
2271
|
-
const bS = hasShort(b) ? 1 : 0;
|
|
2272
|
-
return bS - aS || byFlags(a).localeCompare(byFlags(b));
|
|
2273
|
-
});
|
|
2274
|
-
return base;
|
|
2275
|
-
},
|
|
2276
|
-
});
|
|
2277
|
-
this.addHelpText('beforeAll', () => {
|
|
2278
|
-
const header = this[HELP_HEADER_SYMBOL];
|
|
2279
|
-
return header && header.length > 0 ? `${header}\n\n` : '';
|
|
2280
|
-
});
|
|
2281
|
-
this.addHelpText('afterAll', (ctx) => this.#renderOptionGroups(ctx.command));
|
|
2282
|
-
// Skeleton preSubcommand hook: produce a context if absent, without
|
|
2283
|
-
// mutating process.env. The passOptions hook (when installed) will // compute the final context using merged CLI options; keeping
|
|
2284
|
-
// loadProcess=false here avoids leaking dotenv values into the parent
|
|
2285
|
-
// process env before subcommands execute.
|
|
2286
|
-
this.hook('preSubcommand', async () => {
|
|
2287
|
-
if (this.getCtx())
|
|
2288
|
-
return;
|
|
2289
|
-
await this.resolveAndLoad({ loadProcess: false });
|
|
2290
|
-
});
|
|
2291
|
-
}
|
|
2292
|
-
/**
|
|
2293
|
-
* Resolve options (strict) and compute dotenv context. * Stores the context on the instance under a symbol.
|
|
2294
|
-
*/
|
|
2295
|
-
async resolveAndLoad(customOptions = {}) {
|
|
2296
|
-
// Resolve defaults, then validate strictly under the new host.
|
|
2297
|
-
const optionsResolved = await resolveGetDotenvOptions(customOptions);
|
|
2298
|
-
getDotenvOptionsSchemaResolved.parse(optionsResolved);
|
|
2299
|
-
// Delegate the heavy lifting to the shared helper (guarded path supported).
|
|
2300
|
-
const ctx = await computeContext(optionsResolved, this._plugins, HOST_META_URL);
|
|
2301
|
-
// Persist context on the instance for later access.
|
|
2302
|
-
this[CTX_SYMBOL] =
|
|
2303
|
-
ctx;
|
|
2304
|
-
// Ensure plugins are installed exactly once, then run afterResolve.
|
|
2305
|
-
await this.install();
|
|
2306
|
-
await this._runAfterResolve(ctx);
|
|
2307
|
-
return ctx;
|
|
2308
|
-
}
|
|
2309
|
-
/**
|
|
2310
|
-
* Retrieve the current invocation context (if any).
|
|
2311
|
-
*/
|
|
2312
|
-
getCtx() {
|
|
2313
|
-
return this[CTX_SYMBOL];
|
|
2314
|
-
}
|
|
2315
|
-
/**
|
|
2316
|
-
* Retrieve the merged root CLI options bag (if set by passOptions()).
|
|
2317
|
-
* Downstream-safe: no generics required.
|
|
2318
|
-
*/
|
|
2319
|
-
getOptions() {
|
|
2320
|
-
return this[OPTS_SYMBOL];
|
|
2321
|
-
}
|
|
2322
|
-
/** Internal: set the merged root options bag for this run. */
|
|
2323
|
-
_setOptionsBag(bag) {
|
|
2324
|
-
this[OPTS_SYMBOL] = bag;
|
|
2325
|
-
}
|
|
2326
|
-
/** * Convenience helper to create a namespaced subcommand.
|
|
2327
|
-
*/
|
|
2328
|
-
ns(name) {
|
|
2329
|
-
return this.command(name);
|
|
2330
|
-
}
|
|
2331
|
-
/**
|
|
2332
|
-
* Tag options added during the provided callback as 'app' for grouped help.
|
|
2333
|
-
* Allows downstream apps to demarcate their root-level options.
|
|
2334
|
-
*/
|
|
2335
|
-
tagAppOptions(fn) {
|
|
2336
|
-
const root = this;
|
|
2337
|
-
const originalAddOption = root.addOption.bind(root);
|
|
2338
|
-
const originalOption = root.option.bind(root);
|
|
2339
|
-
const tagLatest = (cmd, group) => {
|
|
2340
|
-
const optsArr = cmd.options;
|
|
2341
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
2342
|
-
const last = optsArr[optsArr.length - 1];
|
|
2343
|
-
last.__group = group;
|
|
2344
|
-
}
|
|
2345
|
-
};
|
|
2346
|
-
root.addOption = function patchedAdd(opt) {
|
|
2347
|
-
opt.__group = 'app';
|
|
2348
|
-
return originalAddOption(opt);
|
|
2349
|
-
};
|
|
2350
|
-
root.option = function patchedOption(...args) {
|
|
2351
|
-
const ret = originalOption(...args);
|
|
2352
|
-
tagLatest(this, 'app');
|
|
2353
|
-
return ret;
|
|
2354
|
-
};
|
|
2355
|
-
try {
|
|
2356
|
-
return fn(root);
|
|
2357
|
-
}
|
|
2358
|
-
finally {
|
|
2359
|
-
root.addOption = originalAddOption;
|
|
2360
|
-
root.option = originalOption;
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
/**
|
|
2364
|
-
* Branding helper: set CLI name/description/version and optional help header.
|
|
2365
|
-
* If version is omitted and importMetaUrl is provided, attempts to read the
|
|
2366
|
-
* nearest package.json version (best-effort; non-fatal on failure).
|
|
2367
|
-
*/
|
|
2368
|
-
async brand(args) {
|
|
2369
|
-
const { name, description, version, importMetaUrl, helpHeader } = args;
|
|
2370
|
-
if (typeof name === 'string' && name.length > 0)
|
|
2371
|
-
this.name(name);
|
|
2372
|
-
if (typeof description === 'string')
|
|
2373
|
-
this.description(description);
|
|
2374
|
-
let v = version;
|
|
2375
|
-
if (!v && importMetaUrl) {
|
|
2376
|
-
try {
|
|
2377
|
-
const fromUrl = url.fileURLToPath(importMetaUrl);
|
|
2378
|
-
const pkgDir = await packageDirectory.packageDirectory({ cwd: fromUrl });
|
|
2379
|
-
if (pkgDir) {
|
|
2380
|
-
const txt = await fs.readFile(`${pkgDir}/package.json`, 'utf-8');
|
|
2381
|
-
const pkg = JSON.parse(txt);
|
|
2382
|
-
if (pkg.version)
|
|
2383
|
-
v = pkg.version;
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
catch {
|
|
2387
|
-
// best-effort only
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
if (v)
|
|
2391
|
-
this.version(v);
|
|
2392
|
-
// Help header:
|
|
2393
|
-
// - If caller provides helpHeader, use it.
|
|
2394
|
-
// - Otherwise, when a version is known, default to "<name> v<version>".
|
|
2395
|
-
if (typeof helpHeader === 'string') {
|
|
2396
|
-
this[HELP_HEADER_SYMBOL] = helpHeader;
|
|
2397
|
-
}
|
|
2398
|
-
else if (v) {
|
|
2399
|
-
// Use the current command name (possibly overridden by 'name' above).
|
|
2400
|
-
const header = `${this.name()} v${v}`;
|
|
2401
|
-
this[HELP_HEADER_SYMBOL] = header;
|
|
2402
|
-
}
|
|
2403
|
-
return this;
|
|
2404
|
-
}
|
|
2405
|
-
/**
|
|
2406
|
-
* Register a plugin for installation (parent level).
|
|
2407
|
-
* Installation occurs on first resolveAndLoad() (or explicit install()).
|
|
2408
|
-
*/
|
|
2409
|
-
use(plugin) {
|
|
2410
|
-
this._plugins.push(plugin);
|
|
2411
|
-
// Immediately run setup so subcommands exist before parsing.
|
|
2412
|
-
const setupOne = (p) => {
|
|
2413
|
-
p.setup(this);
|
|
2414
|
-
for (const child of p.children)
|
|
2415
|
-
setupOne(child);
|
|
2416
|
-
};
|
|
2417
|
-
setupOne(plugin);
|
|
2418
|
-
return this;
|
|
2419
|
-
}
|
|
2420
|
-
/**
|
|
2421
|
-
* Install all registered plugins in parent → children (pre-order).
|
|
2422
|
-
* Runs only once per CLI instance.
|
|
2423
|
-
*/
|
|
2424
|
-
async install() {
|
|
2425
|
-
// Setup is performed immediately in use(); here we only guard for afterResolve.
|
|
2426
|
-
this._installed = true;
|
|
2427
|
-
// Satisfy require-await without altering behavior.
|
|
2428
|
-
await Promise.resolve();
|
|
2429
|
-
}
|
|
2430
|
-
/**
|
|
2431
|
-
* Run afterResolve hooks for all plugins (parent → children).
|
|
2432
|
-
*/
|
|
2433
|
-
async _runAfterResolve(ctx) {
|
|
2434
|
-
const run = async (p) => {
|
|
2435
|
-
if (p.afterResolve)
|
|
2436
|
-
await p.afterResolve(this, ctx);
|
|
2437
|
-
for (const child of p.children)
|
|
2438
|
-
await run(child);
|
|
2439
|
-
};
|
|
2440
|
-
for (const p of this._plugins)
|
|
2441
|
-
await run(p);
|
|
2442
|
-
}
|
|
2443
|
-
// Render App/Plugin grouped options appended after default help.
|
|
2444
|
-
#renderOptionGroups(cmd) {
|
|
2445
|
-
const all = cmd.options ?? [];
|
|
2446
|
-
const byGroup = new Map();
|
|
2447
|
-
for (const o of all) {
|
|
2448
|
-
const opt = o;
|
|
2449
|
-
const g = opt.__group;
|
|
2450
|
-
if (!g || g === 'base')
|
|
2451
|
-
continue; // base handled by default help
|
|
2452
|
-
const rows = byGroup.get(g) ?? [];
|
|
2453
|
-
rows.push({
|
|
2454
|
-
flags: opt.flags ?? '',
|
|
2455
|
-
description: opt.description ?? '',
|
|
2456
|
-
});
|
|
2457
|
-
byGroup.set(g, rows);
|
|
2458
|
-
}
|
|
2459
|
-
if (byGroup.size === 0)
|
|
2460
|
-
return '';
|
|
2461
|
-
const renderRows = (title, rows) => {
|
|
2462
|
-
const width = Math.min(40, rows.reduce((m, r) => Math.max(m, r.flags.length), 0));
|
|
2463
|
-
// Sort within group: short-aliased flags first
|
|
2464
|
-
rows.sort((a, b) => {
|
|
2465
|
-
const aS = /(^|\s|,)-[A-Za-z]/.test(a.flags) ? 1 : 0;
|
|
2466
|
-
const bS = /(^|\s|,)-[A-Za-z]/.test(b.flags) ? 1 : 0;
|
|
2467
|
-
return bS - aS || a.flags.localeCompare(b.flags);
|
|
2468
|
-
});
|
|
2469
|
-
const lines = rows
|
|
2470
|
-
.map((r) => {
|
|
2471
|
-
const pad = ' '.repeat(Math.max(2, width - r.flags.length + 2));
|
|
2472
|
-
return ` ${r.flags}${pad}${r.description}`.trimEnd();
|
|
2473
|
-
})
|
|
2474
|
-
.join('\n');
|
|
2475
|
-
return `\n${title}:\n${lines}\n`;
|
|
2476
|
-
};
|
|
2477
|
-
let out = '';
|
|
2478
|
-
// App options (if any)
|
|
2479
|
-
const app = byGroup.get('app');
|
|
2480
|
-
if (app && app.length > 0) {
|
|
2481
|
-
out += renderRows('App options', app);
|
|
2482
|
-
}
|
|
2483
|
-
// Plugin groups sorted by id
|
|
2484
|
-
const pluginKeys = Array.from(byGroup.keys()).filter((k) => k.startsWith('plugin:'));
|
|
2485
|
-
pluginKeys.sort((a, b) => a.localeCompare(b));
|
|
2486
|
-
for (const k of pluginKeys) {
|
|
2487
|
-
const id = k.slice('plugin:'.length) || '(unknown)';
|
|
2488
|
-
const rows = byGroup.get(k) ?? [];
|
|
2489
|
-
if (rows.length > 0) {
|
|
2490
|
-
out += renderRows(`Plugin options — ${id}`, rows);
|
|
2491
|
-
}
|
|
2492
|
-
}
|
|
2493
|
-
return out;
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
1169
|
+
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
1170
|
+
const baseRootOptionDefaults = {
|
|
1171
|
+
dotenvToken: '.env',
|
|
1172
|
+
loadProcess: true,
|
|
1173
|
+
logger: console,
|
|
1174
|
+
// Diagnostics defaults
|
|
1175
|
+
warnEntropy: true,
|
|
1176
|
+
entropyThreshold: 3.8,
|
|
1177
|
+
entropyMinLength: 16,
|
|
1178
|
+
entropyWhitelist: ['^GIT_', '^npm_', '^CI$', 'SHLVL'],
|
|
1179
|
+
paths: './',
|
|
1180
|
+
pathsDelimiter: ' ',
|
|
1181
|
+
privateToken: 'local',
|
|
1182
|
+
scripts: {
|
|
1183
|
+
'git-status': {
|
|
1184
|
+
cmd: 'git branch --show-current && git status -s -u',
|
|
1185
|
+
shell: true,
|
|
1186
|
+
},
|
|
1187
|
+
},
|
|
1188
|
+
shell: true,
|
|
1189
|
+
vars: '',
|
|
1190
|
+
varsAssignor: '=',
|
|
1191
|
+
varsDelimiter: ' ',
|
|
1192
|
+
// tri-state flags default to unset unless explicitly provided
|
|
1193
|
+
// (debug/log/exclude* resolved via flag utils)
|
|
1194
|
+
};
|
|
2496
1195
|
|
|
2497
|
-
/**
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
const pick = (getter) => {
|
|
2509
|
-
const pl = sources.project?.local;
|
|
2510
|
-
const pp = sources.project?.public;
|
|
2511
|
-
const pk = sources.packaged;
|
|
2512
|
-
return ((pl && getter(pl)) ||
|
|
2513
|
-
(pp && getter(pp)) ||
|
|
2514
|
-
(pk && getter(pk)) ||
|
|
2515
|
-
undefined);
|
|
2516
|
-
};
|
|
2517
|
-
const schema = pick((cfg) => cfg['schema']);
|
|
2518
|
-
if (schema &&
|
|
2519
|
-
typeof schema.safeParse === 'function') {
|
|
2520
|
-
try {
|
|
2521
|
-
const parsed = schema.safeParse(finalEnv);
|
|
2522
|
-
if (!parsed.success) {
|
|
2523
|
-
// Try to render zod-style issues when available.
|
|
2524
|
-
const err = parsed.error;
|
|
2525
|
-
const issues = Array.isArray(err.issues) && err.issues.length > 0
|
|
2526
|
-
? err.issues.map((i) => {
|
|
2527
|
-
const path = Array.isArray(i.path) ? i.path.join('.') : '';
|
|
2528
|
-
const msg = i.message ?? 'Invalid value';
|
|
2529
|
-
return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
|
|
2530
|
-
})
|
|
2531
|
-
: ['[schema] validation failed'];
|
|
2532
|
-
return issues;
|
|
2533
|
-
}
|
|
2534
|
-
return [];
|
|
1196
|
+
/** @internal */
|
|
1197
|
+
const isPlainObject = (value) => value !== null &&
|
|
1198
|
+
typeof value === 'object' &&
|
|
1199
|
+
Object.getPrototypeOf(value) === Object.prototype;
|
|
1200
|
+
const mergeInto = (target, source) => {
|
|
1201
|
+
for (const [key, sVal] of Object.entries(source)) {
|
|
1202
|
+
if (sVal === undefined)
|
|
1203
|
+
continue; // do not overwrite with undefined
|
|
1204
|
+
const tVal = target[key];
|
|
1205
|
+
if (isPlainObject(tVal) && isPlainObject(sVal)) {
|
|
1206
|
+
target[key] = mergeInto({ ...tVal }, sVal);
|
|
2535
1207
|
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
return [
|
|
2539
|
-
'[schema] validation failed (unable to execute schema.safeParse)',
|
|
2540
|
-
];
|
|
1208
|
+
else if (isPlainObject(sVal)) {
|
|
1209
|
+
target[key] = mergeInto({}, sVal);
|
|
2541
1210
|
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
|
|
2545
|
-
const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
|
|
2546
|
-
if (missing.length > 0) {
|
|
2547
|
-
return missing.map((k) => `[requiredKeys] missing: ${k}`);
|
|
1211
|
+
else {
|
|
1212
|
+
target[key] = sVal;
|
|
2548
1213
|
}
|
|
2549
1214
|
}
|
|
2550
|
-
return
|
|
1215
|
+
return target;
|
|
2551
1216
|
};
|
|
2552
|
-
|
|
2553
1217
|
/**
|
|
2554
|
-
*
|
|
2555
|
-
*
|
|
1218
|
+
* Perform a deep defaults-style merge across plain objects. *
|
|
1219
|
+
* - Only merges plain objects (prototype === Object.prototype).
|
|
1220
|
+
* - Arrays and non-objects are replaced, not merged.
|
|
1221
|
+
* - `undefined` values are ignored and do not overwrite prior values.
|
|
1222
|
+
*
|
|
1223
|
+
* @typeParam T - The resulting shape after merging all layers.
|
|
1224
|
+
* @param layers - Zero or more partial layers in ascending precedence order.
|
|
1225
|
+
* @returns The merged object typed as {@link T}.
|
|
1226
|
+
*
|
|
1227
|
+
* @example
|
|
1228
|
+
* defaultsDeep(\{ a: 1, nested: \{ b: 2 \} \}, \{ nested: \{ b: 3, c: 4 \} \})
|
|
1229
|
+
* =\> \{ a: 1, nested: \{ b: 3, c: 4 \} \}
|
|
2556
1230
|
*/
|
|
2557
|
-
const
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
if (Array.isArray(optsArr) && optsArr.length > 0) {
|
|
2563
|
-
const last = optsArr[optsArr.length - 1];
|
|
2564
|
-
last.__group = group;
|
|
2565
|
-
}
|
|
2566
|
-
};
|
|
2567
|
-
const originalAddOption = program.addOption.bind(program);
|
|
2568
|
-
const originalOption = program.option.bind(program);
|
|
2569
|
-
program.addOption = function patchedAdd(opt) {
|
|
2570
|
-
// Tag before adding, in case consumers inspect the Option directly.
|
|
2571
|
-
opt.__group = GROUP;
|
|
2572
|
-
const ret = originalAddOption(opt);
|
|
2573
|
-
return ret;
|
|
2574
|
-
};
|
|
2575
|
-
program.option = function patchedOption(...args) {
|
|
2576
|
-
const ret = originalOption(...args);
|
|
2577
|
-
tagLatest(this, GROUP);
|
|
2578
|
-
return ret;
|
|
2579
|
-
};
|
|
2580
|
-
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
2581
|
-
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
2582
|
-
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
2583
|
-
// Build initial chain.
|
|
2584
|
-
let p = program
|
|
2585
|
-
.enablePositionalOptions()
|
|
2586
|
-
.passThroughOptions()
|
|
2587
|
-
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
2588
|
-
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
2589
|
-
['KEY1', 'VAL1'],
|
|
2590
|
-
['KEY2', 'VAL2'],
|
|
2591
|
-
]
|
|
2592
|
-
.map((v) => v.join(va))
|
|
2593
|
-
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
2594
|
-
// Optional legacy root command flag (kept for generated CLI compatibility).
|
|
2595
|
-
// Default is OFF; the generator opts in explicitly.
|
|
2596
|
-
if (opts?.includeCommandOption === true) {
|
|
2597
|
-
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
2598
|
-
}
|
|
2599
|
-
p = p
|
|
2600
|
-
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
2601
|
-
.addOption(new commander.Option('-s, --shell [string]', (() => {
|
|
2602
|
-
let defaultLabel = '';
|
|
2603
|
-
if (shell !== undefined) {
|
|
2604
|
-
if (typeof shell === 'boolean') {
|
|
2605
|
-
defaultLabel = ' (default OS shell)';
|
|
2606
|
-
}
|
|
2607
|
-
else if (typeof shell === 'string') {
|
|
2608
|
-
// Safe string interpolation
|
|
2609
|
-
defaultLabel = ` (default ${shell})`;
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
|
|
2613
|
-
})()).conflicts('shellOff'))
|
|
2614
|
-
.addOption(new commander.Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
2615
|
-
.addOption(new commander.Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
2616
|
-
.addOption(new commander.Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
2617
|
-
.addOption(new commander.Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
|
|
2618
|
-
((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
|
|
2619
|
-
? ' (default)'
|
|
2620
|
-
: ''}`).conflicts('excludeAllOff'))
|
|
2621
|
-
.addOption(new commander.Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
|
|
2622
|
-
.addOption(new commander.Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
2623
|
-
.addOption(new commander.Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
2624
|
-
.addOption(new commander.Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
2625
|
-
.addOption(new commander.Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
2626
|
-
.addOption(new commander.Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
2627
|
-
.addOption(new commander.Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
2628
|
-
.addOption(new commander.Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
2629
|
-
.addOption(new commander.Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
2630
|
-
.addOption(new commander.Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
2631
|
-
.addOption(new commander.Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
2632
|
-
.addOption(new commander.Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
2633
|
-
.addOption(new commander.Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
2634
|
-
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
2635
|
-
.option('--redact', 'mask secret-like values in logs/trace (presentation-only)')
|
|
2636
|
-
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
2637
|
-
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
2638
|
-
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
2639
|
-
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
2640
|
-
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
2641
|
-
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
2642
|
-
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
2643
|
-
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
2644
|
-
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
2645
|
-
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
2646
|
-
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
2647
|
-
// Hidden scripts pipe-through (stringified)
|
|
2648
|
-
.addOption(new commander.Option('--scripts <string>')
|
|
2649
|
-
.default(JSON.stringify(scripts))
|
|
2650
|
-
.hideHelp());
|
|
2651
|
-
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
2652
|
-
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
2653
|
-
// Validation: strict mode fails on env validation issues (warn by default).
|
|
2654
|
-
p = p.option('--strict', 'fail on env validation errors (schema/requiredKeys)');
|
|
2655
|
-
// Entropy diagnostics (presentation-only)
|
|
2656
|
-
p = p
|
|
2657
|
-
.addOption(new commander.Option('--entropy-warn', 'enable entropy warnings (default on)').conflicts('entropyWarnOff'))
|
|
2658
|
-
.addOption(new commander.Option('--entropy-warn-off', 'disable entropy warnings').conflicts('entropyWarn'))
|
|
2659
|
-
.option('--entropy-threshold <number>', 'entropy bits/char threshold (default 3.8)')
|
|
2660
|
-
.option('--entropy-min-length <number>', 'min length to examine for entropy (default 16)')
|
|
2661
|
-
.option('--entropy-whitelist <pattern...>', 'suppress entropy warnings when key matches any regex pattern')
|
|
2662
|
-
.option('--redact-pattern <pattern...>', 'additional key-match regex patterns to trigger redaction');
|
|
2663
|
-
// Restore original methods to avoid tagging future additions outside base.
|
|
2664
|
-
program.addOption = originalAddOption;
|
|
2665
|
-
program.option = originalOption;
|
|
2666
|
-
return p;
|
|
1231
|
+
const defaultsDeep = (...layers) => {
|
|
1232
|
+
const result = layers
|
|
1233
|
+
.filter(Boolean)
|
|
1234
|
+
.reduce((acc, layer) => mergeInto(acc, layer), {});
|
|
1235
|
+
return result;
|
|
2667
1236
|
};
|
|
2668
1237
|
|
|
2669
1238
|
/**
|
|
@@ -2791,83 +1360,164 @@ const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
|
2791
1360
|
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
2792
1361
|
};
|
|
2793
1362
|
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
1363
|
+
/**
|
|
1364
|
+
* Dotenv expansion utilities.
|
|
1365
|
+
*
|
|
1366
|
+
* This module implements recursive expansion of environment-variable
|
|
1367
|
+
* references in strings and records. It supports both whitespace and
|
|
1368
|
+
* bracket syntaxes with optional defaults:
|
|
1369
|
+
*
|
|
1370
|
+
* - Whitespace: `$VAR[:default]`
|
|
1371
|
+
* - Bracketed: `${VAR[:default]}`
|
|
1372
|
+
*
|
|
1373
|
+
* Escaped dollar signs (`\$`) are preserved.
|
|
1374
|
+
* Unknown variables resolve to empty string unless a default is provided.
|
|
1375
|
+
*/
|
|
1376
|
+
/**
|
|
1377
|
+
* Like String.prototype.search but returns the last index.
|
|
1378
|
+
* @internal
|
|
1379
|
+
*/
|
|
1380
|
+
const searchLast = (str, rgx) => {
|
|
1381
|
+
const matches = Array.from(str.matchAll(rgx));
|
|
1382
|
+
return matches.length > 0 ? (matches.slice(-1)[0]?.index ?? -1) : -1;
|
|
2798
1383
|
};
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
1384
|
+
const replaceMatch = (value, match, ref) => {
|
|
1385
|
+
/**
|
|
1386
|
+
* @internal
|
|
1387
|
+
*/
|
|
1388
|
+
const group = match[0];
|
|
1389
|
+
const key = match[1];
|
|
1390
|
+
const defaultValue = match[2];
|
|
1391
|
+
if (!key)
|
|
1392
|
+
return value;
|
|
1393
|
+
const replacement = value.replace(group, ref[key] ?? defaultValue ?? '');
|
|
1394
|
+
return interpolate(replacement, ref);
|
|
1395
|
+
};
|
|
1396
|
+
const interpolate = (value = '', ref = {}) => {
|
|
1397
|
+
/**
|
|
1398
|
+
* @internal
|
|
1399
|
+
*/
|
|
1400
|
+
// if value is falsy, return it as is
|
|
1401
|
+
if (!value)
|
|
1402
|
+
return value;
|
|
1403
|
+
// get position of last unescaped dollar sign
|
|
1404
|
+
const lastUnescapedDollarSignIndex = searchLast(value, /(?!(?<=\\))\$/g);
|
|
1405
|
+
// return value if none found
|
|
1406
|
+
if (lastUnescapedDollarSignIndex === -1)
|
|
1407
|
+
return value;
|
|
1408
|
+
// evaluate the value tail
|
|
1409
|
+
const tail = value.slice(lastUnescapedDollarSignIndex);
|
|
1410
|
+
// find whitespace pattern: $KEY:DEFAULT
|
|
1411
|
+
const whitespacePattern = /^\$([\w]+)(?::([^\s]*))?/;
|
|
1412
|
+
const whitespaceMatch = whitespacePattern.exec(tail);
|
|
1413
|
+
if (whitespaceMatch != null)
|
|
1414
|
+
return replaceMatch(value, whitespaceMatch, ref);
|
|
1415
|
+
else {
|
|
1416
|
+
// find bracket pattern: ${KEY:DEFAULT}
|
|
1417
|
+
const bracketPattern = /^\${([\w]+)(?::([^}]*))?}/;
|
|
1418
|
+
const bracketMatch = bracketPattern.exec(tail);
|
|
1419
|
+
if (bracketMatch != null)
|
|
1420
|
+
return replaceMatch(value, bracketMatch, ref);
|
|
1421
|
+
}
|
|
1422
|
+
return value;
|
|
1423
|
+
};
|
|
1424
|
+
/**
|
|
1425
|
+
* Recursively expands environment variables in a string. Variables may be
|
|
1426
|
+
* presented with optional default as `$VAR[:default]` or `${VAR[:default]}`.
|
|
1427
|
+
* Unknown variables will expand to an empty string.
|
|
1428
|
+
*
|
|
1429
|
+
* @param value - The string to expand.
|
|
1430
|
+
* @param ref - The reference object to use for variable expansion.
|
|
1431
|
+
* @returns The expanded string.
|
|
1432
|
+
*
|
|
1433
|
+
* @example
|
|
1434
|
+
* ```ts
|
|
1435
|
+
* process.env.FOO = 'bar';
|
|
1436
|
+
* dotenvExpand('Hello $FOO'); // "Hello bar"
|
|
1437
|
+
* dotenvExpand('Hello $BAZ:world'); // "Hello world"
|
|
1438
|
+
* ```
|
|
1439
|
+
*
|
|
1440
|
+
* @remarks
|
|
1441
|
+
* The expansion is recursive. If a referenced variable itself contains
|
|
1442
|
+
* references, those will also be expanded until a stable value is reached.
|
|
1443
|
+
* Escaped references (e.g. `\$FOO`) are preserved as literals.
|
|
1444
|
+
*/
|
|
1445
|
+
const dotenvExpand = (value, ref = process.env) => {
|
|
1446
|
+
const result = interpolate(value, ref);
|
|
1447
|
+
return result ? result.replace(/\\\$/g, '$') : undefined;
|
|
1448
|
+
};
|
|
1449
|
+
/**
|
|
1450
|
+
* Recursively expands environment variables in a string using `process.env` as
|
|
1451
|
+
* the expansion reference. Variables may be presented with optional default as
|
|
1452
|
+
* `$VAR[:default]` or `${VAR[:default]}`. Unknown variables will expand to an
|
|
1453
|
+
* empty string.
|
|
1454
|
+
*
|
|
1455
|
+
* @param value - The string to expand.
|
|
1456
|
+
* @returns The expanded string.
|
|
1457
|
+
*
|
|
1458
|
+
* @example
|
|
1459
|
+
* ```ts
|
|
1460
|
+
* process.env.FOO = 'bar';
|
|
1461
|
+
* dotenvExpandFromProcessEnv('Hello $FOO'); // "Hello bar"
|
|
1462
|
+
* ```
|
|
1463
|
+
*/
|
|
1464
|
+
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
1465
|
+
|
|
1466
|
+
// src/GetDotenvOptions.ts
|
|
1467
|
+
/**
|
|
1468
|
+
* Converts programmatic CLI options to `getDotenv` options. *
|
|
1469
|
+
* @param cliOptions - CLI options. Defaults to `{}`.
|
|
1470
|
+
*
|
|
1471
|
+
* @returns `getDotenv` options.
|
|
1472
|
+
*/
|
|
1473
|
+
const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPattern, vars, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, ...rest }) => {
|
|
1474
|
+
/**
|
|
1475
|
+
* Convert CLI-facing string options into {@link GetDotenvOptions}.
|
|
1476
|
+
*
|
|
1477
|
+
* - Splits {@link GetDotenvCliOptions.paths} using either a delimiter * or a regular expression pattern into a string array. * - Parses {@link GetDotenvCliOptions.vars} as space-separated `KEY=VALUE`
|
|
1478
|
+
* pairs (configurable delimiters) into a {@link ProcessEnv}.
|
|
1479
|
+
* - Drops CLI-only keys that have no programmatic equivalent.
|
|
1480
|
+
*
|
|
1481
|
+
* @remarks
|
|
1482
|
+
* Follows exact-optional semantics by not emitting undefined-valued entries.
|
|
1483
|
+
*/
|
|
1484
|
+
// Drop CLI-only keys (debug/scripts) without relying on Record casts.
|
|
1485
|
+
// Create a shallow copy then delete optional CLI-only keys if present.
|
|
1486
|
+
const restObj = { ...rest };
|
|
1487
|
+
delete restObj.debug;
|
|
1488
|
+
delete restObj.scripts;
|
|
1489
|
+
const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
|
|
1490
|
+
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
1491
|
+
let parsedVars;
|
|
1492
|
+
if (typeof vars === 'string') {
|
|
1493
|
+
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
1494
|
+
? RegExp(varsAssignorPattern)
|
|
1495
|
+
: (varsAssignor ?? '=')));
|
|
1496
|
+
parsedVars = Object.fromEntries(kvPairs);
|
|
1497
|
+
}
|
|
1498
|
+
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
1499
|
+
// Keep only string or undefined values to match ProcessEnv.
|
|
1500
|
+
const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
|
|
1501
|
+
parsedVars = Object.fromEntries(entries);
|
|
1502
|
+
}
|
|
1503
|
+
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
1504
|
+
// expectations and the compat test assertions.
|
|
1505
|
+
if (parsedVars) {
|
|
1506
|
+
parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
|
|
1507
|
+
}
|
|
1508
|
+
// Tolerate paths as either a delimited string or string[]
|
|
1509
|
+
// Use a locally cast union type to avoid lint warnings about always-falsy conditions
|
|
1510
|
+
// under the RootOptionsShape (which declares paths as string | undefined).
|
|
1511
|
+
const pathsAny = paths;
|
|
1512
|
+
const pathsOut = Array.isArray(pathsAny)
|
|
1513
|
+
? pathsAny.filter((p) => typeof p === 'string')
|
|
1514
|
+
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
1515
|
+
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
1516
|
+
return {
|
|
1517
|
+
...restObj,
|
|
1518
|
+
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
1519
|
+
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
1520
|
+
};
|
|
2871
1521
|
};
|
|
2872
1522
|
|
|
2873
1523
|
const dbg = (...args) => {
|
|
@@ -3164,10 +1814,10 @@ const cmdPlugin = (options = {}) => definePlugin({
|
|
|
3164
1814
|
return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
3165
1815
|
};
|
|
3166
1816
|
const aliasKey = aliasSpec ? deriveKey(aliasSpec.flags) : undefined;
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
.
|
|
3170
|
-
.
|
|
1817
|
+
// Create as a GetDotenvCli child so helpInformation includes a trailing blank line.
|
|
1818
|
+
const cmd = cli
|
|
1819
|
+
.createCommand('cmd')
|
|
1820
|
+
.description('Execute command according to the --shell option, conflicts with --command option (default subcommand)')
|
|
3171
1821
|
.enablePositionalOptions()
|
|
3172
1822
|
.passThroughOptions()
|
|
3173
1823
|
.argument('[command...]')
|
|
@@ -3359,7 +2009,7 @@ const demoPlugin = () => definePlugin({
|
|
|
3359
2009
|
const dotenv = (ctx?.dotenv ?? {});
|
|
3360
2010
|
// Inherit stdio for an interactive demo. Use --capture for CI.
|
|
3361
2011
|
await runCommand(['node', '-e', code], false, {
|
|
3362
|
-
env:
|
|
2012
|
+
env: buildSpawnEnv(process.env, dotenv),
|
|
3363
2013
|
stdio: 'inherit',
|
|
3364
2014
|
});
|
|
3365
2015
|
});
|
|
@@ -3396,20 +2046,23 @@ const demoPlugin = () => definePlugin({
|
|
|
3396
2046
|
const ctx = cli.getCtx();
|
|
3397
2047
|
const dotenv = (ctx?.dotenv ?? {});
|
|
3398
2048
|
await runCommand(resolved, shell, {
|
|
3399
|
-
env:
|
|
2049
|
+
env: buildSpawnEnv(process.env, dotenv),
|
|
3400
2050
|
stdio: 'inherit',
|
|
3401
2051
|
});
|
|
3402
2052
|
});
|
|
3403
2053
|
},
|
|
3404
2054
|
/**
|
|
3405
2055
|
* Optional: afterResolve can initialize per-plugin state using ctx.dotenv.
|
|
3406
|
-
* For the demo we
|
|
2056
|
+
* For the demo we emit a single breadcrumb only when GETDOTENV_DEBUG is set,
|
|
2057
|
+
* keeping default runs (tests/CI/smoke) quiet.
|
|
3407
2058
|
*/
|
|
3408
2059
|
afterResolve(_cli, ctx) {
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
2060
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
2061
|
+
const keys = Object.keys(ctx.dotenv);
|
|
2062
|
+
if (keys.length > 0) {
|
|
2063
|
+
// Keep noise low; a single-line breadcrumb is sufficient for the demo.
|
|
2064
|
+
console.error('[demo] afterResolve: dotenv keys loaded:', keys.length);
|
|
2065
|
+
}
|
|
3413
2066
|
}
|
|
3414
2067
|
},
|
|
3415
2068
|
});
|