@karmaniverous/get-dotenv 6.0.0-1 → 6.1.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 (62) hide show
  1. package/README.md +91 -379
  2. package/dist/cli.d.ts +569 -0
  3. package/dist/cli.mjs +18877 -0
  4. package/dist/cliHost.d.ts +528 -184
  5. package/dist/cliHost.mjs +1977 -1428
  6. package/dist/config.d.ts +191 -14
  7. package/dist/config.mjs +266 -81
  8. package/dist/env-overlay.d.ts +223 -16
  9. package/dist/env-overlay.mjs +185 -4
  10. package/dist/getdotenv.cli.mjs +18025 -3196
  11. package/dist/index.d.ts +623 -256
  12. package/dist/index.mjs +18045 -3206
  13. package/dist/plugins-aws.d.ts +221 -91
  14. package/dist/plugins-aws.mjs +2411 -369
  15. package/dist/plugins-batch.d.ts +300 -103
  16. package/dist/plugins-batch.mjs +2560 -484
  17. package/dist/plugins-cmd.d.ts +229 -106
  18. package/dist/plugins-cmd.mjs +2518 -790
  19. package/dist/plugins-init.d.ts +221 -95
  20. package/dist/plugins-init.mjs +2170 -105
  21. package/dist/plugins.d.ts +246 -125
  22. package/dist/plugins.mjs +17941 -1968
  23. package/dist/templates/cli/index.ts +25 -0
  24. package/{templates/cli/ts → dist/templates/cli}/plugins/hello.ts +13 -9
  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 +42 -0
  39. package/dist/templates/index.ts +25 -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 +42 -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 +42 -39
  53. package/templates/cli/index.ts +25 -0
  54. package/templates/cli/plugins/hello.ts +42 -0
  55. package/templates/config/js/getdotenv.config.js +8 -3
  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 +8 -3
  59. package/templates/config/yaml/public/getdotenv.config.yaml +0 -3
  60. package/dist/plugins-demo.d.ts +0 -204
  61. package/dist/plugins-demo.mjs +0 -496
  62. package/templates/cli/ts/index.ts +0 -9
package/dist/config.d.ts CHANGED
@@ -1,36 +1,188 @@
1
1
  /**
2
- * Scripts table shape (configurable shell type).
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
+ */
22
+ shell?: string | boolean;
23
+ /** Whether to load variables into `process.env`. */
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. */
38
+ log?: 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.
3
90
  */
4
- type ScriptsTable<TShell extends string | boolean = string | boolean> = Record<string, string | {
91
+ interface ScriptDef<TShell extends string | boolean = string | boolean> {
92
+ /** The command string to execute. */
5
93
  cmd: string;
94
+ /** Shell override for this script. */
6
95
  shell?: TShell | undefined;
7
- }>;
96
+ }
97
+ /**
98
+ * Scripts table shape.
99
+ */
100
+ type ScriptsTable<TShell extends string | boolean = string | boolean> = Record<string, string | ScriptDef<TShell>>;
8
101
 
102
+ /**
103
+ * Canonical programmatic options and helpers for get-dotenv.
104
+ *
105
+ * Requirements addressed:
106
+ * - GetDotenvOptions derives from the Zod schema output (single source of truth).
107
+ * - Removed deprecated/compat flags from the public shape (e.g., useConfigLoader).
108
+ * - Provide Vars-aware defineDynamic and a typed config builder defineGetDotenvConfig\<Vars, Env\>().
109
+ * - Preserve existing behavior for defaults resolution and compat converters.
110
+ */
111
+
112
+ /**
113
+ * A minimal representation of an environment key/value mapping.
114
+ * Values may be `undefined` to represent "unset".
115
+ */
116
+ type ProcessEnv = Record<string, string | undefined>;
117
+
118
+ /**
119
+ * Unify Scripts via the generic ScriptsTable<TShell> so shell types propagate.
120
+ */
9
121
  type Scripts = ScriptsTable;
10
122
 
