@karmaniverous/get-dotenv 6.0.0-0 → 6.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.
Files changed (93) hide show
  1. package/README.md +86 -334
  2. package/dist/cli.d.ts +569 -0
  3. package/dist/cli.mjs +18788 -0
  4. package/dist/cliHost.d.ts +548 -253
  5. package/dist/cliHost.mjs +1990 -1458
  6. package/dist/config.d.ts +192 -14
  7. package/dist/config.mjs +256 -81
  8. package/dist/env-overlay.d.ts +226 -18
  9. package/dist/env-overlay.mjs +181 -22
  10. package/dist/getdotenv.cli.mjs +18166 -3437
  11. package/dist/index.d.ts +729 -136
  12. package/dist/index.mjs +18207 -3457
  13. package/dist/plugins-aws.d.ts +289 -104
  14. package/dist/plugins-aws.mjs +2462 -350
  15. package/dist/plugins-batch.d.ts +355 -105
  16. package/dist/plugins-batch.mjs +2595 -420
  17. package/dist/plugins-cmd.d.ts +287 -118
  18. package/dist/plugins-cmd.mjs +2661 -839
  19. package/dist/plugins-init.d.ts +272 -100
  20. package/dist/plugins-init.mjs +2152 -37
  21. package/dist/plugins.d.ts +323 -140
  22. package/dist/plugins.mjs +18006 -2025
  23. package/dist/templates/cli/index.ts +26 -0
  24. package/dist/templates/cli/plugins/hello.ts +43 -0
  25. package/dist/templates/config/js/getdotenv.config.js +20 -0
  26. package/dist/templates/config/json/local/getdotenv.config.local.json +7 -0
  27. package/dist/templates/config/json/public/getdotenv.config.json +9 -0
  28. package/dist/templates/config/public/getdotenv.config.json +8 -0
  29. package/dist/templates/config/ts/getdotenv.config.ts +28 -0
  30. package/dist/templates/config/yaml/local/getdotenv.config.local.yaml +7 -0
  31. package/dist/templates/config/yaml/public/getdotenv.config.yaml +7 -0
  32. package/dist/templates/getdotenv.config.js +20 -0
  33. package/dist/templates/getdotenv.config.json +9 -0
  34. package/dist/templates/getdotenv.config.local.json +7 -0
  35. package/dist/templates/getdotenv.config.local.yaml +7 -0
  36. package/dist/templates/getdotenv.config.ts +28 -0
  37. package/dist/templates/getdotenv.config.yaml +7 -0
  38. package/dist/templates/hello.ts +43 -0
  39. package/dist/templates/index.ts +26 -0
  40. package/dist/templates/js/getdotenv.config.js +20 -0
  41. package/dist/templates/json/local/getdotenv.config.local.json +7 -0
  42. package/dist/templates/json/public/getdotenv.config.json +9 -0
  43. package/dist/templates/local/getdotenv.config.local.json +7 -0
  44. package/dist/templates/local/getdotenv.config.local.yaml +7 -0
  45. package/dist/templates/plugins/hello.ts +43 -0
  46. package/dist/templates/public/getdotenv.config.json +9 -0
  47. package/dist/templates/public/getdotenv.config.yaml +7 -0
  48. package/dist/templates/ts/getdotenv.config.ts +28 -0
  49. package/dist/templates/yaml/local/getdotenv.config.local.yaml +7 -0
  50. package/dist/templates/yaml/public/getdotenv.config.yaml +7 -0
  51. package/getdotenv.config.json +1 -19
  52. package/package.json +52 -89
  53. package/templates/cli/index.ts +26 -0
  54. package/templates/cli/plugins/hello.ts +43 -0
  55. package/templates/config/js/getdotenv.config.js +9 -4
  56. package/templates/config/json/public/getdotenv.config.json +0 -3
  57. package/templates/config/public/getdotenv.config.json +0 -5
  58. package/templates/config/ts/getdotenv.config.ts +17 -5
  59. package/templates/config/yaml/public/getdotenv.config.yaml +0 -3
  60. package/dist/cliHost.cjs +0 -2078
  61. package/dist/cliHost.d.cts +0 -451
  62. package/dist/cliHost.d.mts +0 -451
  63. package/dist/config.cjs +0 -252
  64. package/dist/config.d.cts +0 -55
  65. package/dist/config.d.mts +0 -55
  66. package/dist/env-overlay.cjs +0 -163
  67. package/dist/env-overlay.d.cts +0 -50
  68. package/dist/env-overlay.d.mts +0 -50
  69. package/dist/index.cjs +0 -4077
  70. package/dist/index.d.cts +0 -318
  71. package/dist/index.d.mts +0 -318
  72. package/dist/plugins-aws.cjs +0 -666
  73. package/dist/plugins-aws.d.cts +0 -158
  74. package/dist/plugins-aws.d.mts +0 -158
  75. package/dist/plugins-batch.cjs +0 -658
  76. package/dist/plugins-batch.d.cts +0 -181
  77. package/dist/plugins-batch.d.mts +0 -181
  78. package/dist/plugins-cmd.cjs +0 -1112
  79. package/dist/plugins-cmd.d.cts +0 -178
  80. package/dist/plugins-cmd.d.mts +0 -178
  81. package/dist/plugins-demo.cjs +0 -352
  82. package/dist/plugins-demo.d.cts +0 -158
  83. package/dist/plugins-demo.d.mts +0 -158
  84. package/dist/plugins-demo.d.ts +0 -158
  85. package/dist/plugins-demo.mjs +0 -350
  86. package/dist/plugins-init.cjs +0 -289
  87. package/dist/plugins-init.d.cts +0 -162
  88. package/dist/plugins-init.d.mts +0 -162
  89. package/dist/plugins.cjs +0 -2327
  90. package/dist/plugins.d.cts +0 -211
  91. package/dist/plugins.d.mts +0 -211
  92. package/templates/cli/ts/index.ts +0 -9
  93. package/templates/cli/ts/plugins/hello.ts +0 -17
