react-native-config-ultimate 0.0.1 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/load-env.ts CHANGED
@@ -16,9 +16,7 @@ function detect_format(config_path: string): FileFormat {
16
16
  function read_yaml(config_path: string): EnvData {
17
17
  const data = yaml.load(fs.readFileSync(config_path).toString());
18
18
  if (typeof data === 'undefined' || data === null || typeof data !== 'object') {
19
- throw new Error(
20
- `Expected to read object from ${config_path}, but got '${data}'`
21
- );
19
+ throw new Error(`Expected to read object from ${config_path}, but got '${data}'`);
22
20
  }
23
21
  return data as EnvData;
24
22
  }
@@ -47,9 +45,7 @@ export default function load_env(config_paths: string | string[]): EnvData {
47
45
  const paths = Array.isArray(config_paths) ? config_paths : [config_paths];
48
46
 
49
47
  if (paths.length === 0) {
50
- throw new Error(
51
- 'No env file specified. Usage: rncu <env-file> [env-file2 ...]'
52
- );
48
+ throw new Error('No env file specified. Usage: rncu <env-file> [env-file2 ...]');
53
49
  }
54
50
 
55
51
  const formats = paths.map(detect_format);
package/src/render-env.js CHANGED
@@ -54,7 +54,11 @@ function is_boolean(value) {
54
54
  }
55
55
  function escape(value) {
56
56
  if (is_string(value)) {
57
- return value.replace(/"/gm, '\\"');
57
+ return value
58
+ .replace(/\\/gm, '\\\\')
59
+ .replace(/"/gm, '\\"')
60
+ .replace(/\n/gm, '\\n')
61
+ .replace(/\r/gm, '\\r');
58
62
  }
59
63
  return value;
60
64
  }
@@ -98,8 +102,7 @@ function render_env(project_root, lib_root, env, rc) {
98
102
  // Only write xcconfig if the project has an ios folder.
99
103
  // All RN apps have it; some react-native-web apps may not.
100
104
  if (fs.existsSync(path.join(project_root, 'ios'))) {
101
- map[path.join(project_root, 'ios', `${config_file_name}.xcconfig`)] =
102
- render_template('rncu.xcconfig', ios);
105
+ map[path.join(project_root, 'ios', `${config_file_name}.xcconfig`)] = render_template('rncu.xcconfig', ios);
103
106
  }
104
107
  const js_override = rc && typeof rc.js_override === 'boolean' && rc.js_override;
105
108
  map[path.join(lib_root, 'override.js')] = render_template('override.js', {
package/src/render-env.ts CHANGED
@@ -25,7 +25,11 @@ function is_boolean(value: unknown): boolean {
25
25
 
26
26
  function escape(value: unknown): unknown {
27
27
  if (is_string(value)) {
28
- return (value as string).replace(/"/gm, '\\"');
28
+ return (value as string)
29
+ .replace(/\\/gm, '\\\\')
30
+ .replace(/"/gm, '\\"')
31
+ .replace(/\n/gm, '\\n')
32
+ .replace(/\r/gm, '\\r');
29
33
  }
30
34
  return value;
31
35
  }
@@ -56,11 +60,7 @@ function get_compiled_template(template_name: string): HandlebarsTemplateDelegat
56
60
  const cached = template_cache.get(template_name);
57
61
  if (cached) return cached;
58
62
 
59
- const template_path = path.join(
60
- __dirname,
61
- 'templates',
62
- `${template_name}.handlebars`
63
- );
63
+ const template_path = path.join(__dirname, 'templates', `${template_name}.handlebars`);
64
64
  const compiled = handlebars.compile(fs.readFileSync(template_path, 'utf8'));
65
65
  template_cache.set(template_name, compiled);
66
66
  return compiled;
@@ -88,21 +88,17 @@ export default function render_env(
88
88
  const map: FileMap = {
89
89
  [path.join(lib_root, 'index.d.ts')]: render_template('index.d.ts', ios),
90
90
  [path.join(lib_root, 'index.web.js')]: render_template('index.web.js', web),
91
- [path.join(lib_root, 'ios', `${code_file_name}.h`)]: render_template(
92
- 'ConfigValues.h',
93
- ios
94
- ),
95
- [path.join(lib_root, 'android', 'rncu.yaml')]: render_template(
96
- 'rncu.yaml',
97
- android
98
- ),
91
+ [path.join(lib_root, 'ios', `${code_file_name}.h`)]: render_template('ConfigValues.h', ios),
92
+ [path.join(lib_root, 'android', 'rncu.yaml')]: render_template('rncu.yaml', android),
99
93
  };
100
94
 
101
95
  // Only write xcconfig if the project has an ios folder.
102
96
  // All RN apps have it; some react-native-web apps may not.
103
97
  if (fs.existsSync(path.join(project_root, 'ios'))) {
104
- map[path.join(project_root, 'ios', `${config_file_name}.xcconfig`)] =
105
- render_template('rncu.xcconfig', ios);
98
+ map[path.join(project_root, 'ios', `${config_file_name}.xcconfig`)] = render_template(
99
+ 'rncu.xcconfig',
100
+ ios
101
+ );
106
102
  }
107
103
 
108
104
  const js_override = rc && typeof rc.js_override === 'boolean' && rc.js_override;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validate_env = validate_env;
4
+ const VALID_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
4
5
  /**
5
6
  * Validate env data against a schema defined in `.rncurc.js`.
6
7
  * Called after `on_env` so the hook can add/transform vars before validation.
@@ -10,6 +11,12 @@ exports.validate_env = validate_env;
10
11
  */
11
12
  function validate_env(env, schema) {
12
13
  const errors = [];
14
+ // Validate all env key names are valid identifiers
15
+ for (const key of Object.keys(env)) {
16
+ if (!VALID_KEY_PATTERN.test(key)) {
17
+ errors.push(`Invalid env key name: "${key}". Keys must start with a letter or underscore and contain only letters, numbers, and underscores.`);
18
+ }
19
+ }
13
20
  // Pre-compile all regex patterns once, before iterating over env values.
14
21
  // This avoids re-compiling the same pattern for every validated key.
15
22
  const compiled_patterns = new Map();
@@ -36,8 +43,7 @@ function validate_env(env, schema) {
36
43
  if (field.type === 'number' && isNaN(Number(value))) {
37
44
  errors.push(`${key} must be a number, got "${value}"`);
38
45
  }
39
- if (field.type === 'boolean' &&
40
- !['true', 'false', '1', '0'].includes(value.toLowerCase())) {
46
+ if (field.type === 'boolean' && !['true', 'false', '1', '0'].includes(value.toLowerCase())) {
41
47
  errors.push(`${key} must be a boolean (true/false/1/0), got "${value}"`);
42
48
  }
43
49
  const pattern = compiled_patterns.get(key);
@@ -1,5 +1,7 @@
1
1
  import type { EnvData, Schema } from './resolve-env';
2
2
 
3
+ const VALID_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
4
+
3
5
  /**
4
6
  * Validate env data against a schema defined in `.rncurc.js`.
5
7
  * Called after `on_env` so the hook can add/transform vars before validation.
@@ -10,6 +12,15 @@ import type { EnvData, Schema } from './resolve-env';
10
12
  export function validate_env(env: EnvData, schema: Schema): void {
11
13
  const errors: string[] = [];
12
14
 
15
+ // Validate all env key names are valid identifiers
16
+ for (const key of Object.keys(env)) {
17
+ if (!VALID_KEY_PATTERN.test(key)) {
18
+ errors.push(
19
+ `Invalid env key name: "${key}". Keys must start with a letter or underscore and contain only letters, numbers, and underscores.`
20
+ );
21
+ }
22
+ }
23
+
13
24
  // Pre-compile all regex patterns once, before iterating over env values.
14
25
  // This avoids re-compiling the same pattern for every validated key.
15
26
  const compiled_patterns = new Map<string, RegExp>();
@@ -25,8 +36,7 @@ export function validate_env(env: EnvData, schema: Schema): void {
25
36
 
26
37
  for (const [key, field] of Object.entries(schema)) {
27
38
  const raw = env[key];
28
- const missing =
29
- raw === undefined || raw === null || String(raw).trim() === '';
39
+ const missing = raw === undefined || raw === null || String(raw).trim() === '';
30
40
 
31
41
  if (field.required && missing) {
32
42
  errors.push(`Missing required env var: ${key}`);
@@ -41,20 +51,13 @@ export function validate_env(env: EnvData, schema: Schema): void {
41
51
  errors.push(`${key} must be a number, got "${value}"`);
42
52
  }
43
53
 
44
- if (
45
- field.type === 'boolean' &&
46
- !['true', 'false', '1', '0'].includes(value.toLowerCase())
47
- ) {
48
- errors.push(
49
- `${key} must be a boolean (true/false/1/0), got "${value}"`
50
- );
54
+ if (field.type === 'boolean' && !['true', 'false', '1', '0'].includes(value.toLowerCase())) {
55
+ errors.push(`${key} must be a boolean (true/false/1/0), got "${value}"`);
51
56
  }
52
57
 
53
58
  const pattern = compiled_patterns.get(key);
54
59
  if (pattern && !pattern.test(value)) {
55
- errors.push(
56
- `${key} does not match pattern /${field.pattern}/, got "${value}"`
57
- );
60
+ errors.push(`${key} does not match pattern /${field.pattern}/, got "${value}"`);
58
61
  }
59
62
  }
60
63
 
package/src/write-env.js CHANGED
@@ -66,7 +66,9 @@ function write_env(files) {
66
66
  try {
67
67
  fs.unlinkSync(tmp);
68
68
  }
69
- catch ( /* ignore */_a) { /* ignore */ }
69
+ catch (_a) {
70
+ /* ignore */
71
+ }
70
72
  }
71
73
  throw new Error(`[rncu] Failed to prepare output files: ${err instanceof Error ? err.message : String(err)}`);
72
74
  }
@@ -88,12 +90,13 @@ function write_env(files) {
88
90
  try {
89
91
  fs.unlinkSync(tmp);
90
92
  }
91
- catch ( /* ignore */_c) { /* ignore */ }
93
+ catch (_c) {
94
+ /* ignore */
95
+ }
92
96
  }
93
97
  }
94
98
  }
95
99
  if (rename_errors.length > 0) {
96
- throw new Error(`[rncu] Failed to write output files:\n` +
97
- rename_errors.map((e) => ` • ${e}`).join('\n'));
100
+ throw new Error(`[rncu] Failed to write output files:\n` + rename_errors.map((e) => ` • ${e}`).join('\n'));
98
101
  }
99
102
  }
package/src/write-env.ts CHANGED
@@ -30,7 +30,11 @@ export default function write_env(files: FileMap): void {
30
30
  } catch (err) {
31
31
  // Clean up any temp files we already created.
32
32
  for (const { tmp } of pending) {
33
- try { fs.unlinkSync(tmp); } catch { /* ignore */ }
33
+ try {
34
+ fs.unlinkSync(tmp);
35
+ } catch {
36
+ /* ignore */
37
+ }
34
38
  }
35
39
  throw new Error(
36
40
  `[rncu] Failed to prepare output files: ${err instanceof Error ? err.message : String(err)}`
@@ -53,15 +57,18 @@ export default function write_env(files: FileMap): void {
53
57
  rename_errors.push(
54
58
  `${dest}: ${copy_err instanceof Error ? copy_err.message : String(copy_err)}`
55
59
  );
56
- try { fs.unlinkSync(tmp); } catch { /* ignore */ }
60
+ try {
61
+ fs.unlinkSync(tmp);
62
+ } catch {
63
+ /* ignore */
64
+ }
57
65
  }
58
66
  }
59
67
  }
60
68
 
61
69
  if (rename_errors.length > 0) {
62
70
  throw new Error(
63
- `[rncu] Failed to write output files:\n` +
64
- rename_errors.map((e) => ` • ${e}`).join('\n')
71
+ `[rncu] Failed to write output files:\n` + rename_errors.map((e) => ` • ${e}`).join('\n')
65
72
  );
66
73
  }
67
74
  }
package/src/bin.spec.ts DELETED
@@ -1,36 +0,0 @@
1
- import cp from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
4
- // eslint-disable-next-line @typescript-eslint/no-require-imports
5
- const { files_to_assert } = require('./main.spec') as { files_to_assert: string[] };
6
-
7
- describe.each`
8
- extension | env_test_content
9
- ${''} | ${'hello=world'}
10
- ${'.yaml'} | ${'hello: world'}
11
- ${'.yml'} | ${'hello: world'}
12
- `('test codegen', ({ extension, env_test_content }: { extension: string; env_test_content: string }) => {
13
- let project_root: string;
14
- beforeAll(() => {
15
- project_root = path.join(process.cwd(), fs.mkdtempSync('rncu-jest'));
16
- for (const file_path of files_to_assert) {
17
- const { dir } = path.parse(file_path);
18
- const folder = path.join(project_root, dir);
19
- fs.mkdirSync(folder, { recursive: true });
20
- }
21
- });
22
- afterAll(() => {
23
- fs.rmSync(project_root, { recursive: true, force: true });
24
- });
25
- it.each(files_to_assert.map((k) => [k]))(
26
- 'creates file at path %s',
27
- (file_path) => {
28
- const env_file_path = path.join(project_root, `.env${extension}`);
29
- fs.writeFileSync(env_file_path, env_test_content);
30
- cp.execFileSync(path.join(process.cwd(), 'bin.js'), [env_file_path], {
31
- cwd: project_root,
32
- });
33
- expect(fs.existsSync(path.join(project_root, file_path as string))).toEqual(true);
34
- }
35
- );
36
- });
package/src/cli.spec.ts DELETED
@@ -1,224 +0,0 @@
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
- });
@@ -1,16 +0,0 @@
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
- });