11
123
  type GetDotenvConfigResolved = {
12
- dotenvToken?: string;
13
- privateToken?: string;
14
- paths?: string[];
15
- loadProcess?: boolean;
16
- log?: boolean;
17
- shell?: string | boolean;
124
+ /**
125
+ * Help-time/runtime root defaults applied by the host (collapsed families; CLI‑like).
126
+ */
127
+ rootOptionDefaults?: Partial<RootOptionsShape>;
128
+ /**
129
+ * Help-time visibility for root flags; when a key is false the corresponding
130
+ * option(s) are hidden in root help output.
131
+ */
132
+ rootOptionVisibility?: Partial<Record<keyof RootOptionsShape, boolean>>;
133
+ /**
134
+ * Merged scripts table for resolving commands and shell behavior.
135
+ * Entries may be strings or objects with `cmd` and optional `shell`.
136
+ */
18
137
  scripts?: Scripts;
138
+ /**
139
+ * Keys required to be present in the final composed environment.
140
+ * Validation occurs after overlays and dynamics.
141
+ */
19
142
  requiredKeys?: string[];
143
+ /**
144
+ * Optional validation schema (e.g., Zod). When present and it exposes
145
+ * `safeParse(finalEnv)`, the host executes it once after overlays.
146
+ */
20
147
  schema?: unknown;
148
+ /**
149
+ * Public global variables (string‑only).
150
+ */
21
151
  vars?: Record<string, string>;
152
+ /**
153
+ * Public per‑environment variables (string‑only).
154
+ */
22
155
  envVars?: Record<string, Record<string, string>>;
156
+ /**
157
+ * Dynamic variable definitions (JS/TS configs only).
158
+ */
23
159
  dynamic?: unknown;
160
+ /**
161
+ * Per‑plugin configuration slices keyed by realized mount path
162
+ * (for example, "aws/whoami").
163
+ */
24
164
  plugins?: Record<string, unknown>;
25
165
  };
26
166
 
167
+ /**
168
+ * Privacy scope of a configuration file ('public' is checked into git, 'local' is gitignored).
169
+ */
27
170
  type ConfigPrivacy = 'public' | 'local';
171
+ /**
172
+ * Origin scope of a configuration file ('packaged' inside the library, 'project' in the consumer repo).
173
+ */
28
174
  type ConfigScope = 'packaged' | 'project';
29
- type ConfigFile = {
175
+ /**
176
+ * Represents a discovered configuration file.
177
+ */
178
+ interface ConfigFile {
179
+ /** Absolute path to the config file. */
30
180
  path: string;
181
+ /** Privacy scope (public vs local). */
31
182
  privacy: ConfigPrivacy;
183
+ /** Origin scope (packaged vs project). */
32
184
  scope: ConfigScope;
33
- };
185
+ }
34
186
  /**
35
187
  * Discover JSON/YAML config files in the packaged root and project root.
36
188
  * Order: packaged public → project public → project local. */
@@ -43,18 +195,43 @@ declare const discoverConfigFiles: (importMetaUrl?: string) => Promise<ConfigFil
43
195
  * For JS/TS: default export is loaded; "dynamic" is allowed.
44
196
  */
45
197
  declare const loadConfigFile: (filePath: string) => Promise<GetDotenvConfigResolved>;
46
- type ResolvedConfigSources = {
198
+ interface ResolvedConfigSources {
199
+ /** Configuration from the package root (public only). */
47
200
  packaged?: GetDotenvConfigResolved;
201
+ /** Configuration from the project root. */
48
202
  project?: {
203
+ /** Project public configuration. */
49
204
  public?: GetDotenvConfigResolved;
205
+ /** Project local configuration. */
50
206
  local?: GetDotenvConfigResolved;
51
207
  };
52
- };
208
+ }
53
209
  /**
54
210
  * Discover and load configs into resolved shapes, ordered by scope/privacy.
55
211
  * JSON/YAML/JS/TS supported; first match per scope/privacy applies.
56
212
  */
57
213
  declare const resolveGetDotenvConfigSources: (importMetaUrl?: string) => Promise<ResolvedConfigSources>;
214
+ /**
215
+ * Utility primarily for tests: create a file: URL string from a path.
216
+ * @param p - File path.
217
+ */
58
218
  declare const toFileUrl: (p: string) => string;
59
219
 
