@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
package/dist/config.d.ts CHANGED
@@ -1,31 +1,188 @@
1
- type Scripts = Record<string, string | {
2
- cmd: string;
1
+ /**
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
+ */
3
22
  shell?: string | boolean;
4
- }>;
5
-
6
- type GetDotenvConfigResolved = {
7
- dotenvToken?: string;
8
- privateToken?: string;
9
- paths?: string[];
23
+ /** Whether to load variables into `process.env`. */
10
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. */
11
38
  log?: boolean;
12
- 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
+ * 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
+ */
121
+ type Scripts = ScriptsTable;
122
+
123
+ type GetDotenvConfigResolved = {
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
+ */
13
137
  scripts?: Scripts;
138
+ /**
139
+ * Keys required to be present in the final composed environment.
140
+ * Validation occurs after overlays and dynamics.
141
+ */
14
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
+ */
15
147
  schema?: unknown;
148
+ /**
149
+ * Public global variables (string‑only).
150
+ */
16
151
  vars?: Record<string, string>;
152
+ /**
153
+ * Public per‑environment variables (string‑only).
154
+ */
17
155
  envVars?: Record<string, Record<string, string>>;
156
+ /**
157
+ * Dynamic variable definitions (JS/TS configs only).
158
+ */
18
159
  dynamic?: unknown;
160
+ /**
161
+ * Per‑plugin configuration slices keyed by realized mount path
162
+ * (for example, "aws/whoami").
163
+ */
19
164
  plugins?: Record<string, unknown>;
20
165
  };
21
166
 
167
+ /**
168
+ * Privacy scope of a configuration file ('public' is checked into git, 'local' is gitignored).
169
+ */
22
170
  type ConfigPrivacy = 'public' | 'local';
171
+ /**
172
+ * Origin scope of a configuration file ('packaged' inside the library, 'project' in the consumer repo).
173
+ */
23
174
  type ConfigScope = 'packaged' | 'project';
24
- type ConfigFile = {
175
+ /**
176
+ * Represents a discovered configuration file.
177
+ */
178
+ interface ConfigFile {
179
+ /** Absolute path to the config file. */
25
180
  path: string;
181
+ /** Privacy scope (public vs local). */
26
182
  privacy: ConfigPrivacy;
183
+ /** Origin scope (packaged vs project). */
27
184
  scope: ConfigScope;
28
- };
185
+ }
29
186
  /**
30
187
  * Discover JSON/YAML config files in the packaged root and project root.
31
188
  * Order: packaged public → project public → project local. */
@@ -38,18 +195,39 @@ declare const discoverConfigFiles: (importMetaUrl?: string) => Promise<ConfigFil
38
195
  * For JS/TS: default export is loaded; "dynamic" is allowed.
39
196
  */
40
197
  declare const loadConfigFile: (filePath: string) => Promise<GetDotenvConfigResolved>;
41
- type ResolvedConfigSources = {
198
+ interface ResolvedConfigSources {
199
+ /** Configuration from the package root (public only). */
42
200
  packaged?: GetDotenvConfigResolved;
201
+ /** Configuration from the project root. */
43
202
  project?: {
203
+ /** Project public configuration. */
44
204
  public?: GetDotenvConfigResolved;
205
+ /** Project local configuration. */
45
206
  local?: GetDotenvConfigResolved;
46
207
  };
47
- };
208
+ }
48
209
  /**
49
210
  * Discover and load configs into resolved shapes, ordered by scope/privacy.
50
211
  * JSON/YAML/JS/TS supported; first match per scope/privacy applies.
51
212
  */
52
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
+ */
53
218
  declare const toFileUrl: (p: string) => string;
54
219
 
55
- 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
+ declare const validateEnvAgainstSources: (finalEnv: ProcessEnv, sources: ResolvedConfigSources) => string[];
231
+
232
+ export { discoverConfigFiles, loadConfigFile, resolveGetDotenvConfigSources, toFileUrl, validateEnvAgainstSources };
233
+ 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,157 @@ 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
+ * - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
153
+ * 1) try direct import (if a TS loader is active),
154
+ * 2) esbuild bundle to a temp ESM file,
155
+ * 3) typescript.transpileModule fallback for simple modules.
156
+ *
157
+ * @param absPath - absolute path to source file
158
+ * @param cacheDirName - cache subfolder under .tsbuild
159
+ */
160
+ const loadModuleDefault = async (absPath, cacheDirName) => {
161
+ const ext = path.extname(absPath).toLowerCase();
162
+ const fileUrl = url.pathToFileURL(absPath).toString();
163
+ if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
90
164
  return importDefault(fileUrl);
91
165
  }
