react-native-config-ultimate 0.0.1

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -0
  3. package/android/build.gradle +180 -0
  4. package/android/rncu.gradle +132 -0
  5. package/android/src/main/AndroidManifest.xml +4 -0
  6. package/android/src/main/java/com/reactnativeultimateconfig/UltimateConfigModule.java +56 -0
  7. package/android/src/main/java/com/reactnativeultimateconfig/UltimateConfigPackage.java +53 -0
  8. package/bin.js +5 -0
  9. package/index.js +9 -0
  10. package/index.ts +18 -0
  11. package/ios/ConfigValues.h +1 -0
  12. package/ios/UltimateConfig.h +12 -0
  13. package/ios/UltimateConfig.mm +27 -0
  14. package/ios/UltimateConfig.xcodeproj/project.pbxproj +274 -0
  15. package/override.js +1 -0
  16. package/package.json +110 -0
  17. package/react-native-config-ultimate.podspec +41 -0
  18. package/src/NativeUltimateConfig.js +4 -0
  19. package/src/NativeUltimateConfig.ts +15 -0
  20. package/src/bin.spec.ts +36 -0
  21. package/src/cli.js +177 -0
  22. package/src/cli.spec.ts +224 -0
  23. package/src/cli.ts +166 -0
  24. package/src/flatten.js +22 -0
  25. package/src/flatten.spec.ts +16 -0
  26. package/src/flatten.ts +26 -0
  27. package/src/load-env.js +107 -0
  28. package/src/load-env.spec.ts +163 -0
  29. package/src/load-env.ts +84 -0
  30. package/src/main.js +34 -0
  31. package/src/main.spec.ts +171 -0
  32. package/src/main.ts +39 -0
  33. package/src/render-env.js +110 -0
  34. package/src/render-env.ts +115 -0
  35. package/src/resolve-env.js +12 -0
  36. package/src/resolve-env.spec.ts +25 -0
  37. package/src/resolve-env.ts +45 -0
  38. package/src/templates/ConfigValues.h.handlebars +24 -0
  39. package/src/templates/index.d.ts.handlebars +18 -0
  40. package/src/templates/index.web.js.handlebars +1 -0
  41. package/src/templates/override.js.handlebars +16 -0
  42. package/src/templates/rncu.xcconfig.handlebars +4 -0
  43. package/src/templates/rncu.yaml.handlebars +7 -0
  44. package/src/validate-env.js +53 -0
  45. package/src/validate-env.spec.ts +164 -0
  46. package/src/validate-env.ts +68 -0
  47. package/src/write-env.js +99 -0
  48. package/src/write-env.spec.ts +105 -0
  49. package/src/write-env.ts +67 -0