60
- export { discoverConfigFiles, loadConfigFile, resolveGetDotenvConfigSources, toFileUrl };
220
+ /**
221
+ * Validate a composed env against config-provided validation surfaces.
222
+ * Precedence for validation definitions:
223
+ * project.local -\> project.public -\> packaged
224
+ *
225
+ * Behavior:
226
+ * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
227
+ * - Else if `requiredKeys` is present, check presence (value !== undefined).
228
+ * - Returns a flat list of issue strings; caller decides warn vs fail.
229
+ *
230
+ * @param finalEnv - Final composed environment to validate.
231
+ * @param sources - Resolved config sources providing `schema` and/or `requiredKeys`.
232
+ * @returns A list of human-readable issue strings (empty when valid).
233
+ */
234
+ declare const validateEnvAgainstSources: (finalEnv: ProcessEnv, sources: ResolvedConfigSources) => string[];
235
+
236
+ export { discoverConfigFiles, loadConfigFile, resolveGetDotenvConfigSources, toFileUrl, validateEnvAgainstSources };
237
+ export type { ConfigFile, ConfigPrivacy, ConfigScope, ResolvedConfigSources };
package/dist/config.mjs CHANGED
@@ -1,30 +1,98 @@
1
1
  import fs from 'fs-extra';
2
2
  import { packageDirectory } from 'package-directory';
3
3
  import path, { join, extname } from 'path';
4
- import { pathToFileURL, fileURLToPath } from 'url';
4
+ import url, { pathToFileURL, fileURLToPath } from 'url';
5
5
  import YAML from 'yaml';
6
6
  import { z } from 'zod';
7
+ import { createHash } from 'crypto';
7
8
 
9
+ /**
10
+ * Zod schemas for programmatic GetDotenv options.
11
+ *
12
+ * Canonical source of truth for options shape. Public types are derived
13
+ * from these schemas (see consumers via z.output\<\>).
14
+ */
15
+ /**
16
+ * Minimal process env representation used by options and helpers.
17
+ * Values may be `undefined` to indicate "unset".
18
+ */
19
+ const processEnvSchema = z.record(z.string(), z.string().optional());
20
+ // RAW: all fields optional — undefined means "inherit" from lower layers.
21
+ const getDotenvOptionsSchemaRaw = z.object({
22
+ defaultEnv: z.string().optional(),
23
+ dotenvToken: z.string().optional(),
24
+ dynamicPath: z.string().optional(),
25
+ // Dynamic map is intentionally wide for now; refine once sources are normalized.
26
+ dynamic: z.record(z.string(), z.unknown()).optional(),
27
+ env: z.string().optional(),
28
+ excludeDynamic: z.boolean().optional(),
29
+ excludeEnv: z.boolean().optional(),
30
+ excludeGlobal: z.boolean().optional(),
31
+ excludePrivate: z.boolean().optional(),
32
+ excludePublic: z.boolean().optional(),
33
+ loadProcess: z.boolean().optional(),
34
+ log: z.boolean().optional(),
35
+ logger: z.unknown().default(console),
36
+ outputPath: z.string().optional(),
37
+ paths: z.array(z.string()).optional(),
38
+ privateToken: z.string().optional(),
39
+ vars: processEnvSchema.optional(),
40
+ });
41
+
42
+ /**
43
+ * Zod schemas for CLI-facing GetDotenv options (raw/resolved stubs).
44
+ *
45
+ * RAW allows stringly inputs (paths/vars + splitters). RESOLVED will later
46
+ * reflect normalized types (paths: string[], vars: ProcessEnv), applied in the
47
+ * CLI resolution pipeline.
48
+ */
49
+ const getDotenvCliOptionsSchemaRaw = getDotenvOptionsSchemaRaw.extend({
50
+ // CLI-specific fields (stringly inputs before preprocessing)
51
+ debug: z.boolean().optional(),
52
+ strict: z.boolean().optional(),
53
+ capture: z.boolean().optional(),
54
+ trace: z.union([z.boolean(), z.array(z.string())]).optional(),
55
+ redact: z.boolean().optional(),
56
+ warnEntropy: z.boolean().optional(),
57
+ entropyThreshold: z.number().optional(),
58
+ entropyMinLength: z.number().optional(),
59
+ entropyWhitelist: z.array(z.string()).optional(),
60
+ redactPatterns: z.array(z.string()).optional(),
61
+ paths: z.string().optional(),
62
+ pathsDelimiter: z.string().optional(),
63
+ pathsDelimiterPattern: z.string().optional(),
64
+ scripts: z.record(z.string(), z.unknown()).optional(),
65
+ shell: z.union([z.boolean(), z.string()]).optional(),
66
+ vars: z.string().optional(),
67
+ varsAssignor: z.string().optional(),
68
+ varsAssignorPattern: z.string().optional(),
69
+ varsDelimiter: z.string().optional(),
70
+ varsDelimiterPattern: z.string().optional(),
71
+ });
72
+
73
+ const visibilityMap = z.record(z.string(), z.boolean());
8
74
  /**
9
75
  * Zod schemas for configuration files discovered by the new loader.
10
76
  *
11
77
  * Notes:
12
- * - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
13
- * - RESOLVED: normalized shapes (paths always string[]).
14
- * - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS-only).
78
+ * - RAW: all fields optional; only allowed top-level keys are:
79
+ * - rootOptionDefaults, rootOptionVisibility
80
+ * - scripts, vars, envVars
81
+ * - dynamic (JS/TS only), schema (JS/TS only)
82
+ * - plugins, requiredKeys
83
+ * - RESOLVED: mirrors RAW (no path normalization).
84
+ * - For JSON/YAML configs, the loader rejects "dynamic" and "schema" (JS/TS only).
15
85
  */