92
- // Try direct import first in case a TS loader is active.
166
+ // Try direct import first (TS loader active)
93
167
  try {
94
- const val = await importDefault(fileUrl);
95
- if (val)
96
- return val;
168
+ const dyn = await importDefault(fileUrl);
169
+ if (dyn)
170
+ return dyn;
97
171
  }
98
172
  catch {
99
- /* fallthrough */
173
+ /* fall through */
100
174
  }
101
- // esbuild bundle to a temp ESM file
175
+ const stat = await fs.stat(absPath);
176
+ const hash = cacheHash(absPath, stat.mtimeMs);
177
+ const cacheDir = path.resolve('.tsbuild', cacheDirName);
178
+ await fs.ensureDir(cacheDir);
179
+ const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
180
+ // Try esbuild
102
181
  try {
103
182
  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
183
  await esbuild.build({
107
184
  entryPoints: [absPath],
108
185
  bundle: true,
109
186
  platform: 'node',
110
187
  format: 'esm',
111
188
  target: 'node20',
112
- outfile,
189
+ outfile: cacheFile,
113
190
  sourcemap: false,
114
191
  logLevel: 'silent',
115
192
  });
116
- return await importDefault(pathToFileURL(outfile).toString());
193
+ const result = await importDefault(url.pathToFileURL(cacheFile).toString());
194
+ // Best-effort: trim older cache files for this source.
195
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
196
+ return result;
117
197
  }
118
198
  catch {
119
- /* fallthrough to TS transpile */
199
+ /* fall through to TS transpile */
120
200
  }
121
- // typescript.transpileModule simple transpile (single-file)
201
+ // TypeScript transpile fallback
122
202
  try {
123
203
  const ts = (await import('typescript'));
124
- const src = await fs.readFile(absPath, 'utf-8');
125
- const out = ts.transpileModule(src, {
204
+ const code = await fs.readFile(absPath, 'utf-8');
205
+ const out = ts.transpileModule(code, {
126
206
  compilerOptions: {
127
207
  module: 'ESNext',
128
208
  target: 'ES2022',
129
209
  moduleResolution: 'NodeNext',
130
210
  },
131
211
  }).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());
212
+ await fs.writeFile(cacheFile, out, 'utf-8');
213
+ const result = await importDefault(url.pathToFileURL(cacheFile).toString());
214
+ // Best-effort: trim older cache files for this source.
215
+ await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
216
+ return result;
136
217
  }
137
218
  catch {
138
- throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
219
+ // Caller decides final error wording; rethrow for upstream mapping.
220
+ throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
139
221
  }
140
222
  };
223
+
224
+ /**
225
+ * @packageDocumentation
226
+ * Configuration discovery and loading for get‑dotenv. Discovers config files
227
+ * in the packaged root and project root, loads JSON/YAML/JS/TS documents, and
228
+ * validates them against Zod schemas.
229
+ */
230
+ // Discovery candidates (first match wins per scope/privacy).
231
+ // Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
232
+ const PUBLIC_FILENAMES = [
233
+ 'getdotenv.config.json',
234
+ 'getdotenv.config.yaml',
235
+ 'getdotenv.config.yml',
236
+ 'getdotenv.config.js',
237
+ 'getdotenv.config.mjs',
238
+ 'getdotenv.config.cjs',
239
+ 'getdotenv.config.ts',
240
+ 'getdotenv.config.mts',
241
+ 'getdotenv.config.cts',
242
+ ];
243
+ const LOCAL_FILENAMES = [
244
+ 'getdotenv.config.local.json',
245
+ 'getdotenv.config.local.yaml',
246
+ 'getdotenv.config.local.yml',
247
+ 'getdotenv.config.local.js',
248
+ 'getdotenv.config.local.mjs',
249
+ 'getdotenv.config.local.cjs',
250
+ 'getdotenv.config.local.ts',
251
+ 'getdotenv.config.local.mts',
252
+ 'getdotenv.config.local.cts',
253
+ ];
254
+ const isYaml = (p) => ['.yaml', '.yml'].includes(extname(p).toLowerCase());
255
+ const isJson = (p) => extname(p).toLowerCase() === '.json';
256
+ const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(extname(p).toLowerCase());
141
257
  /**
142
258
  * Discover JSON/YAML config files in the packaged root and project root.
143
259
  * Order: packaged public → project public → project local. */