@@ -0,0 +1,224 @@
1
+ // All mocks must be declared before the module is required.
2
+ const mock_main = jest.fn();
3
+ jest.doMock('./main', () => ({ __esModule: true, default: mock_main }));
4
+
5
+ const mock_exists_sync = jest.fn();
6
+ jest.doMock('fs', () => ({
7
+ existsSync: mock_exists_sync,
8
+ }));
9
+
10
+ // Mock watcher returned by chokidar.watch()
11
+ const mock_watcher_on = jest.fn();
12
+ const mock_watcher_close = jest.fn().mockResolvedValue(undefined);
13
+ const mock_watcher = {
14
+ on: mock_watcher_on.mockReturnThis(),
15
+ close: mock_watcher_close,
16
+ };
17
+ const mock_chokidar_watch = jest.fn().mockReturnValue(mock_watcher);
18
+ jest.doMock('chokidar', () => ({ watch: mock_chokidar_watch }));
19
+
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const cli: () => Promise<void> = require('./cli').default;
22
+
23
+ // ─── helpers ────────────────────────────────────────────────────────────────
24
+
25
+ function set_argv(...args: string[]): void {
26
+ process.argv = ['node', 'rncu', ...args];
27
+ }
28
+
29
+ /** Grab the handler registered for a given chokidar event. */
30
+ function get_watcher_handler(
31
+ event: string
32
+ ): ((p: string) => Promise<void>) | undefined {
33
+ const call = (mock_watcher_on.mock.calls as [string, unknown][]).find(
34
+ ([ev]) => ev === event
35
+ );
36
+ return call?.[1] as ((p: string) => Promise<void>) | undefined;
37
+ }
38
+
39
+ // ─── test suite ──────────────────────────────────────────────────────────────
40
+
41
+ describe('cli', () => {
42
+ const original_argv = process.argv;
43
+ const stdin_resume_spy = jest
44
+ .spyOn(process.stdin, 'resume')
45
+ .mockImplementation(() => process.stdin);
46
+ const process_on_spy = jest
47
+ .spyOn(process, 'on')
48
+ .mockImplementation(() => process);
49
+
50
+ beforeEach(() => {
51
+ mock_main.mockReset().mockResolvedValue(undefined);
52
+ // Default: env files exist, RC file does NOT exist.
53
+ // This mimics the real-world baseline: user passes a valid .env file,
54
+ // but hasn't created a .rncurc.js config yet.
55
+ mock_exists_sync.mockReset().mockImplementation((p: string) => !p.endsWith('.rncurc.js'));
56
+ mock_chokidar_watch.mockReset().mockReturnValue(mock_watcher);
57
+ mock_watcher_on.mockReset().mockReturnThis();
58
+ mock_watcher_close.mockReset().mockResolvedValue(undefined);
59
+ stdin_resume_spy.mockClear();
60
+ process_on_spy.mockClear();
61
+ });
62
+
63
+ afterAll(() => {
64
+ process.argv = original_argv;
65
+ stdin_resume_spy.mockRestore();
66
+ process_on_spy.mockRestore();
67
+ });
68
+
69
+ // ── normal (non-watch) mode ─────────────────────────────────────────────
70
+
71
+ describe('normal mode', () => {
72
+ it('runs main once with the env file and exits', async () => {
73
+ set_argv('.env');
74
+ await cli();
75
+ expect(mock_main).toHaveBeenCalledTimes(1);
76
+ expect(mock_main).toHaveBeenCalledWith(
77
+ expect.any(String),
78
+ expect.any(String),
79
+ ['.env'],
80
+ undefined
81
+ );
82
+ expect(mock_chokidar_watch).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('passes multiple env files to main', async () => {
86
+ set_argv('.env.base', '.env.staging');
87
+ await cli();
88
+ expect(mock_main).toHaveBeenCalledWith(
89
+ expect.any(String),
90
+ expect.any(String),
91
+ ['.env.base', '.env.staging'],
92
+ undefined
93
+ );
94
+ });
95
+
96
+ it('loads and passes RC file when it exists', async () => {
97
+ set_argv('.env');
98
+ // All files exist (env file + RC file). The RC file can't actually be
99
+ // resolved via require() in test env, so cli() is expected to throw.
100
+ mock_exists_sync.mockReturnValue(true);
101
+ await expect(cli()).rejects.toThrow();
102
+ });
103
+
104
+ it('propagates errors from main in non-watch mode', async () => {
105
+ set_argv('.env');
106
+ mock_main.mockRejectedValueOnce(new Error('missing var'));
107
+ await expect(cli()).rejects.toThrow('missing var');
108
+ });
109
+ });
110
+
111
+ // ── watch mode ──────────────────────────────────────────────────────────
112
+
113
+ describe('--watch mode', () => {
114
+ it('starts chokidar watcher on the env files', async () => {
115
+ set_argv('.env', '--watch');
116
+ await cli();
117
+ expect(mock_chokidar_watch).toHaveBeenCalledWith(
118
+ ['.env'],
119
+ expect.objectContaining({ ignoreInitial: true, persistent: true })
120
+ );
121
+ });
122
+
123
+ it('also watches .rncurc.js when it exists', async () => {
124
+ set_argv('.env', '--watch');
125
+ // All files exist (env + RC). We suppress the require() error below
126
+ // so the test can verify that .rncurc.js is added to the watcher list.
127
+ mock_exists_sync.mockReturnValue(true);
128
+ // Suppress require() failure for missing rc file in initial run
129
+ mock_main.mockResolvedValue(undefined);
130
+ try {
131
+ await cli();
132
+ } catch {
133
+ // ignore require error for non-existent RC in test env
134
+ }
135
+ const watched_files = mock_chokidar_watch.mock.calls[0]?.[0] as string[];
136
+ expect(watched_files.some((f) => f.endsWith('.rncurc.js'))).toBe(true);
137
+ });
138
+
139
+ it('runs main once immediately on start', async () => {
140
+ set_argv('.env', '--watch');
141
+ await cli();
142
+ expect(mock_main).toHaveBeenCalledTimes(1);
143
+ });
144
+
145
+ it('registers change and add event handlers', async () => {
146
+ set_argv('.env', '--watch');
147
+ await cli();
148
+ const events = (mock_watcher_on.mock.calls as [string, unknown][]).map(
149
+ ([ev]) => ev
150
+ );
151
+ expect(events).toContain('change');
152
+ expect(events).toContain('add');
153
+ });
154
+
155
+ it('re-runs main when a file changes', async () => {
156
+ set_argv('.env', '--watch');
157
+ await cli();
158
+ expect(mock_main).toHaveBeenCalledTimes(1);
159
+
160
+ const on_change = get_watcher_handler('change');
161
+ expect(on_change).toBeDefined();
162
+
163
+ await on_change?.('.env');
164
+ expect(mock_main).toHaveBeenCalledTimes(2);
165
+ });
166
+
167
+ it('re-runs main when a file is added', async () => {
168
+ set_argv('.env', '--watch');
169
+ await cli();
170
+
171
+ const on_add = get_watcher_handler('add');
172
+ await on_add?.('.env.local');
173
+ expect(mock_main).toHaveBeenCalledTimes(2);
174
+ });
175
+
176
+ it('catches errors on re-run and keeps watching (does not throw)', async () => {
177
+ set_argv('.env', '--watch');
178
+ await cli();
179
+
180
+ mock_main.mockRejectedValueOnce(new Error('validation failed'));
181
+ const on_change = get_watcher_handler('change');
182
+ expect(on_change).toBeDefined();
183
+
184
+ // The handler uses `void run(p)` so it returns undefined synchronously
185
+ // and swallows errors inside run()'s try/catch. We trigger it and then
186
+ // drain the microtask queue to let the async error handling complete.
187
+ expect(() => on_change!('.env')).not.toThrow();
188
+ await new Promise<void>((resolve) => setImmediate(resolve));
189
+
190
+ // main was called twice: initial run + change handler
191
+ expect(mock_main).toHaveBeenCalledTimes(2);
192
+ });
193
+
194
+ it('catches initial run errors and still starts the watcher', async () => {
195
+ set_argv('.env', '--watch');
196
+ mock_main.mockRejectedValueOnce(new Error('initial run failed'));
197
+
198
+ // Should not throw even though initial run fails
199
+ await expect(cli()).resolves.toBeUndefined();
200
+ expect(mock_chokidar_watch).toHaveBeenCalled();
201
+ });
202
+
203
+ it('keeps process alive via process.stdin.resume()', async () => {
204
+ set_argv('.env', '--watch');
205
+ await cli();
206
+ expect(stdin_resume_spy).toHaveBeenCalled();
207
+ });
208
+
209
+ it('registers a SIGINT handler for graceful shutdown', async () => {
210
+ set_argv('.env', '--watch');
211
+ await cli();
212
+ const sigint_call = (
213
+ process_on_spy.mock.calls as [string, unknown][]
214
+ ).find(([event]) => event === 'SIGINT');
215
+ expect(sigint_call).toBeDefined();
216
+ });
217
+
218
+ it('uses -w as alias for --watch', async () => {
219
+ set_argv('.env', '-w');
220
+ await cli();
221
+ expect(mock_chokidar_watch).toHaveBeenCalled();
222
+ });
223
+ });
224
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,166 @@
1
+ import yargs from 'yargs';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { watch } from 'chokidar';
5
+ import main from './main';
6
+ import type { RC } from './resolve-env';
7
+
8
+ /**
9
+ * Load the RC file fresh on every call (clears require cache so
10
+ * changes to .rncurc.js are picked up during --watch mode).
11
+ */
12
+ function load_rc(rc_file: string): RC | undefined {
13
+ if (!fs.existsSync(rc_file)) return undefined;
14
+ delete require.cache[require.resolve(rc_file)];
15
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
16
+ return require(rc_file) as RC;
17
+ }
18
+
19
+ function log(msg: string): void {
20
+ process.stdout.write(`[rncu] ${msg}\n`);
21
+ }
22
+
23
+ function log_err(msg: string): void {
24
+ process.stderr.write(`[rncu] ✗ ${msg}\n`);
25
+ }
26
+
27
+ export default async function cli(): Promise<void> {
28
+ const argv = await yargs(process.argv.slice(2))
29
+ .option('projectRoot', {
30
+ type: 'string',
31
+ default: process.cwd(),
32
+ description: 'Root directory of the React Native project',
33
+ })
34
+ .option('libRoot', {
35
+ type: 'string',
36
+ description:
37
+ 'Root directory of react-native-config-ultimate ' +
38
+ '(defaults to <projectRoot>/node_modules/react-native-config-ultimate)',
39
+ })
40
+ .option('watch', {
41
+ alias: 'w',
42
+ type: 'boolean',
43
+ default: false,
44
+ description:
45
+ 'Watch env file(s) for changes and regenerate automatically. ' +
46
+ 'Note: changes to native vars (iOS xcconfig, Android BuildConfig) ' +
47
+ 'still require a full native rebuild.',
48
+ })
49
+ .usage('Usage: $0 <env-file> [env-file2 ...] [options]')
50
+ .help()
51
+ .parseAsync();
52
+
53
+ const project_root = argv.projectRoot;
54
+
55
+ /**
56
+ * Resolve the library root directory.
57
+ *
58
+ * Priority:
59
+ * 1. --libRoot flag (explicit override)
60
+ * 2. Conventional path <projectRoot>/node_modules/react-native-config-ultimate
61
+ * — used when the directory exists (standard install, or bin.spec.ts integration test).
62
+ * 3. require.resolve() — handles npm workspaces hoisting, pnpm, Yarn PnP,
63
+ * and any layout where the package is hoisted above projectRoot.
64
+ * 4. Fall back to conventional path even if it doesn't exist yet
65
+ * (write-env.ts will create the directories on first run).
66
+ */
67
+ const lib_root: string = (() => {
68
+ if (argv.libRoot) return argv.libRoot;
69
+
70
+ const conventional = path.join(
71
+ project_root,
72
+ 'node_modules',
73
+ 'react-native-config-ultimate'
74
+ );
75
+
76
+ // If the directory already exists at the conventional location, use it.
77
+ // This handles standard installs and the integration test temp-dir setup.
78
+ if (fs.existsSync(conventional)) return conventional;
79
+
80
+ // Otherwise, try require.resolve to handle hoisted workspaces.
81
+ try {
82
+ const pkg_json = require.resolve(
83
+ 'react-native-config-ultimate/package.json',
84
+ { paths: [project_root] }
85
+ );
86
+ return path.dirname(pkg_json);
87
+ } catch {
88
+ // Last resort: return the conventional path and let write-env create it.
89
+ return conventional;
90
+ }
91
+ })();
92
+
93
+ // Accept one or more positional env file paths.
94
+ // Multiple files are merged left-to-right (last file wins for conflicting keys).
95
+ const env_files = argv._.map(String);
96
+
97
+ // Validate env files exist before running anything.
98
+ const missing_files = env_files.filter((f) => !fs.existsSync(f));
99
+ if (missing_files.length > 0) {
100
+ for (const f of missing_files) {
101
+ log_err(`env file not found: ${f}`);
102
+ }
103
+ process.exit(1);
104
+ }
105
+
106
+ const rc_file = path.resolve(project_root, '.rncurc.js');
107
+
108
+ // Helper: run the full pipeline once, returning duration in ms.
109
+ // Never throws — errors are caught and logged so watch mode stays alive.
110
+ async function run(changed_path?: string): Promise<void> {
111
+ if (changed_path) {
112
+ log(`${changed_path} changed → regenerating...`);
113
+ }
114
+ const start = Date.now();
115
+ try {
116
+ const rc = load_rc(rc_file);
117
+ await main(project_root, lib_root, env_files, rc);
118
+ if (changed_path) {
119
+ log(`✓ done in ${Date.now() - start}ms`);
120
+ }
121
+ } catch (err) {
122
+ log_err(err instanceof Error ? err.message : String(err));
123
+ }
124
+ }
125
+
126
+ // Initial run (always runs, throws on error in non-watch mode).
127
+ if (!argv.watch) {
128
+ const rc = load_rc(rc_file);
129
+ await main(project_root, lib_root, env_files, rc);
130
+ return;
131
+ }
132
+
133
+ // --watch mode ────────────────────────────────────────────────────────────
134
+ // Initial run (errors are caught — we still want to start watching).
135
+ await run();
136
+
137
+ // Files to watch: env files + RC file (if it exists).
138
+ const files_to_watch = [
139
+ ...env_files,
140
+ ...(fs.existsSync(rc_file) ? [rc_file] : []),
141
+ ];
142
+
143
+ const watcher = watch(files_to_watch, {
144
+ ignoreInitial: true,
145
+ persistent: true,
146
+ });
147
+
148
+ log(
149
+ `watching: ${files_to_watch.join(', ')}\n` +
150
+ `[rncu] ⚠ native vars (xcconfig/BuildConfig) require a full rebuild to take effect`
151
+ );
152
+
153
+ watcher.on('change', (p) => void run(p));
154
+ watcher.on('add', (p) => void run(p));
155
+
156
+ // Keep the process alive (chokidar persistent:true already does this,
157
+ // but stdin.resume makes it explicit and survives edge cases).
158
+ process.stdin.resume();
159
+
160
+ // Graceful shutdown on Ctrl+C.
161
+ process.on('SIGINT', () => {
162
+ process.stdout.write('\n');
163
+ log('stopping...');
164
+ void watcher.close().then(() => process.exit(0));
165
+ });
166
+ }
package/src/flatten.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = flatten;
7
+ const assert_1 = __importDefault(require("assert"));
8
+ const VALID_PLATFORMS = ['ios', 'android', 'web'];
9
+ function flatten(config, platform) {
10
+ (0, assert_1.default)(config && typeof config === 'object', 'Config should be non-null object');
11
+ (0, assert_1.default)(VALID_PLATFORMS.includes(platform), '`platform` should one of: ' + VALID_PLATFORMS.join(', '));
12
+ const result = {};
13
+ for (const [key, value] of Object.entries(config)) {
14
+ if (value && typeof value === 'object') {
15
+ result[key] = value[platform];
16
+ }
17
+ else {
18
+ result[key] = value;
19
+ }
20
+ }
21
+ return result;
22
+ }
@@ -0,0 +1,16 @@
1
+ import flatten from './flatten';
2
+
3
+ describe('flatten', () => {
4
+ it('flattens config per platform', () => {
5
+ const config = {
6
+ value1: 'abc',
7
+ value2: { ios: 'def', android: 'xyz', web: '123' },
8
+ };
9
+ const flat_ios = flatten(config, 'ios');
10
+ expect(flat_ios).toEqual({ value1: 'abc', value2: 'def' });
11
+ const flat_android = flatten(config, 'android');
12
+ expect(flat_android).toEqual({ value1: 'abc', value2: 'xyz' });
13
+ const flat_web = flatten(config, 'web');
14
+ expect(flat_web).toEqual({ value1: 'abc', value2: '123' });
15
+ });
16
+ });
package/src/flatten.ts ADDED
@@ -0,0 +1,26 @@
1
+ import assert from 'assert';
2
+
3
+ type Platform = 'ios' | 'android' | 'web';
4
+ export type ConfigValue = string | number | boolean;
5
+ export type PerPlatformValue = Partial<Record<Platform, ConfigValue>>;
6
+ export type EnvConfig = Record<string, ConfigValue | PerPlatformValue>;
7
+ export type FlatConfig = Record<string, ConfigValue | undefined>;
8
+
9
+ const VALID_PLATFORMS: Platform[] = ['ios', 'android', 'web'];
10
+
11
+ export default function flatten(config: EnvConfig, platform: Platform): FlatConfig {
12
+ assert(config && typeof config === 'object', 'Config should be non-null object');
13
+ assert(
14
+ VALID_PLATFORMS.includes(platform),
15
+ '`platform` should one of: ' + VALID_PLATFORMS.join(', ')
16
+ );
17
+ const result: FlatConfig = {};
18
+ for (const [key, value] of Object.entries(config)) {
19
+ if (value && typeof value === 'object') {
20
+ result[key] = (value as PerPlatformValue)[platform];
21
+ } else {
22
+ result[key] = value as ConfigValue;
23
+ }
24
+ }
25
+ return result;
26
+ }
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.default = load_env;
37
+ const dotenv = __importStar(require("dotenv"));
38
+ const dotenv_expand_1 = require("dotenv-expand");
39
+ const yaml = __importStar(require("js-yaml"));
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs"));
42
+ function detect_format(config_path) {
43
+ const { ext } = path.parse(config_path);
44
+ return ext === '.yml' || ext === '.yaml' ? 'yaml' : 'dotenv';
45
+ }
46
+ function read_yaml(config_path) {
47
+ const data = yaml.load(fs.readFileSync(config_path).toString());
48
+ if (typeof data === 'undefined' || data === null || typeof data !== 'object') {
49
+ throw new Error(`Expected to read object from ${config_path}, but got '${data}'`);
50
+ }
51
+ return data;
52
+ }
53
+ /**
54
+ * Load one or more env files and merge them (last file wins for conflicting keys).
55
+ *
56
+ * Dotenv files (.env, .env.staging, etc.):
57
+ * - All files are merged first, then variable expansion runs once.
58
+ * - This means cross-file `$VAR` references work:
59
+ * .env.base: BASE_URL=https://api.example.com
60
+ * .env.staging: API_URL=$BASE_URL/v1 → https://api.example.com/v1
61
+ *
62
+ * YAML files (.yml, .yaml):
63
+ * - Each file is loaded and shallow-merged (last wins for top-level keys).
64
+ * - No variable expansion is applied (use YAML anchors instead).
65
+ *
66
+ * @example
67
+ * // Single file (backward-compatible):
68
+ * load_env('.env')
69
+ *
70
+ * // Multi-file merge:
71
+ * load_env(['.env.base', '.env.staging'])
72
+ */
73
+ function load_env(config_paths) {
74
+ var _a, _b;
75
+ const paths = Array.isArray(config_paths) ? config_paths : [config_paths];
76
+ if (paths.length === 0) {
77
+ throw new Error('No env file specified. Usage: rncu <env-file> [env-file2 ...]');
78
+ }
79
+ const formats = paths.map(detect_format);
80
+ const allDotenv = formats.every((f) => f === 'dotenv');
81
+ if (allDotenv) {
82
+ // Merge raw parsed content first, then expand once —
83
+ // so cross-file $VAR references resolve correctly.
84
+ const raw = {};
85
+ for (const p of paths) {
86
+ const content = fs.readFileSync(p, 'utf8');
87
+ Object.assign(raw, dotenv.parse(content));
88
+ }
89
+ const result = (0, dotenv_expand_1.expand)({ parsed: raw });
90
+ return ((_a = result.parsed) !== null && _a !== void 0 ? _a : raw);
91
+ }
92
+ // YAML or mixed: load each file individually and shallow-merge.
93
+ const merged = {};
94
+ for (let i = 0; i < paths.length; i++) {
95
+ const p = paths[i];
96
+ if (formats[i] === 'yaml') {
97
+ Object.assign(merged, read_yaml(p));
98
+ }
99
+ else {
100
+ const content = fs.readFileSync(p, 'utf8');
101
+ const parsed = dotenv.parse(content);
102
+ const expanded = (0, dotenv_expand_1.expand)({ parsed });
103
+ Object.assign(merged, (_b = expanded.parsed) !== null && _b !== void 0 ? _b : parsed);
104
+ }
105
+ }
106
+ return merged;
107
+ }