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,115 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import handlebars from 'handlebars';
4
+
5
+ import type { FlatConfig } from './flatten';
6
+ import type { RC } from './resolve-env';
7
+ import type { FileMap } from './write-env';
8
+
9
+ const code_file_name = 'ConfigValues';
10
+ const config_file_name = 'rncu';
11
+
12
+ // ─── Handlebars helpers (registered once at module load, not per-call) ────────
13
+
14
+ function is_string(value: unknown): boolean {
15
+ return typeof value === 'string';
16
+ }
17
+
18
+ function is_number(value: unknown): boolean {
19
+ return typeof value === 'number';
20
+ }
21
+
22
+ function is_boolean(value: unknown): boolean {
23
+ return typeof value === 'boolean';
24
+ }
25
+
26
+ function escape(value: unknown): unknown {
27
+ if (is_string(value)) {
28
+ return (value as string).replace(/"/gm, '\\"');
29
+ }
30
+ return value;
31
+ }
32
+
33
+ function xcconfig_format(value: unknown): unknown {
34
+ if (is_string(value)) {
35
+ return (value as string).replace(/\/\//gm, '/$()/');
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function to_json(value: unknown): string {
41
+ return JSON.stringify(value, null, 2);
42
+ }
43
+
44
+ handlebars.registerHelper('isBoolean', is_boolean);
45
+ handlebars.registerHelper('isString', is_string);
46
+ handlebars.registerHelper('isNumber', is_number);
47
+ handlebars.registerHelper('escape', escape);
48
+ handlebars.registerHelper('xcconfigFormat', xcconfig_format);
49
+ handlebars.registerHelper('toJSON', to_json);
50
+
51
+ // ─── Template cache (compiled once per template name, not per build) ──────────
52
+
53
+ const template_cache = new Map<string, HandlebarsTemplateDelegate>();
54
+
55
+ function get_compiled_template(template_name: string): HandlebarsTemplateDelegate {
56
+ const cached = template_cache.get(template_name);
57
+ if (cached) return cached;
58
+
59
+ const template_path = path.join(
60
+ __dirname,
61
+ 'templates',
62
+ `${template_name}.handlebars`
63
+ );
64
+ const compiled = handlebars.compile(fs.readFileSync(template_path, 'utf8'));
65
+ template_cache.set(template_name, compiled);
66
+ return compiled;
67
+ }
68
+
69
+ function render_template(template_name: string, data: unknown): string {
70
+ return get_compiled_template(template_name)(data);
71
+ }
72
+
73
+ // ─── Public API ───────────────────────────────────────────────────────────────
74
+
75
+ interface PlatformEnv {
76
+ ios: FlatConfig;
77
+ android: FlatConfig;
78
+ web: FlatConfig;
79
+ }
80
+
81
+ export default function render_env(
82
+ project_root: string,
83
+ lib_root: string,
84
+ env: PlatformEnv,
85
+ rc?: RC
86
+ ): FileMap {
87
+ const { ios, android, web } = env;
88
+ const map: FileMap = {
89
+ [path.join(lib_root, 'index.d.ts')]: render_template('index.d.ts', ios),
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
+ ),
99
+ };
100
+
101
+ // Only write xcconfig if the project has an ios folder.
102
+ // All RN apps have it; some react-native-web apps may not.
103
+ 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);
106
+ }
107
+
108
+ const js_override = rc && typeof rc.js_override === 'boolean' && rc.js_override;
109
+ map[path.join(lib_root, 'override.js')] = render_template('override.js', {
110
+ ios: js_override ? ios : {},
111
+ android: js_override ? android : {},
112
+ });
113
+
114
+ return map;
115
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = resolve_env;
4
+ async function resolve_env(env, rc) {
5
+ if (rc && rc.on_env) {
6
+ const patched_env = await rc.on_env(env);
7
+ return typeof patched_env === 'undefined' ? env : patched_env;
8
+ }
9
+ else {
10
+ return env;
11
+ }
12
+ }
@@ -0,0 +1,25 @@
1
+ import resolve_env from './resolve-env';
2
+ import type { EnvData } from './resolve-env';
3
+
4
+ describe('resolve_env', () => {
5
+ const original: EnvData = { original: 'data' };
6
+ it.each`
7
+ on_env_result | expected
8
+ ${1} | ${1}
9
+ ${''} | ${''}
10
+ ${'hello'} | ${'hello'}
11
+ ${0} | ${0}
12
+ ${1} | ${1}
13
+ ${false} | ${false}
14
+ ${true} | ${true}
15
+ ${null} | ${null}
16
+ ${undefined} | ${original}
17
+ `(
18
+ "when hook returns '$on_env_result' env is resolved to '$expected'",
19
+ async ({ on_env_result, expected }: { on_env_result: unknown; expected: unknown }) => {
20
+ const on_env = jest.fn();
21
+ on_env.mockReturnValueOnce(on_env_result);
22
+ expect(await resolve_env(original, { on_env })).toEqual(expected);
23
+ }
24
+ );
25
+ });
@@ -0,0 +1,45 @@
1
+ export type EnvData = Record<string, unknown>;
2
+
3
+ /**
4
+ * Schema definition for a single environment variable.
5
+ * Used in `.rncurc.js` to validate env vars at build time.
6
+ *
7
+ * @example
8
+ * module.exports = {
9
+ * schema: {
10
+ * API_URL: { type: 'string', required: true },
11
+ * TIMEOUT_MS: { type: 'number', required: true },
12
+ * DEBUG: { type: 'boolean', required: false },
13
+ * ENV_NAME: { type: 'string', required: true, pattern: '^(dev|staging|prod)$' },
14
+ * }
15
+ * };
16
+ */
17
+ export interface SchemaField {
18
+ /** Expected type. Strings are always accepted as-is; number/boolean validate parsability. */
19
+ type: 'string' | 'number' | 'boolean';
20
+ /** If true, build fails when this var is missing or empty. Default: false. */
21
+ required?: boolean;
22
+ /** Optional regex pattern the value must match (applied to string representation). */
23
+ pattern?: string;
24
+ }
25
+
26
+ export type Schema = Record<string, SchemaField>;
27
+
28
+ export interface RC {
29
+ on_env?: (env: EnvData) => unknown;
30
+ js_override?: boolean;
31
+ /**
32
+ * Optional schema for build-time validation of env vars.
33
+ * Validated after `on_env` runs (so the hook can add/transform vars before validation).
34
+ */
35
+ schema?: Schema;
36
+ }
37
+
38
+ export default async function resolve_env(env: EnvData, rc?: RC): Promise<EnvData> {
39
+ if (rc && rc.on_env) {
40
+ const patched_env = await rc.on_env(env);
41
+ return typeof patched_env === 'undefined' ? env : (patched_env as EnvData);
42
+ } else {
43
+ return env;
44
+ }
45
+ }
@@ -0,0 +1,24 @@
1
+ // DO NOT COMMIT OR EDIT THIS FILE
2
+ {{#each @root}}
3
+ {{#if (isString this)}}
4
+ #define {{@key}} @"{{{escape this}}}"
5
+ {{/if}}
6
+ {{#if (isBoolean this)}}
7
+ {{#if this}}
8
+ #define {{@key}} @(YES)
9
+ {{else}}
10
+ #define {{@key}} @(NO)
11
+ {{/if}}
12
+ {{/if}}
13
+ {{#if (isNumber this)}}
14
+ #define {{@key}} @({{{this}}})
15
+ {{/if}}
16
+ {{/each}}
17
+
18
+ static NSDictionary *getValues() {
19
+ return @{
20
+ {{#each @root}}
21
+ @"{{@key}}": {{@key}},
22
+ {{/each}}
23
+ };
24
+ }
@@ -0,0 +1,18 @@
1
+ // DO NOT COMMIT OR EDIT THIS FILE
2
+ export interface ConfigVariables {
3
+ {{#each @root}}
4
+ {{#if (isString this)}}
5
+ {{@key}}: string;
6
+ {{/if}}
7
+ {{#if (isBoolean this)}}
8
+ {{@key}}: boolean;
9
+ {{/if}}
10
+ {{#if (isNumber this)}}
11
+ {{@key}}: number;
12
+ {{/if}}
13
+ {{/each}}
14
+ }
15
+
16
+ declare const UltimateConfig: ConfigVariables;
17
+
18
+ export default UltimateConfig;
@@ -0,0 +1 @@
1
+ module.exports = {{{toJSON @root}}}
@@ -0,0 +1,16 @@
1
+ const RN = require("react-native");
2
+
3
+ const IOS_DATA = {{{toJSON @root.ios}}}
4
+
5
+ const ANDROID_DATA = {{{toJSON @root.android}}}
6
+
7
+ module.exports = {
8
+ {{#each @root.ios}}
9
+ get {{{@key}}}() {
10
+ return RN.Platform.select({
11
+ ios: IOS_DATA["{{{@key}}}"],
12
+ android: ANDROID_DATA["{{{@key}}}"],
13
+ });
14
+ },
15
+ {{/each}}
16
+ }
@@ -0,0 +1,4 @@
1
+ // DO NOT COMMIT OR EDIT THIS FILE
2
+ {{#each @root}}
3
+ {{@key}}={{{xcconfigFormat this}}}
4
+ {{/each}}
@@ -0,0 +1,7 @@
1
+ {{#each @root}}
2
+ {{#if (isString this)}}
3
+ {{@key}}: "{{{escape this}}}"
4
+ {{else}}
5
+ {{@key}}: {{{this}}}
6
+ {{/if}}
7
+ {{/each}}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validate_env = validate_env;
4
+ /**
5
+ * Validate env data against a schema defined in `.rncurc.js`.
6
+ * Called after `on_env` so the hook can add/transform vars before validation.
7
+ *
8
+ * Throws with a human-readable error listing ALL failures at once
9
+ * (not just the first one), so users can fix everything in one pass.
10
+ */
11
+ function validate_env(env, schema) {
12
+ const errors = [];
13
+ // Pre-compile all regex patterns once, before iterating over env values.
14
+ // This avoids re-compiling the same pattern for every validated key.
15
+ const compiled_patterns = new Map();
16
+ for (const [key, field] of Object.entries(schema)) {
17
+ if (field.pattern) {
18
+ try {
19
+ compiled_patterns.set(key, new RegExp(field.pattern));
20
+ }
21
+ catch (_a) {
22
+ errors.push(`${key}: invalid regex pattern /${field.pattern}/`);
23
+ }
24
+ }
25
+ }
26
+ for (const [key, field] of Object.entries(schema)) {
27
+ const raw = env[key];
28
+ const missing = raw === undefined || raw === null || String(raw).trim() === '';
29
+ if (field.required && missing) {
30
+ errors.push(`Missing required env var: ${key}`);
31
+ continue; // can't type-check a missing value
32
+ }
33
+ if (missing)
34
+ continue; // optional and not present → OK
35
+ const value = String(raw);
36
+ if (field.type === 'number' && isNaN(Number(value))) {
37
+ errors.push(`${key} must be a number, got "${value}"`);
38
+ }
39
+ if (field.type === 'boolean' &&
40
+ !['true', 'false', '1', '0'].includes(value.toLowerCase())) {
41
+ errors.push(`${key} must be a boolean (true/false/1/0), got "${value}"`);
42
+ }
43
+ const pattern = compiled_patterns.get(key);
44
+ if (pattern && !pattern.test(value)) {
45
+ errors.push(`${key} does not match pattern /${field.pattern}/, got "${value}"`);
46
+ }
47
+ }
48
+ if (errors.length > 0) {
49
+ throw new Error(`\n\n❌ react-native-config-ultimate: env validation failed:\n` +
50
+ errors.map((e) => ` • ${e}`).join('\n') +
51
+ '\n');
52
+ }
53
+ }
@@ -0,0 +1,164 @@
1
+ import { validate_env } from './validate-env';
2
+ import type { Schema } from './resolve-env';
3
+
4
+ describe('validate-env', () => {
5
+ describe('required fields', () => {
6
+ it('throws when a required var is missing', () => {
7
+ const schema: Schema = { API_KEY: { type: 'string', required: true } };
8
+ expect(() => validate_env({}, schema)).toThrow('Missing required env var: API_KEY');
9
+ });
10
+
11
+ it('throws when a required var is an empty string', () => {
12
+ const schema: Schema = { API_KEY: { type: 'string', required: true } };
13
+ expect(() => validate_env({ API_KEY: '' }, schema)).toThrow(
14
+ 'Missing required env var: API_KEY'
15
+ );
16
+ });
17
+
18
+ it('throws when a required var is null', () => {
19
+ const schema: Schema = { API_KEY: { type: 'string', required: true } };
20
+ expect(() => validate_env({ API_KEY: null }, schema)).toThrow(
21
+ 'Missing required env var: API_KEY'
22
+ );
23
+ });
24
+
25
+ it('does not throw when a required var is present', () => {
26
+ const schema: Schema = { API_KEY: { type: 'string', required: true } };
27
+ expect(() => validate_env({ API_KEY: 'my-key' }, schema)).not.toThrow();
28
+ });
29
+
30
+ it('does not throw when an optional var is missing', () => {
31
+ const schema: Schema = { DEBUG: { type: 'boolean', required: false } };
32
+ expect(() => validate_env({}, schema)).not.toThrow();
33
+ });
34
+
35
+ it('does not throw when required is not specified and var is missing', () => {
36
+ const schema: Schema = { DEBUG: { type: 'boolean' } };
37
+ expect(() => validate_env({}, schema)).not.toThrow();
38
+ });
39
+ });
40
+
41
+ describe('type validation', () => {
42
+ describe('number', () => {
43
+ it('accepts valid number strings', () => {
44
+ const schema: Schema = { TIMEOUT: { type: 'number' } };
45
+ expect(() => validate_env({ TIMEOUT: '3000' }, schema)).not.toThrow();
46
+ expect(() => validate_env({ TIMEOUT: '0' }, schema)).not.toThrow();
47
+ expect(() => validate_env({ TIMEOUT: '-1' }, schema)).not.toThrow();
48
+ expect(() => validate_env({ TIMEOUT: '3.14' }, schema)).not.toThrow();
49
+ });
50
+
51
+ it('throws for non-numeric strings', () => {
52
+ const schema: Schema = { TIMEOUT: { type: 'number' } };
53
+ expect(() => validate_env({ TIMEOUT: 'fast' }, schema)).toThrow(
54
+ 'TIMEOUT must be a number, got "fast"'
55
+ );
56
+ expect(() => validate_env({ TIMEOUT: 'abc' }, schema)).toThrow(
57
+ 'TIMEOUT must be a number'
58
+ );
59
+ });
60
+
61
+ it('accepts actual number values', () => {
62
+ const schema: Schema = { TIMEOUT: { type: 'number' } };
63
+ expect(() => validate_env({ TIMEOUT: 3000 }, schema)).not.toThrow();
64
+ });
65
+ });
66
+
67
+ describe('boolean', () => {
68
+ it('accepts valid boolean strings', () => {
69
+ const schema: Schema = { DEBUG: { type: 'boolean' } };
70
+ for (const v of ['true', 'false', '1', '0', 'TRUE', 'FALSE']) {
71
+ expect(() => validate_env({ DEBUG: v }, schema)).not.toThrow();
72
+ }
73
+ });
74
+
75
+ it('throws for invalid boolean strings', () => {
76
+ const schema: Schema = { DEBUG: { type: 'boolean' } };
77
+ expect(() => validate_env({ DEBUG: 'yes' }, schema)).toThrow(
78
+ 'DEBUG must be a boolean (true/false/1/0), got "yes"'
79
+ );
80
+ expect(() => validate_env({ DEBUG: 'enabled' }, schema)).toThrow(
81
+ 'DEBUG must be a boolean'
82
+ );
83
+ });
84
+ });
85
+
86
+ describe('string', () => {
87
+ it('accepts any non-empty value as string', () => {
88
+ const schema: Schema = { NAME: { type: 'string' } };
89
+ expect(() => validate_env({ NAME: 'hello' }, schema)).not.toThrow();
90
+ expect(() => validate_env({ NAME: '123' }, schema)).not.toThrow();
91
+ expect(() => validate_env({ NAME: 'true' }, schema)).not.toThrow();
92
+ });
93
+ });
94
+ });
95
+
96
+ describe('pattern validation', () => {
97
+ it('accepts values matching the pattern', () => {
98
+ const schema: Schema = {
99
+ ENV: { type: 'string', pattern: '^(dev|staging|prod)$' },
100
+ };
101
+ expect(() => validate_env({ ENV: 'dev' }, schema)).not.toThrow();
102
+ expect(() => validate_env({ ENV: 'staging' }, schema)).not.toThrow();
103
+ expect(() => validate_env({ ENV: 'prod' }, schema)).not.toThrow();
104
+ });
105
+
106
+ it('throws for values not matching the pattern', () => {
107
+ const schema: Schema = {
108
+ ENV: { type: 'string', pattern: '^(dev|staging|prod)$' },
109
+ };
110
+ expect(() => validate_env({ ENV: 'production' }, schema)).toThrow(
111
+ 'ENV does not match pattern /^(dev|staging|prod)$/, got "production"'
112
+ );
113
+ });
114
+
115
+ it('pattern validation is skipped for missing optional vars', () => {
116
+ const schema: Schema = {
117
+ ENV: { type: 'string', pattern: '^(dev|staging|prod)$' },
118
+ };
119
+ expect(() => validate_env({}, schema)).not.toThrow();
120
+ });
121
+ });
122
+
123
+ describe('multiple errors', () => {
124
+ it('reports all validation errors at once, not just the first', () => {
125
+ const schema: Schema = {
126
+ API_KEY: { type: 'string', required: true },
127
+ TIMEOUT: { type: 'number', required: true },
128
+ ENV: { type: 'string', pattern: '^(dev|staging|prod)$' },
129
+ };
130
+ let error: Error | undefined;
131
+ try {
132
+ validate_env({ TIMEOUT: 'fast', ENV: 'production' }, schema);
133
+ } catch (e) {
134
+ error = e as Error;
135
+ }
136
+ expect(error).toBeDefined();
137
+ expect(error?.message).toContain('Missing required env var: API_KEY');
138
+ expect(error?.message).toContain('TIMEOUT must be a number');
139
+ expect(error?.message).toContain('ENV does not match pattern');
140
+ });
141
+ });
142
+
143
+ describe('does not throw for valid env', () => {
144
+ it('passes a complete valid env without error', () => {
145
+ const schema: Schema = {
146
+ API_KEY: { type: 'string', required: true },
147
+ TIMEOUT_MS: { type: 'number', required: true },
148
+ DEBUG: { type: 'boolean', required: false },
149
+ ENV_NAME: { type: 'string', required: true, pattern: '^(dev|staging|prod)$' },
150
+ };
151
+ expect(() =>
152
+ validate_env(
153
+ {
154
+ API_KEY: 'secret-key',
155
+ TIMEOUT_MS: '5000',
156
+ ENV_NAME: 'staging',
157
+ // DEBUG intentionally omitted (optional)
158
+ },
159
+ schema
160
+ )
161
+ ).not.toThrow();
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,68 @@
1
+ import type { EnvData, Schema } from './resolve-env';
2
+
3
+ /**
4
+ * Validate env data against a schema defined in `.rncurc.js`.
5
+ * Called after `on_env` so the hook can add/transform vars before validation.
6
+ *
7
+ * Throws with a human-readable error listing ALL failures at once
8
+ * (not just the first one), so users can fix everything in one pass.
9
+ */
10
+ export function validate_env(env: EnvData, schema: Schema): void {
11
+ const errors: string[] = [];
12
+
13
+ // Pre-compile all regex patterns once, before iterating over env values.
14
+ // This avoids re-compiling the same pattern for every validated key.
15
+ const compiled_patterns = new Map<string, RegExp>();
16
+ for (const [key, field] of Object.entries(schema)) {
17
+ if (field.pattern) {
18
+ try {
19
+ compiled_patterns.set(key, new RegExp(field.pattern));
20
+ } catch {
21
+ errors.push(`${key}: invalid regex pattern /${field.pattern}/`);
22
+ }
23
+ }
24
+ }
25
+
26
+ for (const [key, field] of Object.entries(schema)) {
27
+ const raw = env[key];
28
+ const missing =
29
+ raw === undefined || raw === null || String(raw).trim() === '';
30
+
31
+ if (field.required && missing) {
32
+ errors.push(`Missing required env var: ${key}`);
33
+ continue; // can't type-check a missing value
34
+ }
35
+
36
+ if (missing) continue; // optional and not present → OK
37
+
38
+ const value = String(raw);
39
+
40
+ if (field.type === 'number' && isNaN(Number(value))) {
41
+ errors.push(`${key} must be a number, got "${value}"`);
42
+ }
43
+
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
+ );
51
+ }
52
+
53
+ const pattern = compiled_patterns.get(key);
54
+ if (pattern && !pattern.test(value)) {
55
+ errors.push(
56
+ `${key} does not match pattern /${field.pattern}/, got "${value}"`
57
+ );
58
+ }
59
+ }
60
+
61
+ if (errors.length > 0) {
62
+ throw new Error(
63
+ `\n\n❌ react-native-config-ultimate: env validation failed:\n` +
64
+ errors.map((e) => ` • ${e}`).join('\n') +
65
+ '\n'
66
+ );
67
+ }
68
+ }
@@ -0,0 +1,99 @@
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 = write_env;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ /**
41
+ * Atomically write all generated files.
42
+ *
43
+ * Strategy: write each file to a temp path first, then rename (atomic on POSIX).
44
+ * If any write fails, we abort before committing any renames so the project
45
+ * is never left in a partially-written state.
46
+ *
47
+ * On Windows, `fs.renameSync` across drives may fail — in that case we fall
48
+ * back to a direct `writeFileSync` (best-effort, still better than nothing).
49
+ */
50
+ function write_env(files) {
51
+ const tmp_dir = os.tmpdir();
52
+ // Phase 1: write all content to temp files — if anything fails, no real files are touched.
53
+ const pending = [];
54
+ try {
55
+ for (const dest of Object.keys(files)) {
56
+ // Ensure the destination directory exists (handles first-run and hoisted workspaces).
57
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
58
+ const tmp = path.join(tmp_dir, `rncu_${Date.now()}_${Math.random().toString(36).slice(2)}`);
59
+ fs.writeFileSync(tmp, files[dest], 'utf8');
60
+ pending.push({ tmp, dest });
61
+ }
62
+ }
63
+ catch (err) {
64
+ // Clean up any temp files we already created.
65
+ for (const { tmp } of pending) {
66
+ try {
67
+ fs.unlinkSync(tmp);
68
+ }
69
+ catch ( /* ignore */_a) { /* ignore */ }
70
+ }
71
+ throw new Error(`[rncu] Failed to prepare output files: ${err instanceof Error ? err.message : String(err)}`);
72
+ }
73
+ // Phase 2: atomically rename temp → dest.
74
+ // We collect errors and rethrow at the end so the caller gets a clear message.
75
+ const rename_errors = [];
76
+ for (const { tmp, dest } of pending) {
77
+ try {
78
+ fs.renameSync(tmp, dest);
79
+ }
80
+ catch (_b) {
81
+ // Cross-device rename (e.g. Windows different drives) — fall back to copy+delete.
82
+ try {
83
+ fs.copyFileSync(tmp, dest);
84
+ fs.unlinkSync(tmp);
85
+ }
86
+ catch (copy_err) {
87
+ rename_errors.push(`${dest}: ${copy_err instanceof Error ? copy_err.message : String(copy_err)}`);
88
+ try {
89
+ fs.unlinkSync(tmp);
90
+ }
91
+ catch ( /* ignore */_c) { /* ignore */ }
92
+ }
93
+ }
94
+ }
95
+ if (rename_errors.length > 0) {
96
+ throw new Error(`[rncu] Failed to write output files:\n` +
97
+ rename_errors.map((e) => ` • ${e}`).join('\n'));
98
+ }
99
+ }