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,163 @@
1
+ const mockReadFileSync = jest.fn();
2
+ jest.doMock('fs', () => ({ readFileSync: mockReadFileSync }));
3
+
4
+ const mockParse = jest.fn();
5
+ jest.doMock('dotenv', () => ({ parse: mockParse }));
6
+
7
+ const mockExpand = jest.fn();
8
+ jest.doMock('dotenv-expand', () => ({ expand: mockExpand }));
9
+
10
+ const mockYaml = jest.fn();
11
+ jest.doMock('js-yaml', () => ({ load: mockYaml }));
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
14
+ const load_env: (paths: string | string[]) => Record<string, unknown> =
15
+ require('./load-env').default;
16
+
17
+ describe('load-env', () => {
18
+ beforeEach(() => {
19
+ mockReadFileSync.mockReset();
20
+ mockParse.mockReset();
21
+ mockExpand.mockReset();
22
+ mockYaml.mockReset();
23
+ // Default expand: return parsed as-is (no expansion side effects)
24
+ mockExpand.mockImplementation(
25
+ (input: { parsed: Record<string, string> }) => input
26
+ );
27
+ });
28
+
29
+ describe('dotenv format', () => {
30
+ it('reads a single dotenv file (backward-compatible string arg)', () => {
31
+ mockReadFileSync.mockReturnValueOnce('hello=world');
32
+ mockParse.mockReturnValueOnce({ hello: 'world' });
33
+ const result = load_env('hello');
34
+ expect(mockReadFileSync).toHaveBeenCalledWith('hello', 'utf8');
35
+ expect(mockParse).toHaveBeenCalledWith('hello=world');
36
+ expect(result).toEqual({ hello: 'world' });
37
+ });
38
+
39
+ it('reads a single dotenv file when passed as an array', () => {
40
+ mockReadFileSync.mockReturnValueOnce('hello=world');
41
+ mockParse.mockReturnValueOnce({ hello: 'world' });
42
+ const result = load_env(['hello']);
43
+ expect(mockReadFileSync).toHaveBeenCalledWith('hello', 'utf8');
44
+ expect(result).toEqual({ hello: 'world' });
45
+ });
46
+
47
+ it('merges multiple dotenv files, last file wins for conflicts', () => {
48
+ mockReadFileSync
49
+ .mockReturnValueOnce('A=base\nB=base')
50
+ .mockReturnValueOnce('B=override\nC=new');
51
+ mockParse
52
+ .mockReturnValueOnce({ A: 'base', B: 'base' })
53
+ .mockReturnValueOnce({ B: 'override', C: 'new' });
54
+ const result = load_env(['.env.base', '.env.staging']);
55
+ expect(mockReadFileSync).toHaveBeenCalledTimes(2);
56
+ // expand is called once with the merged raw object
57
+ expect(mockExpand).toHaveBeenCalledWith({
58
+ parsed: { A: 'base', B: 'override', C: 'new' },
59
+ });
60
+ expect(result).toEqual({ A: 'base', B: 'override', C: 'new' });
61
+ });
62
+
63
+ it('expands $VAR references using dotenv-expand', () => {
64
+ mockReadFileSync.mockReturnValueOnce('BASE=https://api.com\nURL=$BASE/v1');
65
+ mockParse.mockReturnValueOnce({ BASE: 'https://api.com', URL: '$BASE/v1' });
66
+ mockExpand.mockReturnValueOnce({
67
+ parsed: { BASE: 'https://api.com', URL: 'https://api.com/v1' },
68
+ });
69
+ const result = load_env('.env');
70
+ expect(result).toEqual({
71
+ BASE: 'https://api.com',
72
+ URL: 'https://api.com/v1',
73
+ });
74
+ });
75
+
76
+ it('expands cross-file $VAR references when merging multiple files', () => {
77
+ mockReadFileSync
78
+ .mockReturnValueOnce('BASE_URL=https://api.com')
79
+ .mockReturnValueOnce('API_URL=$BASE_URL/v1');
80
+ mockParse
81
+ .mockReturnValueOnce({ BASE_URL: 'https://api.com' })
82
+ .mockReturnValueOnce({ API_URL: '$BASE_URL/v1' });
83
+ // Expand is called with merged raw — so cross-file reference resolves
84
+ mockExpand.mockReturnValueOnce({
85
+ parsed: {
86
+ BASE_URL: 'https://api.com',
87
+ API_URL: 'https://api.com/v1',
88
+ },
89
+ });
90
+ const result = load_env(['.env.base', '.env.staging']);
91
+ expect(mockExpand).toHaveBeenCalledWith({
92
+ parsed: { BASE_URL: 'https://api.com', API_URL: '$BASE_URL/v1' },
93
+ });
94
+ expect(result).toEqual({
95
+ BASE_URL: 'https://api.com',
96
+ API_URL: 'https://api.com/v1',
97
+ });
98
+ });
99
+ });
100
+
101
+ describe('yaml format', () => {
102
+ it.each`
103
+ extension
104
+ ${'yml'}
105
+ ${'yaml'}
106
+ `(
107
+ "reads yaml when extension is '.$extension'",
108
+ ({ extension }: { extension: string }) => {
109
+ mockReadFileSync.mockReturnValueOnce(Buffer.from('data'));
110
+ mockYaml.mockReturnValueOnce({ hello: 'world' });
111
+ const result = load_env(`hello.${extension}`);
112
+ expect(mockReadFileSync).toHaveBeenCalledWith(`hello.${extension}`);
113
+ expect(mockYaml).toHaveBeenCalledWith('data');
114
+ expect(result).toEqual({ hello: 'world' });
115
+ }
116
+ );
117
+
118
+ it('merges multiple yaml files, last file wins for conflicts', () => {
119
+ mockReadFileSync
120
+ .mockReturnValueOnce(Buffer.from('A: base\nB: base'))
121
+ .mockReturnValueOnce(Buffer.from('B: override\nC: new'));
122
+ mockYaml
123
+ .mockReturnValueOnce({ A: 'base', B: 'base' })
124
+ .mockReturnValueOnce({ B: 'override', C: 'new' });
125
+ const result = load_env(['base.yaml', 'staging.yaml']);
126
+ expect(mockReadFileSync).toHaveBeenCalledTimes(2);
127
+ expect(result).toEqual({ A: 'base', B: 'override', C: 'new' });
128
+ });
129
+
130
+ describe.each`
131
+ extension
132
+ ${'yml'}
133
+ ${'yaml'}
134
+ `(
135
+ "throws when yaml is not an object with extension '.$extension'",
136
+ ({ extension }: { extension: string }) => {
137
+ it.each`
138
+ content
139
+ ${'abc:def'}
140
+ ${false}
141
+ ${true}
142
+ ${42}
143
+ ${null}
144
+ ${undefined}
145
+ `("when content is '$content'", ({ content }: { content: unknown }) => {
146
+ mockReadFileSync.mockReturnValueOnce(Buffer.from('data'));
147
+ mockYaml.mockReturnValueOnce(content);
148
+ expect(() => {
149
+ load_env(`hello.${extension}`);
150
+ }).toThrow();
151
+ });
152
+ }
153
+ );
154
+ });
155
+
156
+ describe('edge cases', () => {
157
+ it('throws when no files are provided', () => {
158
+ expect(() => load_env([])).toThrow(
159
+ 'No env file specified'
160
+ );
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,84 @@
1
+ import * as dotenv from 'dotenv';
2
+ import { expand } from 'dotenv-expand';
3
+ import * as yaml from 'js-yaml';
4
+ import * as path from 'path';
5
+ import * as fs from 'fs';
6
+
7
+ import type { EnvData } from './resolve-env';
8
+
9
+ type FileFormat = 'dotenv' | 'yaml';
10
+
11
+ function detect_format(config_path: string): FileFormat {
12
+ const { ext } = path.parse(config_path);
13
+ return ext === '.yml' || ext === '.yaml' ? 'yaml' : 'dotenv';
14
+ }
15
+
16
+ function read_yaml(config_path: string): EnvData {
17
+ const data = yaml.load(fs.readFileSync(config_path).toString());
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
+ );
22
+ }
23
+ return data as EnvData;
24
+ }
25
+
26
+ /**
27
+ * Load one or more env files and merge them (last file wins for conflicting keys).
28
+ *
29
+ * Dotenv files (.env, .env.staging, etc.):
30
+ * - All files are merged first, then variable expansion runs once.
31
+ * - This means cross-file `$VAR` references work:
32
+ * .env.base: BASE_URL=https://api.example.com
33
+ * .env.staging: API_URL=$BASE_URL/v1 → https://api.example.com/v1
34
+ *
35
+ * YAML files (.yml, .yaml):
36
+ * - Each file is loaded and shallow-merged (last wins for top-level keys).
37
+ * - No variable expansion is applied (use YAML anchors instead).
38
+ *
39
+ * @example
40
+ * // Single file (backward-compatible):
41
+ * load_env('.env')
42
+ *
43
+ * // Multi-file merge:
44
+ * load_env(['.env.base', '.env.staging'])
45
+ */
46
+ export default function load_env(config_paths: string | string[]): EnvData {
47
+ const paths = Array.isArray(config_paths) ? config_paths : [config_paths];
48
+
49
+ if (paths.length === 0) {
50
+ throw new Error(
51
+ 'No env file specified. Usage: rncu <env-file> [env-file2 ...]'
52
+ );
53
+ }
54
+
55
+ const formats = paths.map(detect_format);
56
+ const allDotenv = formats.every((f) => f === 'dotenv');
57
+
58
+ if (allDotenv) {
59
+ // Merge raw parsed content first, then expand once —
60
+ // so cross-file $VAR references resolve correctly.
61
+ const raw: Record<string, string> = {};
62
+ for (const p of paths) {
63
+ const content = fs.readFileSync(p, 'utf8');
64
+ Object.assign(raw, dotenv.parse(content));
65
+ }
66
+ const result = expand({ parsed: raw });
67
+ return (result.parsed ?? raw) as EnvData;
68
+ }
69
+
70
+ // YAML or mixed: load each file individually and shallow-merge.
71
+ const merged: EnvData = {};
72
+ for (let i = 0; i < paths.length; i++) {
73
+ const p = paths[i];
74
+ if (formats[i] === 'yaml') {
75
+ Object.assign(merged, read_yaml(p));
76
+ } else {
77
+ const content = fs.readFileSync(p, 'utf8');
78
+ const parsed = dotenv.parse(content);
79
+ const expanded = expand({ parsed });
80
+ Object.assign(merged, expanded.parsed ?? parsed);
81
+ }
82
+ }
83
+ return merged;
84
+ }
package/src/main.js ADDED
@@ -0,0 +1,34 @@
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 = main;
7
+ const load_env_1 = __importDefault(require("./load-env"));
8
+ const render_env_1 = __importDefault(require("./render-env"));
9
+ const write_env_1 = __importDefault(require("./write-env"));
10
+ const flatten_1 = __importDefault(require("./flatten"));
11
+ const resolve_env_1 = __importDefault(require("./resolve-env"));
12
+ const validate_env_1 = require("./validate-env");
13
+ /**
14
+ * Main build-time pipeline:
15
+ * load → resolve (on_env hook) → validate (schema) → flatten → render → write
16
+ *
17
+ * @param project_root Root of the React Native project
18
+ * @param lib_root Root of the react-native-config-ultimate install
19
+ * @param env_file Path(s) to env file(s). Multiple files are merged (last wins).
20
+ * @param rc Optional RC config from `.rncurc.js`
21
+ */
22
+ async function main(project_root, lib_root, env_file, rc) {
23
+ const env = await (0, resolve_env_1.default)((0, load_env_1.default)(env_file), rc);
24
+ if (rc === null || rc === void 0 ? void 0 : rc.schema) {
25
+ (0, validate_env_1.validate_env)(env, rc.schema);
26
+ }
27
+ const flat = {
28
+ ios: (0, flatten_1.default)(env, 'ios'),
29
+ android: (0, flatten_1.default)(env, 'android'),
30
+ web: (0, flatten_1.default)(env, 'web'),
31
+ };
32
+ const files_to_write = (0, render_env_1.default)(project_root, lib_root, flat, rc);
33
+ (0, write_env_1.default)(files_to_write);
34
+ }
@@ -0,0 +1,171 @@
1
+ const mock_load_env = jest.fn();
2
+ jest.doMock('./load-env', () => ({ __esModule: true, default: mock_load_env }));
3
+ const mock_render_env = jest.fn();
4
+ jest.doMock('./render-env', () => ({ __esModule: true, default: mock_render_env }));
5
+ const mock_write_env = jest.fn();
6
+ jest.doMock('./write-env', () => ({ __esModule: true, default: mock_write_env }));
7
+ const mock_flatten = jest.fn();
8
+ jest.doMock('./flatten', () => ({ __esModule: true, default: mock_flatten }));
9
+ const mock_validate_env = jest.fn();
10
+ jest.doMock('./validate-env', () => ({ validate_env: mock_validate_env }));
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
13
+ const main: (...args: unknown[]) => Promise<void> = require('./main').default;
14
+
15
+
16
+ export const files_to_assert = [
17
+ 'ios/rncu.xcconfig',
18
+ 'node_modules/react-native-config-ultimate/ios/ConfigValues.h',
19
+ 'node_modules/react-native-config-ultimate/android/rncu.yaml',
20
+ 'node_modules/react-native-config-ultimate/index.d.ts',
21
+ 'node_modules/react-native-config-ultimate/index.web.js',
22
+ 'node_modules/react-native-config-ultimate/override.js',
23
+ ];
24
+
25
+ describe('main', () => {
26
+ beforeEach(() => {
27
+ mock_load_env.mockReset();
28
+ mock_render_env.mockReset();
29
+ mock_write_env.mockReset();
30
+ mock_flatten.mockReset();
31
+ mock_validate_env.mockReset();
32
+ });
33
+
34
+ it('execute render with paths (string arg — backward-compatible)', async () => {
35
+ mock_load_env.mockReturnValueOnce({ data: true });
36
+ mock_flatten.mockReturnValueOnce({ data: true, ios: true });
37
+ mock_flatten.mockReturnValueOnce({ data: true, android: true });
38
+ mock_flatten.mockReturnValueOnce({ data: true, web: true });
39
+ mock_render_env.mockReturnValueOnce({ hello: 'world' });
40
+ await main(
41
+ 'project',
42
+ 'project/node_modules/react-native-config-ultimate',
43
+ 'file'
44
+ );
45
+ expect(mock_load_env).toHaveBeenCalledWith('file');
46
+ expect(mock_flatten).toHaveBeenCalledWith({ data: true }, 'ios');
47
+ expect(mock_flatten).toHaveBeenCalledWith({ data: true }, 'android');
48
+ expect(mock_flatten).toHaveBeenCalledWith({ data: true }, 'web');
49
+ expect(mock_render_env).toHaveBeenCalledWith(
50
+ 'project',
51
+ 'project/node_modules/react-native-config-ultimate',
52
+ {
53
+ ios: { data: true, ios: true },
54
+ android: { data: true, android: true },
55
+ web: { data: true, web: true },
56
+ },
57
+ undefined
58
+ );
59
+ expect(mock_write_env).toHaveBeenCalledWith({ hello: 'world' });
60
+ });
61
+
62
+ it('passes array of env files to load_env (multi-file merge)', async () => {
63
+ mock_load_env.mockReturnValueOnce({ data: true });
64
+ mock_flatten.mockReturnValue({});
65
+ mock_render_env.mockReturnValueOnce({});
66
+ await main(
67
+ 'project',
68
+ 'project/node_modules/react-native-config-ultimate',
69
+ ['.env.base', '.env.staging']
70
+ );
71
+ expect(mock_load_env).toHaveBeenCalledWith(['.env.base', '.env.staging']);
72
+ });
73
+ describe('rc.on_env', () => {
74
+ it('invoke rc hook with config before flattening', async () => {
75
+ const on_env = jest.fn();
76
+ mock_load_env.mockReturnValueOnce({ data: true });
77
+ await main(
78
+ 'project',
79
+ 'project/node_modules/react-native-config-ultimate',
80
+ 'file',
81
+ { on_env }
82
+ );
83
+ expect(on_env).toHaveBeenCalledWith({ data: true });
84
+ expect(mock_flatten).toHaveBeenCalledWith({ data: true }, 'ios');
85
+ expect(mock_flatten).toHaveBeenCalledWith({ data: true }, 'android');
86
+ expect(mock_flatten).toHaveBeenCalledWith({ data: true }, 'web');
87
+ });
88
+ it('hook can add or remove values', async () => {
89
+ const on_env = jest.fn();
90
+ on_env.mockImplementation((env: Record<string, unknown>) => {
91
+ const { key1, ...rest } = env;
92
+ void key1;
93
+ return { ...rest, key2: 'hello' };
94
+ });
95
+ mock_load_env.mockReturnValueOnce({ data: true, key1: 'bye' });
96
+ await main(
97
+ 'project',
98
+ 'project/node_modules/react-native-config-ultimate',
99
+ 'file',
100
+ { on_env }
101
+ );
102
+ expect(on_env).toHaveBeenCalledWith({ data: true, key1: 'bye' });
103
+ expect(mock_flatten).toHaveBeenCalledWith({ data: true, key2: 'hello' }, 'ios');
104
+ expect(mock_flatten).toHaveBeenCalledWith({ data: true, key2: 'hello' }, 'android');
105
+ expect(mock_flatten).toHaveBeenCalledWith({ data: true, key2: 'hello' }, 'web');
106
+ });
107
+ });
108
+
109
+ describe('rc.schema', () => {
110
+ it('calls validate_env when schema is provided', async () => {
111
+ const schema = { API_KEY: { type: 'string' as const, required: true } };
112
+ mock_load_env.mockReturnValueOnce({ API_KEY: 'secret' });
113
+ mock_flatten.mockReturnValue({});
114
+ mock_render_env.mockReturnValueOnce({});
115
+ await main(
116
+ 'project',
117
+ 'project/node_modules/react-native-config-ultimate',
118
+ 'file',
119
+ { schema }
120
+ );
121
+ expect(mock_validate_env).toHaveBeenCalledWith({ API_KEY: 'secret' }, schema);
122
+ });
123
+
124
+ it('does not call validate_env when no schema is provided', async () => {
125
+ mock_load_env.mockReturnValueOnce({ data: true });
126
+ mock_flatten.mockReturnValue({});
127
+ mock_render_env.mockReturnValueOnce({});
128
+ await main(
129
+ 'project',
130
+ 'project/node_modules/react-native-config-ultimate',
131
+ 'file'
132
+ );
133
+ expect(mock_validate_env).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it('validates env AFTER on_env hook runs (hook output is validated)', async () => {
137
+ const schema = { INJECTED_KEY: { type: 'string' as const, required: true } };
138
+ const on_env = jest.fn().mockReturnValue({ INJECTED_KEY: 'from-hook' });
139
+ mock_load_env.mockReturnValueOnce({});
140
+ mock_flatten.mockReturnValue({});
141
+ mock_render_env.mockReturnValueOnce({});
142
+ await main(
143
+ 'project',
144
+ 'project/node_modules/react-native-config-ultimate',
145
+ 'file',
146
+ { on_env, schema }
147
+ );
148
+ // validate_env receives the HOOK output, not the raw env
149
+ expect(mock_validate_env).toHaveBeenCalledWith(
150
+ { INJECTED_KEY: 'from-hook' },
151
+ schema
152
+ );
153
+ });
154
+
155
+ it('propagates validation error thrown by validate_env', async () => {
156
+ const schema = { API_KEY: { type: 'string' as const, required: true } };
157
+ mock_load_env.mockReturnValueOnce({});
158
+ mock_validate_env.mockImplementation(() => {
159
+ throw new Error('❌ validation failed: Missing required env var: API_KEY');
160
+ });
161
+ await expect(
162
+ main(
163
+ 'project',
164
+ 'project/node_modules/react-native-config-ultimate',
165
+ 'file',
166
+ { schema }
167
+ )
168
+ ).rejects.toThrow('Missing required env var: API_KEY');
169
+ });
170
+ });
171
+ });
package/src/main.ts ADDED
@@ -0,0 +1,39 @@
1
+ import load_env from './load-env';
2
+ import render_env from './render-env';
3
+ import write_env from './write-env';
4
+ import flatten from './flatten';
5
+ import resolve_env from './resolve-env';
6
+ import { validate_env } from './validate-env';
7
+
8
+ import type { RC, EnvData } from './resolve-env';
9
+ import type { EnvConfig } from './flatten';
10
+
11
+ /**
12
+ * Main build-time pipeline:
13
+ * load → resolve (on_env hook) → validate (schema) → flatten → render → write
14
+ *
15
+ * @param project_root Root of the React Native project
16
+ * @param lib_root Root of the react-native-config-ultimate install
17
+ * @param env_file Path(s) to env file(s). Multiple files are merged (last wins).
18
+ * @param rc Optional RC config from `.rncurc.js`
19
+ */
20
+ export default async function main(
21
+ project_root: string,
22
+ lib_root: string,
23
+ env_file: string | string[],
24
+ rc?: RC
25
+ ): Promise<void> {
26
+ const env: EnvData = await resolve_env(load_env(env_file), rc);
27
+
28
+ if (rc?.schema) {
29
+ validate_env(env, rc.schema);
30
+ }
31
+
32
+ const flat = {
33
+ ios: flatten(env as EnvConfig, 'ios'),
34
+ android: flatten(env as EnvConfig, 'android'),
35
+ web: flatten(env as EnvConfig, 'web'),
36
+ };
37
+ const files_to_write = render_env(project_root, lib_root, flat, rc);
38
+ write_env(files_to_write);
39
+ }
@@ -0,0 +1,110 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.default = render_env;
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs"));
42
+ const handlebars_1 = __importDefault(require("handlebars"));
43
+ const code_file_name = 'ConfigValues';
44
+ const config_file_name = 'rncu';
45
+ // ─── Handlebars helpers (registered once at module load, not per-call) ────────
46
+ function is_string(value) {
47
+ return typeof value === 'string';
48
+ }
49
+ function is_number(value) {
50
+ return typeof value === 'number';
51
+ }
52
+ function is_boolean(value) {
53
+ return typeof value === 'boolean';
54
+ }
55
+ function escape(value) {
56
+ if (is_string(value)) {
57
+ return value.replace(/"/gm, '\\"');
58
+ }
59
+ return value;
60
+ }
61
+ function xcconfig_format(value) {
62
+ if (is_string(value)) {
63
+ return value.replace(/\/\//gm, '/$()/');
64
+ }
65
+ return value;
66
+ }
67
+ function to_json(value) {
68
+ return JSON.stringify(value, null, 2);
69
+ }
70
+ handlebars_1.default.registerHelper('isBoolean', is_boolean);
71
+ handlebars_1.default.registerHelper('isString', is_string);
72
+ handlebars_1.default.registerHelper('isNumber', is_number);
73
+ handlebars_1.default.registerHelper('escape', escape);
74
+ handlebars_1.default.registerHelper('xcconfigFormat', xcconfig_format);
75
+ handlebars_1.default.registerHelper('toJSON', to_json);
76
+ // ─── Template cache (compiled once per template name, not per build) ──────────
77
+ const template_cache = new Map();
78
+ function get_compiled_template(template_name) {
79
+ const cached = template_cache.get(template_name);
80
+ if (cached)
81
+ return cached;
82
+ const template_path = path.join(__dirname, 'templates', `${template_name}.handlebars`);
83
+ const compiled = handlebars_1.default.compile(fs.readFileSync(template_path, 'utf8'));
84
+ template_cache.set(template_name, compiled);
85
+ return compiled;
86
+ }
87
+ function render_template(template_name, data) {
88
+ return get_compiled_template(template_name)(data);
89
+ }
90
+ function render_env(project_root, lib_root, env, rc) {
91
+ const { ios, android, web } = env;
92
+ const map = {
93
+ [path.join(lib_root, 'index.d.ts')]: render_template('index.d.ts', ios),
94
+ [path.join(lib_root, 'index.web.js')]: render_template('index.web.js', web),
95
+ [path.join(lib_root, 'ios', `${code_file_name}.h`)]: render_template('ConfigValues.h', ios),
96
+ [path.join(lib_root, 'android', 'rncu.yaml')]: render_template('rncu.yaml', android),
97
+ };
98
+ // Only write xcconfig if the project has an ios folder.
99
+ // All RN apps have it; some react-native-web apps may not.
100
+ 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);
103
+ }
104
+ const js_override = rc && typeof rc.js_override === 'boolean' && rc.js_override;
105
+ map[path.join(lib_root, 'override.js')] = render_template('override.js', {
106
+ ios: js_override ? ios : {},
107
+ android: js_override ? android : {},
108
+ });
109
+ return map;
110
+ }