@@ -190,8 +306,8 @@ const loadConfigFile = async (filePath) => {
190
306
  try {
191
307
  const abs = path.resolve(filePath);
192
308
  if (isJsOrTs(abs)) {
193
- // JS/TS support: load default export via robust pipeline.
194
- const mod = await loadJsTsDefault(abs);
309
+ // JS/TS support: load default export via shared robust pipeline.
310
+ const mod = await loadModuleDefault(abs, 'getdotenv-config');
195
311
  raw = mod ?? {};
196
312
  }
197
313
  else {
@@ -241,7 +357,66 @@ const resolveGetDotenvConfigSources = async (importMetaUrl) => {
241
357
  }
242
358
  return result;
243
359
  };
244
- // Utility primarily for tests: create a file: URL string from a path
360
+ /**
361
+ * Utility primarily for tests: create a file: URL string from a path.
362
+ * @param p - File path.
363
+ */
245
364
  const toFileUrl = (p) => pathToFileURL(path.resolve(p)).toString();
246
365
 
247
- export { discoverConfigFiles, loadConfigFile, resolveGetDotenvConfigSources, toFileUrl };
366
+ /**
367
+ * Validate a composed env against config-provided validation surfaces.
368
+ * Precedence for validation definitions:
369
+ * project.local -\> project.public -\> packaged
370
+ *
371
+ * Behavior:
372
+ * - If a JS/TS `schema` is present, use schema.safeParse(finalEnv).
373
+ * - Else if `requiredKeys` is present, check presence (value !== undefined).
374
+ * - Returns a flat list of issue strings; caller decides warn vs fail.
375
+ */
376
+ const validateEnvAgainstSources = (finalEnv, sources) => {
377
+ const pick = (getter) => {
378
+ const pl = sources.project?.local;
379
+ const pp = sources.project?.public;
380
+ const pk = sources.packaged;
381
+ return ((pl && getter(pl)) ||
382
+ (pp && getter(pp)) ||
383
+ (pk && getter(pk)) ||
384
+ undefined);
385
+ };
386
+ const schema = pick((cfg) => cfg['schema']);
387
+ if (schema &&
388
+ typeof schema.safeParse === 'function') {
389
+ try {
390
+ const parsed = schema.safeParse(finalEnv);
391
+ if (!parsed.success) {
392
+ // Try to render zod-style issues when available.
393
+ const err = parsed.error;
394
+ const issues = Array.isArray(err.issues) && err.issues.length > 0
395
+ ? err.issues.map((i) => {
396
+ const path = Array.isArray(i.path) ? i.path.join('.') : '';
397
+ const msg = i.message ?? 'Invalid value';
398
+ return path ? `[schema] ${path}: ${msg}` : `[schema] ${msg}`;
399
+ })
400
+ : ['[schema] validation failed'];
401
+ return issues;
402
+ }
403
+ return [];
404
+ }
405
+ catch {
406
+ // If schema invocation fails, surface a single diagnostic.
407
+ return [
408
+ '[schema] validation failed (unable to execute schema.safeParse)',
409
+ ];
410
+ }
411
+ }
412
+ const requiredKeys = pick((cfg) => cfg['requiredKeys']);
413
+ if (Array.isArray(requiredKeys) && requiredKeys.length > 0) {
414
+ const missing = requiredKeys.filter((k) => finalEnv[k] === undefined);
415
+ if (missing.length > 0) {
416
+ return missing.map((k) => `[requiredKeys] missing: ${k}`);
417
+ }
418
+ }
419
+ return [];
420
+ };
421
+
422
+ export { discoverConfigFiles, loadConfigFile, resolveGetDotenvConfigSources, toFileUrl, validateEnvAgainstSources };