16
86
  // String-only env value map
17
87
  const stringMap = z.record(z.string(), z.string());
18
88
  const envStringMap = z.record(z.string(), stringMap);
19
- // Allow string[] or single string for "paths" in RAW; normalize later.
20
- const rawPathsSchema = z.union([z.array(z.string()), z.string()]).optional();
89
+ /**
90
+ * Raw configuration schema for get‑dotenv config files (JSON/YAML/JS/TS).
91
+ * Validates allowed top‑level keys without performing path normalization.
92
+ */
21
93
  const getDotenvConfigSchemaRaw = z.object({
22
- dotenvToken: z.string().optional(),
23
- privateToken: z.string().optional(),
24
- paths: rawPathsSchema,
25
- loadProcess: z.boolean().optional(),
26
- log: z.boolean().optional(),
27
- shell: z.union([z.string(), z.boolean()]).optional(),
94
+ rootOptionDefaults: getDotenvCliOptionsSchemaRaw.optional(),
95
+ rootOptionVisibility: visibilityMap.optional(),
28
96
  scripts: z.record(z.string(), z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
29
97
  requiredKeys: z.array(z.string()).optional(),
30
98
  schema: z.unknown().optional(), // JS/TS-only; loader rejects in JSON/YAML
@@ -35,109 +103,163 @@ const getDotenvConfigSchemaRaw = z.object({
35
103
  // Per-plugin config bag; validated by plugins/host when used.
36
104
  plugins: z.record(z.string(), z.unknown()).optional(),
37
105
  });
38
- // Normalize paths to string[]
39
- const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
40
- const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => ({
41
- ...raw,
42
- paths: normalizePaths(raw.paths),
43
- }));
106
+ /**
107
+ * Resolved configuration schema which preserves the raw shape while narrowing
108
+ * the output to {@link GetDotenvConfigResolved}. Consumers get a strongly typed
109
+ * object, while the underlying validation remains Zod‑driven.
110
+ */
111
+ const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => raw);
44
112
 
45
- // Discovery candidates (first match wins per scope/privacy).
46
- // Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
47
- const PUBLIC_FILENAMES = [
48
- 'getdotenv.config.json',
49
- 'getdotenv.config.yaml',
50
- 'getdotenv.config.yml',
51
- 'getdotenv.config.js',
52
- 'getdotenv.config.mjs',
53
- 'getdotenv.config.cjs',
54
- 'getdotenv.config.ts',
55
- 'getdotenv.config.mts',
56
- 'getdotenv.config.cts',
57
- ];
58
- const LOCAL_FILENAMES = [
59
- 'getdotenv.config.local.json',
60
- 'getdotenv.config.local.yaml',
61
- 'getdotenv.config.local.yml',
62
- 'getdotenv.config.local.js',
63
- 'getdotenv.config.local.mjs',
64
- 'getdotenv.config.local.cjs',
65
- 'getdotenv.config.local.ts',
66
- 'getdotenv.config.local.mts',
67
- 'getdotenv.config.local.cts',
68
- ];
69
- const isYaml = (p) => ['.yaml', '.yml'].includes(extname(p).toLowerCase());
70
- const isJson = (p) => extname(p).toLowerCase() === '.json';
71
- const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(extname(p).toLowerCase());
72
- // --- Internal JS/TS module loader helpers (default export) ---
73
113
  const importDefault = async (fileUrl) => {
74
114
  const mod = (await import(fileUrl));
75
115
  return mod.default;
76
116
  };
77
- const cacheName = (absPath, suffix) => {
78
- // sanitized filename with suffix; recompile on mtime changes not tracked here (simplified)
79
- const base = path.basename(absPath).replace(/[^a-zA-Z0-9._-]/g, '_');
80
- return `${base}.${suffix}.mjs`;
81
- };
82
- const ensureDir = async (dir) => {
83
- await fs.ensureDir(dir);
84
- return dir;
117
+ const cacheHash = (absPath, mtimeMs) => createHash('sha1')
118
+ .update(absPath)
119
+ .update(String(mtimeMs))
120
+ .digest('hex')
121
+ .slice(0, 12);
122
+ /**
123
+ * Remove older compiled cache files for a given source base name, keeping
124
+ * at most `keep` most-recent files. Errors are ignored by design.
125
+ */
126
+ const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Number.parseInt(process.env.GETDOTENV_CACHE_KEEP ?? '2'))) => {
127
+ try {
128
+ const entries = await fs.readdir(cacheDir);
129
+ const mine = entries
130
+ .filter((f) => f.startsWith(`${baseName}.`) && f.endsWith('.mjs'))
131
+ .map((f) => path.join(cacheDir, f));
132
+ if (mine.length <= keep)
133
+ return;
134
+ const stats = await Promise.all(mine.map(async (p) => ({ p, mtimeMs: (await fs.stat(p)).mtimeMs })));
135
+ stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
136
+ const toDelete = stats.slice(keep).map((s) => s.p);
137
+ await Promise.all(toDelete.map(async (p) => {
138
+ try {
139
+ await fs.remove(p);
140
+ }
141
+ catch {
142
+ // best-effort cleanup
143
+ }
144
+ }));
145
+ }
146
+ catch {
147
+ // best-effort cleanup
148
+ }
85
149
  };