@@ -1,35 +1,238 @@
1
1
  /**
2
- * A minimal representation of an environment key/value mapping.
3
- * Values may be `undefined` to represent "unset". */ type ProcessEnv = Record<string, string | undefined>;
4
-
5
- type Scripts = Record<string, string | {
6
- cmd: string;
2
+ * Minimal root options shape shared by CLI and generator layers.
3
+ * Keep keys optional to respect exactOptionalPropertyTypes semantics.
4
+ *
5
+ * @public
6
+ */
7
+ interface RootOptionsShape {
8
+ /** Target environment (dotenv-expanded). */
9
+ env?: string;
10
+ /** Explicit variable overrides (dotenv-expanded). */
11
+ vars?: string;
12
+ /** Command to execute (dotenv-expanded). */
13
+ command?: string;
14
+ /** Output path for the consolidated environment file (dotenv-expanded). */
15
+ outputPath?: string;
16
+ /**
17
+ * Shell execution strategy.
18
+ * - `true`: use default OS shell.
19
+ * - `false`: use plain execution (no shell).
20
+ * - string: use specific shell path.
21
+ */
7
22
  shell?: string | boolean;
8
- }>;
9
-
10
- type GetDotenvConfigResolved = {
11
- dotenvToken?: string;
12
- privateToken?: string;
13
- paths?: string[];
23
+ /** Whether to load variables into `process.env`. */
14
24
  loadProcess?: boolean;
25
+ /** Exclude all variables from loading. */
26
+ excludeAll?: boolean;
27
+ /** Exclude dynamic variables. */
28
+ excludeDynamic?: boolean;
29
+ /** Exclude environment-specific variables. */
30
+ excludeEnv?: boolean;
31
+ /** Exclude global variables. */
32
+ excludeGlobal?: boolean;
33
+ /** Exclude private variables. */
34
+ excludePrivate?: boolean;
35
+ /** Exclude public variables. */
36
+ excludePublic?: boolean;
37
+ /** Enable console logging of loaded variables. */
15
38
  log?: boolean;
16
- shell?: string | boolean;
39
+ /** Enable debug logging to stderr. */
40
+ debug?: boolean;
41
+ /** Capture child process stdio (useful for tests/CI). */
42
+ capture?: boolean;
43
+ /** Fail on validation errors (schema/requiredKeys). */
44
+ strict?: boolean;
45
+ /** Enable presentation-time redaction of secret-like keys. */
46
+ redact?: boolean;
47
+ /** Enable entropy warnings for high-entropy values. */
48
+ warnEntropy?: boolean;
49
+ /** Entropy threshold (bits/char) for warnings (default 3.8). */
50
+ entropyThreshold?: number;
51
+ /** Minimum string length to check for entropy (default 16). */
52
+ entropyMinLength?: number;
53
+ /** Regex patterns for keys to exclude from entropy checks. */
54
+ entropyWhitelist?: ReadonlyArray<string>;
55
+ /** Additional regex patterns for keys to redact. */
56
+ redactPatterns?: string[];
57
+ /** Default target environment when not specified. */
58
+ defaultEnv?: string;
59
+ /** Token indicating a dotenv file (default: ".env"). */
60
+ dotenvToken?: string;
61
+ /** Path to dynamic variables module (default: undefined). */
62
+ dynamicPath?: string;
63
+ /**
64
+ * Emit diagnostics for child env composition.
65
+ * - `true`: trace all keys.
66
+ * - `string[]`: trace selected keys.
67
+ */
68
+ trace?: boolean | string[];
69
+ /** Paths to search for dotenv files (space-delimited string or array). */
70
+ paths?: string;
71
+ /** Delimiter for paths string (default: space). */
72
+ pathsDelimiter?: string;
73
+ /** Regex pattern for paths delimiter. */
74
+ pathsDelimiterPattern?: string;
75
+ /** Token indicating private variables (default: "local"). */
76
+ privateToken?: string;
77
+ /** Delimiter for vars string (default: space). */
78
+ varsDelimiter?: string;
79
+ /** Regex pattern for vars delimiter. */
80
+ varsDelimiterPattern?: string;
81
+ /** Assignment operator for vars (default: "="). */
82
+ varsAssignor?: string;
83
+ /** Regex pattern for vars assignment operator. */
84
+ varsAssignorPattern?: string;
85
+ /** Table of named scripts for execution. */
86
+ scripts?: ScriptsTable;
87
+ }
88
+ /**
89
+ * Definition for a single script entry.
90
+ */
91
+ interface ScriptDef<TShell extends string | boolean = string | boolean> {
92
+ /** The command string to execute. */
93
+ cmd: string;
94
+ /** Shell override for this script. */
95
+ shell?: TShell | undefined;
96
+ }
97
+ /**
98
+ * Scripts table shape.
99
+ */
100
+ type ScriptsTable<TShell extends string | boolean = string | boolean> = Record<string, string | ScriptDef<TShell>>;
101
+
102
+ /**
103
+ * Unify Scripts via the generic ScriptsTable<TShell> so shell types propagate.
104
+ */
105
+ type Scripts = ScriptsTable;
106
+
107
+ type GetDotenvConfigResolved = {
108
+ /**
109
+ * Help-time/runtime root defaults applied by the host (collapsed families; CLI‑like).
110
+ */
111
+ rootOptionDefaults?: Partial<RootOptionsShape>;
112
+ /**
113
+ * Help-time visibility for root flags; when a key is false the corresponding
114
+ * option(s) are hidden in root help output.
115
+ */
116
+ rootOptionVisibility?: Partial<Record<keyof RootOptionsShape, boolean>>;
117
+ /**
118
+ * Merged scripts table for resolving commands and shell behavior.
119
+ * Entries may be strings or objects with `cmd` and optional `shell`.
120
+ */
17
121
  scripts?: Scripts;
122
+ /**
123
+ * Keys required to be present in the final composed environment.
124
+ * Validation occurs after overlays and dynamics.
125
+ */
18
126
  requiredKeys?: string[];
127
+ /**
128
+ * Optional validation schema (e.g., Zod). When present and it exposes
129
+ * `safeParse(finalEnv)`, the host executes it once after overlays.
130
+ */
19
131
  schema?: unknown;
132
+ /**
133
+ * Public global variables (string‑only).
134
+ */
20
135
  vars?: Record<string, string>;
136
+ /**
137
+ * Public per‑environment variables (string‑only).
138
+ */
21
139
  envVars?: Record<string, Record<string, string>>;
140
+ /**
141
+ * Dynamic variable definitions (JS/TS configs only).
142
+ */
22
143
  dynamic?: unknown;
144
+ /**
145
+ * Per‑plugin configuration slices keyed by realized mount path
146
+ * (for example, "aws/whoami").
147
+ */
23
148
  plugins?: Record<string, unknown>;
24
149
  };
25
150
 
151
+ /**
152
+ * Canonical programmatic options and helpers for get-dotenv.
153
+ *
154
+ * Requirements addressed:
155
+ * - GetDotenvOptions derives from the Zod schema output (single source of truth).
156
+ * - Removed deprecated/compat flags from the public shape (e.g., useConfigLoader).
157
+ * - Provide Vars-aware defineDynamic and a typed config builder defineGetDotenvConfig\<Vars, Env\>().
158
+ * - Preserve existing behavior for defaults resolution and compat converters.
159
+ */
160
+
161
+ /**
162
+ * A minimal representation of an environment key/value mapping.
163
+ * Values may be `undefined` to represent "unset".
164
+ */
165
+ type ProcessEnv = Record<string, string | undefined>;
166
+ /**
167
+ * Dynamic variable function signature. Receives the current expanded variables
168
+ * and the selected environment (if any), and returns either a string to set
169
+ * or `undefined` to unset/skip the variable.
170
+ */
171
+ type GetDotenvDynamicFunction = (vars: ProcessEnv, env: string | undefined) => string | undefined;
172
+ /**
173
+ * A map of dynamic variable definitions.
174
+ * Keys are variable names; values are either literal strings or functions.
175
+ */
176
+ type GetDotenvDynamic = Record<string, GetDotenvDynamicFunction | ReturnType<GetDotenvDynamicFunction>>;
177
+
178
+ /**
179
+ * Apply a dynamic map to the target progressively.
180
+ * - Functions receive (target, env) and may return string | undefined.
181
+ * - Literals are assigned directly (including undefined).
182
+ */
183
+ declare function applyDynamicMap(target: ProcessEnv, map?: GetDotenvDynamic, env?: string): void;
184
+ /**
185
+ * Load a default-export dynamic map from a JS/TS file and apply it.
186
+ * Uses util/loadModuleDefault for robust TS handling (direct import, esbuild,
187
+ * typescript.transpile fallback).
188
+ *
189
+ * Error behavior:
190
+ * - On failure to load/compile/evaluate the module, throws a unified message:
191
+ * "Unable to load dynamic TypeScript file: <absPath>. Install 'esbuild'..."
192
+ */
193
+ declare function loadAndApplyDynamic(target: ProcessEnv, absPath: string, env: string | undefined, cacheDirName: string): Promise<void>;
194
+
195
+ /**
196
+ * Configuration sources for environment overlay.
197
+ * Organized by scope (packaged vs project) and privacy (public vs local).
198
+ *
199
+ * @public
200
+ */
26
201
  type OverlayConfigSources = {
202
+ /** Packaged configuration (public). */
27
203
  packaged?: GetDotenvConfigResolved;
204
+ /** Project configuration (public and local). */
28
205
  project?: {
206
+ /** Project public configuration. */
29
207
  public?: GetDotenvConfigResolved;
208
+ /** Project local configuration. */
30
209
  local?: GetDotenvConfigResolved;
31
210
  };
32
211
  };
212
+ /**
213
+ * Base options for overlaying config-provided values onto a dotenv map.
214
+ *
215
+ * @typeParam B - base env shape
216
+ * @public
217
+ */
218
+ interface OverlayEnvOptionsBase<B extends Record<string, string | undefined> | Readonly<Record<string, string | undefined>>> {
219
+ /** Base environment variables. */
220
+ base: B;
221
+ /** Target environment name. */
222
+ env: string | undefined;
223
+ /** Configuration sources to overlay. */
224
+ configs: OverlayConfigSources;
225
+ }
226
+ /**
227
+ * Options including explicit programmatic variables which take top precedence.
228
+ *
229
+ * @typeParam B - base env shape
230
+ * @typeParam P - programmatic vars shape
231
+ * @public
232
+ */
233
+ interface OverlayEnvOptionsWithProgrammatic<B extends Record<string, string | undefined> | Readonly<Record<string, string | undefined>>, P extends Record<string, string | undefined> | Readonly<Record<string, string | undefined>>> extends OverlayEnvOptionsBase<B> {
234
+ programmaticVars: P;
235
+ }
33
236
  /**
34
237
  * Overlay config-provided values onto a base ProcessEnv using precedence axes:
35
238
  * - kind: env \> global
@@ -39,12 +242,17 @@ type OverlayConfigSources = {
39
242
  * Programmatic explicit vars (if provided) override all config slices.
40
243
  * Progressive expansion is applied within each slice.
41
244
  */
42
- declare const overlayEnv: ({ base, env, configs, programmaticVars, }: {
43
- base: ProcessEnv;
245
+ declare function overlayEnv<B extends ProcessEnv | Readonly<Record<string, string | undefined>>>(args: {
246
+ base: B;
247
+ env: string | undefined;
248
+ configs: OverlayConfigSources;
249
+ }): B;
250
+ declare function overlayEnv<B extends ProcessEnv | Readonly<Record<string, string | undefined>>, P extends ProcessEnv | Readonly<Record<string, string | undefined>>>(args: {
251
+ base: B;
44
252
  env: string | undefined;
45
253
  configs: OverlayConfigSources;
46
- programmaticVars?: ProcessEnv;
47
- }) => ProcessEnv;
254
+ programmaticVars: P;
255
+ }): B & P;
48
256
 
49
- export { overlayEnv };
50
- export type { OverlayConfigSources };
257
+ export { applyDynamicMap, loadAndApplyDynamic, overlayEnv };
258
+ export type { OverlayConfigSources, OverlayEnvOptionsBase, OverlayEnvOptionsWithProgrammatic };
@@ -1,3 +1,8 @@
1
+ import fs from 'fs-extra';
2
+ import { createHash } from 'crypto';
3
+ import path from 'path';
4
+ import url from 'url';
5
+
1
6
  /**
2
7
  * Dotenv expansion utilities.
3
8
  *
@@ -109,14 +114,176 @@ const dotenvExpand = (value, ref = process.env) => {
109
114
  * When `progressive` is true, each expanded key becomes available for
110
115
  * subsequent expansions in the same object (left-to-right by object key order).
111
116
  */
112
- const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduce((acc, key) => {
113
- const { ref = process.env, progressive = false } = options;
114
- acc[key] = dotenvExpand(values[key], {
115
- ...ref,
116
- ...(progressive ? acc : {}),
117
- });
118
- return acc;
119
- }, {});
117
+ function dotenvExpandAll(values, options = {}) {
118
+ const { ref = process.env, progressive = false, } = options;
119
+ const out = Object.keys(values).reduce((acc, key) => {
120
+ acc[key] = dotenvExpand(values[key], {
121
+ ...ref,
122
+ ...(progressive ? acc : {}),
123
+ });
124
+ return acc;
125
+ }, {});
126
+ // Key-preserving return with a permissive index signature to allow later additions.
127
+ return out;
128
+ }
129
+
130
+ const importDefault = async (fileUrl) => {
131
+ const mod = (await import(fileUrl));
132
+ return mod.default;
133
+ };
134
+ const cacheHash = (absPath, mtimeMs) => createHash('sha1')
135
+ .update(absPath)
136
+ .update(String(mtimeMs))
137
+ .digest('hex')
138
+ .slice(0, 12);
139
+ /**
140
+ * Remove older compiled cache files for a given source base name, keeping
141
+ * at most `keep` most-recent files. Errors are ignored by design.
142
+ */
143
+ const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Number.parseInt(process.env.GETDOTENV_CACHE_KEEP ?? '2'))) => {
144
+ try {
145
+ const entries = await fs.readdir(cacheDir);
146
+ const mine = entries
147
+ .filter((f) => f.startsWith(`${baseName}.`) && f.endsWith('.mjs'))
148
+ .map((f) => path.join(cacheDir, f));
149
+ if (mine.length <= keep)
150
+ return;
151
+ const stats = await Promise.all(mine.map(async (p) => ({ p, mtimeMs: (await fs.stat(p)).mtimeMs })));
152
+ stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
153
+ const toDelete = stats.slice(keep).map((s) => s.p);
154
+ await Promise.all(toDelete.map(async (p) => {
155
+ try {
156
+ await fs.remove(p);
157
+ }
158
+ catch {
159
+ // best-effort cleanup
160
+ }
161
+ }));
162
+ }
163
+ catch {
164
+ // best-effort cleanup
165
+ }
166
+ };
167
+ /**
168
+ * Load a module default export from a JS/TS file with robust fallbacks:
169
+ * - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
170
+ * 1) try direct import (if a TS loader is active),
171
+ * 2) esbuild bundle to a temp ESM file,
172
+ * 3) typescript.transpileModule fallback for simple modules.
173
+ *
174
+ * @param absPath - absolute path to source file
175
+ * @param cacheDirName - cache subfolder under .tsbuild
176
+ */
177
+ const loadModuleDefault = async (absPath, cacheDirName) => {
178
+ const ext = path.extname(absPath).toLowerCase();
179
+ const fileUrl = url.pathToFileURL(absPath).toString();
180
+ if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
181
+ return importDefault(fileUrl);
182
+ }
183
+ // Try direct import first (TS loader active)
184
+ try {
185
+ const dyn = await importDefault(fileUrl);
186
+ if (dyn)
187
+ return dyn;
188
+ }
189
+ catch {
190
+ /* fall through */
191
+ }
192
+ const stat = await fs.stat(absPath);
193
+ const hash = cacheHash(absPath, stat.mtimeMs);
194
+ const cacheDir = path.resolve('.tsbuild', cacheDirName);
195
+ await fs.ensureDir(cacheDir);
196
+ const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
197
+ // Try esbuild
198
+ try {
199
+ const esbuild = (await import('esbuild'));
200
+ await esbuild.build({
201
+ entryPoints: [absPath],
202
+ bundle: true,
203
+ platform: 'node',
204
+ format: 'esm',
205
+ target: 'node20',
206
+ outfile: cacheFile,
207
+ sourcemap: false,
208
+ logLevel: 'silent',
209
+ });
210
+ const result = await importDefault(url.pathToFileURL(cacheFile).toString());
211
+ // Best-effort: trim older cache files for this source.
212
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
213
+ return result;
214
+ }
215
+ catch {
216
+ /* fall through to TS transpile */
217
+ }
218
+ // TypeScript transpile fallback
219
+ try {
220
+ const ts = (await import('typescript'));
221
+ const code = await fs.readFile(absPath, 'utf-8');
222
+ const out = ts.transpileModule(code, {
223
+ compilerOptions: {
224
+ module: 'ESNext',
225
+ target: 'ES2022',
226
+ moduleResolution: 'NodeNext',
227
+ },
228
+ }).outputText;
229
+ await fs.writeFile(cacheFile, out, 'utf-8');
230
+ const result = await importDefault(url.pathToFileURL(cacheFile).toString());
231
+ // Best-effort: trim older cache files for this source.
232
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
233
+ return result;
234
+ }
235
+ catch {
236
+ // Caller decides final error wording; rethrow for upstream mapping.
237
+ throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
238
+ }
239
+ };
240
+
241
+ /** src/env/dynamic.ts
242
+ * Helpers for applying and loading dynamic variables (JS/TS).
243
+ *
244
+ * Requirements addressed:
245
+ * - Single service to apply a dynamic map progressively.
246
+ * - Single service to load a JS/TS dynamic module with robust fallbacks (util/loadModuleDefault).
247
+ * - Unify error messaging so callers show consistent guidance.
248
+ */
249
+ /**
250
+ * Apply a dynamic map to the target progressively.
251
+ * - Functions receive (target, env) and may return string | undefined.
252
+ * - Literals are assigned directly (including undefined).
253
+ */
254
+ function applyDynamicMap(target, map, env) {
255
+ if (!map)
256
+ return;
257
+ for (const key of Object.keys(map)) {
258
+ const val = typeof map[key] === 'function'
259
+ ? map[key](target, env)
260
+ : map[key];
261
+ Object.assign(target, { [key]: val });
262
+ }
263
+ }
264
+ /**
265
+ * Load a default-export dynamic map from a JS/TS file and apply it.
266
+ * Uses util/loadModuleDefault for robust TS handling (direct import, esbuild,
267
+ * typescript.transpile fallback).
268
+ *
269
+ * Error behavior:
270
+ * - On failure to load/compile/evaluate the module, throws a unified message:
271
+ * "Unable to load dynamic TypeScript file: <absPath>. Install 'esbuild'..."
272
+ */
273
+ async function loadAndApplyDynamic(target, absPath, env, cacheDirName) {
274
+ if (!(await fs.exists(absPath)))
275
+ return;
276
+ let dyn;
277
+ try {
278
+ dyn = await loadModuleDefault(absPath, cacheDirName);
279
+ }
280
+ catch {
281
+ // Preserve legacy/clear guidance used by tests and docs.
282
+ throw new Error(`Unable to load dynamic TypeScript file: ${absPath}. ` +
283
+ `Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
284
+ }
285
+ applyDynamicMap(target, dyn, env);
286
+ }
120
287
 
121
288
  const applyKv = (current, kv) => {
122
289
  if (!kv || Object.keys(kv).length === 0)
@@ -132,16 +299,8 @@ const applyConfigSlice = (current, cfg, env) => {
132
299
  const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
133
300
  return applyKv(afterGlobal, envKv);
134
301
  };
135
- /**
136
- * Overlay config-provided values onto a base ProcessEnv using precedence axes:
137
- * - kind: env \> global
138
- * - privacy: local \> public
139
- * - source: project \> packaged \> base
140
- *
141
- * Programmatic explicit vars (if provided) override all config slices.
142
- * Progressive expansion is applied within each slice.
143
- */
144
- const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
302
+ function overlayEnv(args) {
303
+ const { base, env, configs } = args;
145
304
  let current = { ...base };
146
305
  // Source: packaged (public -> local)
147
306
  current = applyConfigSlice(current, configs.packaged, env);
@@ -151,11 +310,11 @@ const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
151
310
  current = applyConfigSlice(current, configs.project?.public, env);
152
311
  current = applyConfigSlice(current, configs.project?.local, env);
153
312
  // Programmatic explicit vars (top of static tier)
154
- if (programmaticVars) {
155
- const toApply = Object.fromEntries(Object.entries(programmaticVars).filter(([_k, v]) => typeof v === 'string'));
313
+ if ('programmaticVars' in args) {
314
+ const toApply = Object.fromEntries(Object.entries(args.programmaticVars).filter(([_k, v]) => typeof v === 'string'));
156
315
  current = applyKv(current, toApply);
157
316
  }
158
317
  return current;
159
- };
318
+ }
160
319
 
161
- export { overlayEnv };
320
+ export { applyDynamicMap, loadAndApplyDynamic, overlayEnv };