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/README.md +23 -11
- package/android/build.gradle +13 -0
- package/android/rncu.gradle +24 -4
- package/android/src/main/java/com/reactnativeultimateconfig/UltimateConfigPackage.java +3 -3
- package/android/src/newarch/java/com/reactnativeultimateconfig/UltimateConfigModule.java +102 -0
- package/android/src/{main → oldarch}/java/com/reactnativeultimateconfig/UltimateConfigModule.java +38 -3
- package/index.js +21 -2
- package/index.ts +24 -5
- package/ios/ConfigValues.h +2 -1
- package/ios/UltimateConfig.mm +16 -2
- package/package.json +15 -5
- package/react-native-config-ultimate.podspec +5 -15
- package/src/NativeUltimateConfig.ts +10 -4
- package/src/cli.js +4 -5
- package/src/cli.ts +5 -13
- package/src/load-env.ts +2 -6
- package/src/render-env.js +6 -3
- package/src/render-env.ts +12 -16
- package/src/validate-env.js +8 -2
- package/src/validate-env.ts +15 -12
- package/src/write-env.js +7 -4
- package/src/write-env.ts +11 -4
- package/src/bin.spec.ts +0 -36
- package/src/cli.spec.ts +0 -224
- package/src/flatten.spec.ts +0 -16
- package/src/load-env.spec.ts +0 -163
- package/src/main.spec.ts +0 -171
- package/src/resolve-env.spec.ts +0 -25
- package/src/validate-env.spec.ts +0 -164
- package/src/write-env.spec.ts +0 -105
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/validate-env.js
CHANGED
|
@@ -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);
|
package/src/validate-env.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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 (
|
|
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 {
|
|
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 {
|
|
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
|
-
});
|
package/src/flatten.spec.ts
DELETED
|
@@ -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
|
-
});
|