86
- const loadJsTsDefault = async (absPath) => {
87
- const fileUrl = pathToFileURL(absPath).toString();
88
- const ext = extname(absPath).toLowerCase();
89
- if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
150
+ /**
151
+ * Load a module default export from a JS/TS file with robust fallbacks.
152
+ *
153
+ * Behavior by extension:
154
+ *
155
+ * - `.js`/`.mjs`/`.cjs`: direct dynamic import.
156
+ * - `.ts`/`.mts`/`.cts`/`.tsx`:
157
+ * - try direct dynamic import (when a TS loader is active),
158
+ * - else compile via `esbuild` to a cached `.mjs` file and import,
159
+ * - else fallback to `typescript.transpileModule` for simple modules.
160
+ *
161
+ * @typeParam T - Type of the expected default export.
162
+ * @param absPath - Absolute path to the source file.
163
+ * @param cacheDirName - Cache subfolder under `.tsbuild/`.
164
+ * @returns A `Promise\<T | undefined\>` resolving to the default export (if any).
165
+ */
166
+ const loadModuleDefault = async (absPath, cacheDirName) => {
167
+ const ext = path.extname(absPath).toLowerCase();
168
+ const fileUrl = url.pathToFileURL(absPath).toString();
169
+ if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
90
170
  return importDefault(fileUrl);
91
171
  }
92
- // Try direct import first in case a TS loader is active.
172
+ // Try direct import first (TS loader active)
93
173
  try {
94
- const val = await importDefault(fileUrl);
95
- if (val)
96
- return val;
174
+ const dyn = await importDefault(fileUrl);
175
+ if (dyn)
176
+ return dyn;
97
177
  }
98
178
  catch {
99
- /* fallthrough */
179
+ /* fall through */
100
180
  }
101
- // esbuild bundle to a temp ESM file
181
+ const stat = await fs.stat(absPath);
182
+ const hash = cacheHash(absPath, stat.mtimeMs);
183
+ const cacheDir = path.resolve('.tsbuild', cacheDirName);
184
+ await fs.ensureDir(cacheDir);
185
+ const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
186
+ // Try esbuild
102
187
  try {
103
188
  const esbuild = (await import('esbuild'));
104
- const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
105
- const outfile = path.join(outDir, cacheName(absPath, 'bundle'));
106
189
  await esbuild.build({
107
190
  entryPoints: [absPath],
108
191
  bundle: true,
109
192
  platform: 'node',
110
193
  format: 'esm',
111
194
  target: 'node20',
112
- outfile,
195
+ outfile: cacheFile,
113
196
  sourcemap: false,
114
197
  logLevel: 'silent',
115
198
  });
116
- return await importDefault(pathToFileURL(outfile).toString());
199
+ const result = await importDefault(url.pathToFileURL(cacheFile).toString());
200
+ // Best-effort: trim older cache files for this source.
201
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
202
+ return result;
117
203
  }
118
204
  catch {
119
- /* fallthrough to TS transpile */
205
+ /* fall through to TS transpile */
120
206
  }
121
- // typescript.transpileModule simple transpile (single-file)
207
+ // TypeScript transpile fallback
122
208
  try {
123
209
  const ts = (await import('typescript'));
124
- const src = await fs.readFile(absPath, 'utf-8');
125
- const out = ts.transpileModule(src, {
210
+ const code = await fs.readFile(absPath, 'utf-8');
211
+ const out = ts.transpileModule(code, {
126
212
  compilerOptions: {
127
213
  module: 'ESNext',
128
214
  target: 'ES2022',
129
215
  moduleResolution: 'NodeNext',
130
216
  },
131
217
  }).outputText;
132
- const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
133
- const outfile = path.join(outDir, cacheName(absPath, 'ts'));
134
- await fs.writeFile(outfile, out, 'utf-8');
135
- return await importDefault(pathToFileURL(outfile).toString());
218
+ await fs.writeFile(cacheFile, out, 'utf-8');
219
+ const result = await importDefault(url.pathToFileURL(cacheFile).toString());
220
+ // Best-effort: trim older cache files for this source.
221
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
222
+ return result;
136
223
  }
137
224
  catch {
138
- throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
225
+ // Caller decides final error wording; rethrow for upstream mapping.
226
+ throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
139
227
  }
140
228
  };
229
+
230
+ /**
231
+ * @packageDocumentation
232
+ * Configuration discovery and loading for get‑dotenv. Discovers config files
233
+ * in the packaged root and project root, loads JSON/YAML/JS/TS documents, and
234
+ * validates them against Zod schemas.
235
+ */
236
+ // Discovery candidates (first match wins per scope/privacy).
237
+ // Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
238
+ const PUBLIC_FILENAMES = [
239
+ 'getdotenv.config.json',
240
+ 'getdotenv.config.yaml',
241
+ 'getdotenv.config.yml',
242
+ 'getdotenv.config.js',
243
+ 'getdotenv.config.mjs',
244
+ 'getdotenv.config.cjs',
245
+ 'getdotenv.config.ts',
246
+ 'getdotenv.config.mts',
247
+ 'getdotenv.config.cts',
248
+ ];
249
+ const LOCAL_FILENAMES = [
250
+ 'getdotenv.config.local.json',
251
+ 'getdotenv.config.local.yaml',
252
+ 'getdotenv.config.local.yml',
253
+ 'getdotenv.config.local.js',
254
+ 'getdotenv.config.local.mjs',
255
+ 'getdotenv.config.local.cjs',
256
+ 'getdotenv.config.local.ts',
257
+ 'getdotenv.config.local.mts',
258
+ 'getdotenv.config.local.cts',
259
+ ];
260
+ const isYaml = (p) => ['.yaml', '.yml'].includes(extname(p).toLowerCase());
261
+ const isJson = (p) => extname(p).toLowerCase() === '.json';
262
+ const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(extname(p).toLowerCase());
141
263
  /**
142
264
  * Discover JSON/YAML config files in the packaged root and project root.
143
265
  * Order: packaged public → project public → project local. */
@@ -190,8 +312,8 @@ const loadConfigFile = async (filePath) => {
190
312
  try {
191
313
  const abs = path.resolve(filePath);
192
314
  if (isJsOrTs(abs)) {
193
- // JS/TS support: load default export via robust pipeline.
194
- const mod = await loadJsTsDefault(abs);
315
+ // JS/TS support: load default export via shared robust pipeline.
316
+ const mod = await loadModuleDefault(abs, 'getdotenv-config');
195
317
  raw = mod ?? {};
196
318
  }
197
319
  else {
@@ -241,7 +363,70 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
241
363
  }
242
364
  return result;
243
365
  };
244
- // Utility primarily for tests: create a file: URL string from a path
366
+ /**
367
+ * Utility primarily for tests: create a file: URL string from a path.
368
+ * @param p - File path.
369
+ */
245
370
  const toFileUrl = (p) => pathToFileURL(path.resolve(p)).toString();
246
371
 
247
- export { discoverConfigFiles, loadConfigFile, resolveGetDotenvConfigSources, toFileUrl };
372
+ /**
373
+ * Validate a composed env against config-provided validation surfaces.
374
+ * Precedence for validation definitions:
375
+ * project.local -\> project.public -\> packaged
376
+ *
377
+ * Behavior:
378
+ * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
379
+ * - Else if `requiredKeys` is present, check presence (value !== undefined).
380
+ * - Returns a flat list of issue strings; caller decides warn vs fail.
381
+ *
382
+ * @param finalEnv - Final composed environment to validate.
383
+ * @param sources - Resolved config sources providing `schema` and/or `requiredKeys`.
384
+ * @returns A list of human-readable issue strings (empty when valid).
385
+ */
386
+ const validateEnvAgainstSources = (finalEnv, sources) => {
387
+ const pick = (getter) => {
388
+ const pl = sources.project?.local;
389
+ const pp = sources.project?.public;
390
+ const pk = sources.packaged;
391
+ return ((pl && getter(pl)) ||
392
+ (pp && getter(pp)) ||
393
+ (pk && getter(pk)) ||
394
+ undefined);
395
+ };
396
+ const schema = pick((cfg) => cfg['schema']);
397
+ if (schema &&
398
+ typeof schema.safeParse === 'function') {
399
+ try {
400
+ const parsed = schema.safeParse(finalEnv);
401
+ if (!parsed.success) {
402
+ // Try to render zod-style issues when available.
403
+ const err = parsed.error;
404
+ const issues = Array.isArray(err.issues) && err.issues.length > 0
405
+ ? err.issues.map((i) => {
406
+ const path = Array.isArray(i.path) ? i.path.join('.') : '';
407
+ const msg = i.message ?? 'Invalid value';
408
+ return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
409
+ })
410
+ : ['[schema] validation failed'];
411
+ return issues;
412
+ }
413
+ return [];
414
+ }
415
+ catch {
416
+ // If schema invocation fails, surface a single diagnostic.
417
+ return [
418
+ '[schema] validation failed (unable to execute schema.safeParse)',
419
+ ];
420
+ }
421
+ }
422
+ const requiredKeys = pick((cfg) => cfg['requiredKeys']);
423
+ if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
424
+ const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
425
+ if (missing.length > 0) {
426
+ return missing.map((k) => `[requiredKeys] missing: ${k}`);
427
+ }
428
+ }
429
+ return [];
430
+ };
431
+
432
+ export { discoverConfigFiles, loadConfigFile, resolveGetDotenvConfigSources, toFileUrl, validateEnvAgainstSources };