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,105 @@
1
+ // Mocks must be declared before the module is required.
2
+ const mockWriteFileSync = jest.fn();
3
+ const mockRenameSync = jest.fn();
4
+ const mockUnlinkSync = jest.fn();
5
+ const mockCopyFileSync = jest.fn();
6
+ const mockMkdirSync = jest.fn(); // needed since write-env creates dest dirs
7
+
8
+ jest.mock('fs', () => ({
9
+ writeFileSync: (...args: unknown[]) => mockWriteFileSync(...args),
10
+ renameSync: (...args: unknown[]) => mockRenameSync(...args),
11
+ unlinkSync: (...args: unknown[]) => mockUnlinkSync(...args),
12
+ copyFileSync: (...args: unknown[]) => mockCopyFileSync(...args),
13
+ mkdirSync: (...args: unknown[]) => mockMkdirSync(...args),
14
+ }));
15
+
16
+ jest.mock('os', () => ({ tmpdir: () => '/tmp' }));
17
+
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const write_env: (env: Record<string, string>) => void = require('./write-env').default;
20
+
21
+ // ─── helpers ─────────────────────────────────────────────────────────────────
22
+
23
+ /** Returns the temp file path used for a given destination. */
24
+ function tmp_for(dest: string): string {
25
+ const rename_call = mockRenameSync.mock.calls.find(
26
+ ([, d]: [string, string]) => d === dest
27
+ );
28
+ return rename_call?.[0] as string;
29
+ }
30
+
31
+ // ─── tests ───────────────────────────────────────────────────────────────────
32
+
33
+ describe('write-env', () => {
34
+ beforeEach(() => {
35
+ mockWriteFileSync.mockReset();
36
+ mockRenameSync.mockReset();
37
+ mockUnlinkSync.mockReset();
38
+ mockCopyFileSync.mockReset();
39
+ mockMkdirSync.mockReset();
40
+ });
41
+
42
+ it('writes content to a temp file then renames to destination (atomic write)', () => {
43
+ write_env({ hello: 'world' });
44
+
45
+ // Phase 1: content written to a temp path under /tmp
46
+ expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
47
+ const [tmp_path, content, encoding] = mockWriteFileSync.mock.calls[0] as [string, string, string];
48
+ expect(tmp_path).toMatch(/^\/tmp\/rncu_/);
49
+ expect(content).toBe('world');
50
+ expect(encoding).toBe('utf8');
51
+
52
+ // Phase 2: temp renamed to final destination (atomic)
53
+ expect(mockRenameSync).toHaveBeenCalledWith(tmp_path, 'hello');
54
+ });
55
+
56
+ it('writes multiple files atomically', () => {
57
+ write_env({ hello: 'world', hey: 'you' });
58
+
59
+ expect(mockWriteFileSync).toHaveBeenCalledTimes(2);
60
+ expect(mockRenameSync).toHaveBeenCalledTimes(2);
61
+
62
+ // Each dest should have been renamed from a unique temp path
63
+ const tmp_hello = tmp_for('hello');
64
+ const tmp_hey = tmp_for('hey');
65
+ expect(tmp_hello).toBeDefined();
66
+ expect(tmp_hey).toBeDefined();
67
+ expect(tmp_hello).not.toBe(tmp_hey);
68
+ });
69
+
70
+ it('falls back to copyFileSync + unlinkSync when renameSync fails (cross-device)', () => {
71
+ // Simulate cross-device rename error (e.g. /tmp on different device)
72
+ mockRenameSync.mockImplementation(() => { throw new Error('EXDEV'); });
73
+
74
+ write_env({ hello: 'world' });
75
+
76
+ expect(mockCopyFileSync).toHaveBeenCalledWith(expect.stringMatching(/^\/tmp\/rncu_/), 'hello');
77
+ expect(mockUnlinkSync).toHaveBeenCalled();
78
+ });
79
+
80
+ it('throws a descriptive error if copyFileSync also fails', () => {
81
+ mockRenameSync.mockImplementation(() => { throw new Error('EXDEV'); });
82
+ mockCopyFileSync.mockImplementation(() => { throw new Error('EACCES: permission denied'); });
83
+
84
+ expect(() => write_env({ hello: 'world' })).toThrow(
85
+ /Failed to write output files/
86
+ );
87
+ expect(() => write_env({ hello: 'world' })).toThrow('hello');
88
+ });
89
+
90
+ it('cleans up temp files when Phase 1 write fails', () => {
91
+ // First write succeeds, second fails mid-write
92
+ mockWriteFileSync
93
+ .mockImplementationOnce(() => { /* success */ })
94
+ .mockImplementationOnce(() => { throw new Error('ENOSPC: no space left'); });
95
+
96
+ expect(() => write_env({ hello: 'world', hey: 'you' })).toThrow(
97
+ /Failed to prepare output files/
98
+ );
99
+
100
+ // The first temp file should have been cleaned up
101
+ expect(mockUnlinkSync).toHaveBeenCalledTimes(1);
102
+ // No renames should have happened (no real files touched)
103
+ expect(mockRenameSync).not.toHaveBeenCalled();
104
+ });
105
+ });
@@ -0,0 +1,67 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+
5
+ export type FileMap = Record<string, string>;
6
+
7
+ /**
8
+ * Atomically write all generated files.
9
+ *
10
+ * Strategy: write each file to a temp path first, then rename (atomic on POSIX).
11
+ * If any write fails, we abort before committing any renames so the project
12
+ * is never left in a partially-written state.
13
+ *
14
+ * On Windows, `fs.renameSync` across drives may fail — in that case we fall
15
+ * back to a direct `writeFileSync` (best-effort, still better than nothing).
16
+ */
17
+ export default function write_env(files: FileMap): void {
18
+ const tmp_dir = os.tmpdir();
19
+ // Phase 1: write all content to temp files — if anything fails, no real files are touched.
20
+ const pending: Array<{ tmp: string; dest: string }> = [];
21
+
22
+ try {
23
+ for (const dest of Object.keys(files)) {
24
+ // Ensure the destination directory exists (handles first-run and hoisted workspaces).
25
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
26
+ const tmp = path.join(tmp_dir, `rncu_${Date.now()}_${Math.random().toString(36).slice(2)}`);
27
+ fs.writeFileSync(tmp, files[dest], 'utf8');
28
+ pending.push({ tmp, dest });
29
+ }
30
+ } catch (err) {
31
+ // Clean up any temp files we already created.
32
+ for (const { tmp } of pending) {
33
+ try { fs.unlinkSync(tmp); } catch { /* ignore */ }
34
+ }
35
+ throw new Error(
36
+ `[rncu] Failed to prepare output files: ${err instanceof Error ? err.message : String(err)}`
37
+ );
38
+ }
39
+
40
+ // Phase 2: atomically rename temp → dest.
41
+ // We collect errors and rethrow at the end so the caller gets a clear message.
42
+ const rename_errors: string[] = [];
43
+
44
+ for (const { tmp, dest } of pending) {
45
+ try {
46
+ fs.renameSync(tmp, dest);
47
+ } catch {
48
+ // Cross-device rename (e.g. Windows different drives) — fall back to copy+delete.
49
+ try {
50
+ fs.copyFileSync(tmp, dest);
51
+ fs.unlinkSync(tmp);
52
+ } catch (copy_err) {
53
+ rename_errors.push(
54
+ `${dest}: ${copy_err instanceof Error ? copy_err.message : String(copy_err)}`
55
+ );
56
+ try { fs.unlinkSync(tmp); } catch { /* ignore */ }
57
+ }
58
+ }
59
+ }
60
+
61
+ if (rename_errors.length > 0) {
62
+ throw new Error(
63
+ `[rncu] Failed to write output files:\n` +
64
+ rename_errors.map((e) => ` • ${e}`).join('\n')
65
+ );
66
+ }
67
+ }