react-native-config 1.6.1 → 1.7.0

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 CHANGED
@@ -1,8 +1,24 @@
1
- # Config variables for React Native apps
1
+ <p align="center">
2
+ <img src="brand/logo.png" alt="" width="120" height="120" />
3
+ </p>
2
4
 
3
- Module to expose config variables to your javascript code in React Native, supporting iOS, Android, macOS and Windows.
5
+ <h1 align="center">react-native-config</h1>
4
6
 
5
- Bring some [12 factor](http://12factor.net/config) love to your mobile apps!
7
+ <p align="center">
8
+ Config variables for React Native apps, supporting iOS, Android, macOS and Windows.
9
+ </p>
10
+
11
+ Expose config variables to your JavaScript code — bring some
12
+ [12 factor](http://12factor.net/config) love to your mobile apps!
13
+
14
+ > [!TIP]
15
+ > **Help keep this library maintained.** If `react-native-config` is useful to you or your
16
+ > company, please consider
17
+ > [sponsoring it on Open Collective](https://opencollective.com/react-native-config).
18
+ > Contributions go toward the maintenance work that keeps it working across new React Native
19
+ > releases. See
20
+ > [#885](https://github.com/react-native-config/react-native-config/issues/885) for what
21
+ > sponsorship pays for and other ways to help.
6
22
 
7
23
  ## Basic Usage
8
24
 
@@ -26,6 +42,9 @@ Keep in mind this module doesn't obfuscate or encrypt secrets for packaging, so
26
42
 
27
43
  ## Setup
28
44
 
45
+ > ⚠️ Note (Android): react-native-config v1.6.0+ requires React Native 0.74 or higher.
46
+ > If you use an older RN version, see Troubleshooting below.
47
+
29
48
  Install the package:
30
49
 
31
50
  ```
@@ -34,20 +53,34 @@ $ yarn add react-native-config
34
53
 
35
54
  Link the library:
36
55
 
37
- (Note: For React Native 0.60 or greater, [autolinking](https://reactnative.dev/blog/2019/07/03/version-60#native-modules-are-now-autolinked) is available)
56
+ On React Native 0.60 and above there is no link step — the library is
57
+ [autolinked](https://reactnative.dev/blog/2019/07/03/version-60#native-modules-are-now-autolinked).
58
+ Rebuild the app so the native side is picked up. On iOS / macOS, install the pod first:
59
+
60
+ ```
61
+ (cd ios; pod install)
62
+ ```
63
+
64
+ > [!WARNING]
65
+ > Do not link this library manually on React Native 0.60 or above, and do not disable its
66
+ > autolinking in `react-native.config.js`. Autolinking is what registers the native module and,
67
+ > on the New Architecture, generates its TurboModule bindings — adding
68
+ > `include ':react-native-config'` to `android/settings.gradle` instead does neither, and the
69
+ > module then resolves to `null` at runtime. See
70
+ > [TypeError: Cannot read property 'getConfig' of null](#typeerror-cannot-read-property-getconfig-of-null).
38
71
 
39
72
  (Note: For Windows, this module supports autolinking when used with `react-native-windows@0.63`
40
73
  or later. For earlier versions you need to manually link the module.)
41
74
 
75
+ <details>
76
+ <summary><b>Manual linking</b> — only for React Native below 0.60, or react-native-windows below 0.63</summary>
77
+
42
78
  ```
43
79
  $ react-native link react-native-config
44
80
  ```
45
81
 
46
- if cocoapods are used in the project then pod has to be installed as well:
47
-
48
- ```
49
- (cd ios; pod install)
50
- ```
82
+ (`react-native link` was removed from the React Native CLI; it is only available on the older
83
+ versions these instructions apply to.)
51
84
 
52
85
  - Manual Link (iOS / macOS)
53
86
 
@@ -115,6 +148,8 @@ if cocoapods are used in the project then pod has to be installed as well:
115
148
 
116
149
  Add `PackageProviders().Append(winrt::RNCConfig::ReactPackageProvider());` before `InitializeComponent();`.
117
150
 
151
+ </details>
152
+
118
153
  ### Extra step for Android
119
154
  #### Using RN-Integrate
120
155
  Apply extra steps automatically:
@@ -132,16 +167,29 @@ apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.grad
132
167
 
133
168
  #### Advanced Android Setup
134
169
 
135
- In `android/app/build.gradle`, if you use `applicationIdSuffix` or `applicationId` that is different from the package name indicated in `AndroidManifest.xml` in `<manifest package="...">` tag, for example, to support different build variants:
136
- Add this in `android/app/build.gradle`
170
+ `BuildConfig` is generated in your module's `namespace`, which is not always the same as its
171
+ `applicationId` `applicationIdSuffix` and per-flavor `applicationId` change the latter and
172
+ leave the former alone. The library resolves this for you: it looks for `BuildConfig` in the
173
+ package declaring your `Application` class (that is, the `namespace`) before falling back to the
174
+ `applicationId`, so the common variant setups need no extra configuration.
175
+
176
+ If your `BuildConfig` lives somewhere neither of those points at, name the package explicitly in
177
+ `android/app/build.gradle`:
137
178
 
138
179
  ```
139
180
  defaultConfig {
140
181
  ...
141
- resValue "string", "build_config_package", "YOUR_PACKAGE_NAME_IN_ANDROIDMANIFEST_XML"
182
+ resValue "string", "build_config_package", "YOUR_NAMESPACE"
142
183
  }
143
184
  ```
144
185
 
186
+ where `YOUR_NAMESPACE` matches the `namespace` in `android/app/build.gradle` (on React Native
187
+ 0.72 and older, the `package` attribute of `<manifest>` in `AndroidManifest.xml`). This value
188
+ takes priority over the automatic resolution above.
189
+
190
+ If the config arrives in JS as `{}`, check logcat for `ReactConfig: Could not find BuildConfig
191
+ class` — the message lists every package that was tried.
192
+
145
193
  ## TypeScript declaration for your .env file
146
194
 
147
195
  If you want to get autocompletion and typesafety for your .env files. Create a file named `react-native-config.d.ts` in the same directory where you put your type declarations, and add the following contents:
@@ -320,44 +368,63 @@ Also note that besides requiring lowercase, the matching is done with `buildFlav
320
368
 
321
369
  #### iOS / macOS
322
370
 
323
- The basic idea in iOS is to have one scheme per environment file, so you can easily alternate between them.
371
+ There are two ways to pick the env file. Prefer the first: nothing is copied over anything else,
372
+ and the same setup works from Xcode, the CLI and CI.
324
373
 
325
- Start by creating a new scheme:
374
+ ##### Per build configuration (recommended)
326
375
 
327
- - In the Xcode menu, go to Product > Scheme > Edit Scheme
328
- - Click Duplicate Scheme on the bottom
329
- - Give it a proper name on the top left. For instance: "Myapp (staging)"
330
- - Make sure the "Shared" checkbox is checked so the scheme is added to your version control system
376
+ Create one build configuration per environment — in Xcode, select the project, then _Info_ >
377
+ _Configurations_, and duplicate `Debug` and `Release` into e.g. `Debug-Staging` and
378
+ `Release-Staging`. Name the env files to match, and one line in the `Podfile` covers every
379
+ configuration, present and future:
331
380
 
332
- Then edit the newly created scheme to make it use a different env file. From the same "manage scheme" window:
381
+ ```ruby
382
+ post_install do |installer|
383
+ installer.pods_project.targets.each do |target|
384
+ next unless target.name == 'react-native-config'
333
385
 
334
- - Expand the "Build" settings on left
335
- - Click "Pre-actions", and under the plus sign select "New Run Script Action"
336
- - Where it says "Type a script or drag a script file", type:
337
- ```
338
- cp "${PROJECT_DIR}/../.env.staging" "${PROJECT_DIR}/../.env" # replace .env.staging for your file
339
- ```
340
- Also ensure that "Provide build settings from", just above the script, has a value selected so that PROJECT_DIR is set.
386
+ target.build_configurations.each do |config|
387
+ config.build_settings['ENVFILE'] = '.env.$(CONFIGURATION)'
388
+ end
389
+ end
390
+ end
391
+ ```
341
392
 
342
- Alternatively, if you have separated build configurations, you may easily set the different envfiles per configuration by adding these lines into the end of Podfile:
393
+ With configurations `Debug-Staging` and `Release-Staging`, that reads `.env.Debug-Staging` and
394
+ `.env.Release-Staging` from the project root. The path is relative to the project root, and
395
+ `$(CONFIGURATION)` — or any other build setting, such as `$(PLATFORM_NAME)` — is expanded when the
396
+ script runs.
397
+
398
+ If your env files are not named after your configurations, map them explicitly instead:
343
399
 
344
400
  ```ruby
345
401
  ENVFILES = {
346
- 'Debug' => '$(PODS_ROOT)/../../.env.debug',
347
- 'Release' => '$(PODS_ROOT)/../../.env.production',
402
+ 'Debug' => '.env.development',
403
+ 'Release' => '.env.production',
404
+ 'Debug-Staging' => '.env.staging',
405
+ 'Release-Staging' => '.env.staging',
348
406
  }
349
407
  post_install do |installer|
350
408
  installer.pods_project.targets.each do |target|
409
+ next unless target.name == 'react-native-config'
410
+
351
411
  target.build_configurations.each do |config|
352
- if target.name == 'react-native-config'
353
- config.build_settings['ENVFILE'] = ENVFILES[config.name]
354
- end
412
+ config.build_settings['ENVFILE'] = ENVFILES[config.name]
355
413
  end
356
414
  end
357
415
  end
358
416
  ```
359
417
 
360
- Note that if you have flipper enabled in your Podfile, you must move the `flipper_post_install` into the newely added hook since Podfile doesn't allow multiple `post_install` hooks.
418
+ Run `pod install` after editing the `Podfile`. The chosen file is echoed in the build log search
419
+ it for `ENVFILE=` to see what was selected and what it expanded to.
420
+
421
+ > [!IMPORTANT]
422
+ > If `ENVFILE` names a file that does not exist, the build does **not** fail: it falls back to
423
+ > `.env`, which means a correct-looking setup can quietly ship the wrong environment. The build log
424
+ > flags this — search for `ENVFILE was set, but that file is missing`.
425
+
426
+ Note that if you have flipper enabled in your Podfile, you must move the `flipper_post_install`
427
+ into the newly added hook, since Podfile doesn't allow multiple `post_install` hooks.
361
428
 
362
429
  ```diff
363
430
  target 'MyApp' do
@@ -372,15 +439,51 @@ Note that if you have flipper enabled in your Podfile, you must move the `flippe
372
439
  + flipper_post_install(installer)
373
440
 
374
441
  installer.pods_project.targets.each do |target|
442
+ next unless target.name == 'react-native-config'
443
+
375
444
  target.build_configurations.each do |config|
376
- if target.name == 'react-native-config'
377
- config.build_settings['ENVFILE'] = ENVFILES[config.name]
378
- end
445
+ config.build_settings['ENVFILE'] = '.env.$(CONFIGURATION)'
379
446
  end
380
447
  end
381
448
  end
382
449
  ```
383
450
 
451
+ ##### If you have several app targets
452
+
453
+ Selection is per build **configuration**, not per target. CocoaPods builds one
454
+ `react-native-config` pod target per configuration and shares it between the app targets that
455
+ depend on it, so two targets built as `Debug` both get the same env file — there is no point at
456
+ which the library can tell them apart.
457
+
458
+ Give each environment its own build configurations (`Debug-Staging`, `Release-Staging`, …), set
459
+ each target's scheme to use them, and the setup above then distinguishes them correctly.
460
+
461
+ ##### Per scheme (copies the file)
462
+
463
+ The older approach: one scheme per environment, each copying its env file over `.env` before the
464
+ build. It rewrites a file in your project on every build, and the copy is easy to forget on CI, so
465
+ prefer the configuration-based setup above unless you specifically need this.
466
+
467
+ Start by creating a new scheme:
468
+
469
+ - In the Xcode menu, go to Product > Scheme > Edit Scheme
470
+ - Click Duplicate Scheme on the bottom
471
+ - Give it a proper name on the top left. For instance: "Myapp (staging)"
472
+ - Make sure the "Shared" checkbox is checked so the scheme is added to your version control system
473
+
474
+ Then edit the newly created scheme to make it use a different env file. From the same "manage
475
+ scheme" window:
476
+
477
+ - Expand the "Build" settings on left
478
+ - Click "Pre-actions", and under the plus sign select "New Run Script Action"
479
+ - Where it says "Type a script or drag a script file", type:
480
+ ```
481
+ cp "${PROJECT_DIR}/../.env.staging" "${PROJECT_DIR}/../.env" # replace .env.staging for your file
482
+ ```
483
+
484
+ Also ensure that "Provide build settings from", just above the script, has a value selected so that
485
+ PROJECT_DIR is set.
486
+
384
487
  ## Troubleshooting
385
488
 
386
489
  ### Problems with Proguard
@@ -395,12 +498,78 @@ If using Dexguard, the shrinking phase will remove resources it thinks are unuse
395
498
 
396
499
  -keepresources string/build_config_package
397
500
 
501
+ ### Config is empty (`{}`) on iOS
502
+
503
+ The values are baked in at build time by the `Config codegen` build phase, so an empty `Config`
504
+ means that phase either did not run or did not find an env file. The library says which it
505
+ was — check the Xcode console (or `npx react-native log-ios`) for a line starting with
506
+ `[react-native-config]`:
507
+
508
+ - **"no env file was found. Looked for `<path>`"** — nothing was read. If the path is wrong,
509
+ select the intended file with `ENVFILE` (`ENVFILE=.env.staging npx react-native run-ios`); if
510
+ the path is right but the file is somewhere else, the project root is probably not where the
511
+ library expects it (common in monorepos). If the path looks correct, the build phase never ran:
512
+ re-run `pod install` and build again.
513
+ - **"the env file was read from `<path>`, and no variables were parsed out of it"** — the file
514
+ was found but yielded nothing. Check that it contains plain `KEY=value` lines.
515
+
516
+ The same paths are listed at build time. Search the Xcode build log for `Missing .env file` to
517
+ see every location that was tried, in order.
518
+
519
+ Note that `ENVFILE` naming a file that does not exist is not an error: the library falls back to
520
+ `.env`. The logged path is the file the values actually came from, which is the quickest way to
521
+ spot that fallback.
522
+
523
+ ### TypeError: Cannot read property 'getConfig' of null
524
+
525
+ The JavaScript side loaded but the native module is not registered in the build, so
526
+ `TurboModuleRegistry` returned `null`. In rough order of likelihood:
527
+
528
+ 1. **The app was not rebuilt** after the library was installed. Restarting Metro does not rebuild
529
+ native code — rebuild the app itself.
530
+ 2. **Autolinking is disabled for this library.** Look for an entry like this in
531
+ `react-native.config.js` and remove it:
532
+
533
+ ```js
534
+ dependencies: {
535
+ 'react-native-config': {
536
+ platforms: { android: null }, // <- remove
537
+ },
538
+ },
539
+ ```
540
+
541
+ 3. **The library is linked manually.** On React Native 0.60+ autolinking replaces manual linking,
542
+ and on the New Architecture a manually linked module is never registered as a TurboModule.
543
+ Remove `include ':react-native-config'` (and the accompanying `project(...)` line) from
544
+ `android/settings.gradle`, and `implementation project(':react-native-config')` from
545
+ `android/app/build.gradle`.
546
+ 4. **iOS only:** `pod install` has not been run since the library was installed.
547
+
548
+ After changing any of the above, rebuild from clean — on Android, delete `android/build` and
549
+ `android/app/build` first, since a stale build can keep the old registration.
550
+
398
551
  ### TypeError: _reactNativeConfig.default.getConstants is not a function
399
552
 
400
553
  This error stems from `.env` file being malformed. Accepted formats are listed here https://regex101.com/r/cbm5Tp/1. Common causes are:
401
554
  - Missing the .env file entirely
402
555
  - Rogue space anywhere, example: in front of env variable: ` MY_ENV='foo'`
403
556
 
557
+ ### Android build error: cannot find symbol BaseReactPackage
558
+
559
+ Starting from **react-native-config v1.6.0**, the Android implementation uses
560
+ `BaseReactPackage` instead of `ReactPackage`.
561
+
562
+ `BaseReactPackage` was introduced in **React Native 0.74**, so projects on React
563
+ Native 0.73 or older will see build errors like:
564
+
565
+ > cannot find symbol
566
+ > class BaseReactPackage
567
+
568
+ To fix this:
569
+
570
+ - Use `react-native-config` **below 1.6.0** (e.g. `1.5.10`), or
571
+ - Upgrade React Native to **0.74 or higher**
572
+
404
573
  ## Testing
405
574
 
406
575
  Since `react-native-config` contains native code, it cannot be run in a node.js environment (Jest, Mocha). [react-native-config-node](https://github.com/CureApp/react-native-config-node) provides a way to mock `react-native-config` for use in test runners - exactly as it is used in the actual app.
@@ -7,12 +7,18 @@ import com.facebook.react.bridge.WritableMap;
7
7
  import java.lang.ClassNotFoundException;
8
8
  import java.lang.IllegalAccessException;
9
9
  import java.lang.reflect.Field;
10
+ import java.util.ArrayList;
11
+ import java.util.LinkedHashSet;
12
+ import java.util.List;
10
13
  import java.util.Map;
11
14
  import java.util.HashMap;
15
+ import java.util.Set;
12
16
 
13
17
  public class RNCConfigModuleImpl {
14
18
  public static final String NAME = "RNCConfigModule";
15
19
 
20
+ private static final String TAG = "ReactNative";
21
+
16
22
  private ReactApplicationContext context;
17
23
 
18
24
  public RNCConfigModuleImpl(ReactApplicationContext context) {
@@ -32,31 +38,78 @@ public class RNCConfigModuleImpl {
32
38
  // because of type safety on the new arch
33
39
  final Map<String, Object> realConstants = new HashMap<>();
34
40
 
35
- try {
36
- int resId = this.context.getResources().getIdentifier("build_config_package", "string", context.getPackageName());
37
- String className;
41
+ final List<String> candidates = buildConfigPackageCandidates();
42
+ Class<?> clazz = null;
43
+
44
+ for (String candidate : candidates) {
38
45
  try {
39
- className = this.context.getString(resId);
40
- } catch (Resources.NotFoundException e) {
41
- className = this.context.getApplicationContext().getPackageName();
46
+ clazz = Class.forName(candidate + ".BuildConfig");
47
+ break;
48
+ }
49
+ catch (ClassNotFoundException e) {
50
+ // Not here - try the next candidate.
42
51
  }
43
- Class clazz = Class.forName(className + ".BuildConfig");
52
+ }
53
+
54
+ if (clazz == null) {
55
+ Log.w(TAG, "ReactConfig: Could not find BuildConfig class. Tried: " + candidates + ". "
56
+ + "If BuildConfig is generated somewhere else, point the library at it with "
57
+ + "`resValue \"string\", \"build_config_package\", \"<your namespace>\"` in android/app/build.gradle.");
58
+ }
59
+ else {
44
60
  Field[] fields = clazz.getDeclaredFields();
45
61
  for(Field f: fields) {
46
62
  try {
47
63
  realConstants.put(f.getName(), f.get(null));
48
64
  }
49
65
  catch (IllegalAccessException e) {
50
- Log.d("ReactNative", "ReactConfig: Could not access BuildConfig field " + f.getName());
66
+ Log.d(TAG, "ReactConfig: Could not access BuildConfig field " + f.getName());
51
67
  }
52
68
  }
53
69
  }
54
- catch (ClassNotFoundException e) {
55
- Log.d("ReactNative", "ReactConfig: Could not find BuildConfig class");
56
- }
57
70
 
58
71
  ret.put("config", realConstants);
59
72
 
60
73
  return MapConverter.convertMapToWritableMap(ret);
61
74
  }
75
+
76
+ /**
77
+ * Packages that may hold the app's generated BuildConfig, most-specific first.
78
+ *
79
+ * BuildConfig is generated in the module's `namespace`, which is not necessarily the
80
+ * applicationId: `applicationIdSuffix` and per-flavor `applicationId` change the latter and
81
+ * leave the former alone. Resolving only via getPackageName() therefore misses BuildConfig on
82
+ * any such variant, and the config silently arrives in JS as {}.
83
+ */
84
+ private List<String> buildConfigPackageCandidates() {
85
+ // LinkedHashSet keeps the priority order below while dropping duplicates, which is the
86
+ // common case: for most apps all three candidates are the same string.
87
+ final Set<String> candidates = new LinkedHashSet<>();
88
+
89
+ // 1. An explicit `resValue "string", "build_config_package", "..."`. Honoured first so that
90
+ // existing setups relying on it keep working, and so it stays an escape hatch when the
91
+ // derived candidates below are wrong.
92
+ int resId = this.context.getResources().getIdentifier(
93
+ "build_config_package", "string", this.context.getPackageName());
94
+ if (resId != 0) {
95
+ try {
96
+ candidates.add(this.context.getString(resId));
97
+ }
98
+ catch (Resources.NotFoundException e) {
99
+ // Declared but unreadable - fall through to the derived candidates.
100
+ }
101
+ }
102
+
103
+ // 2. The package declaring the Application class. That class is part of the app module, so
104
+ // its package is the `namespace` - unaffected by applicationId/applicationIdSuffix.
105
+ Package applicationPackage = this.context.getApplicationContext().getClass().getPackage();
106
+ if (applicationPackage != null) {
107
+ candidates.add(applicationPackage.getName());
108
+ }
109
+
110
+ // 3. The applicationId. Correct whenever it matches the namespace, which is the default.
111
+ candidates.add(this.context.getApplicationContext().getPackageName());
112
+
113
+ return new ArrayList<>(candidates);
114
+ }
62
115
  }
package/index.js CHANGED
@@ -1,5 +1,27 @@
1
1
  "use strict";
2
2
 
3
- export const Config =
4
- require("./codegen/NativeConfigModule").default.getConfig().config;
3
+ const NativeConfigModule = require("./codegen/NativeConfigModule").default;
4
+
5
+ if (NativeConfigModule == null) {
6
+ // TurboModuleRegistry.get returns null when the native module is not registered in the app.
7
+ // Left alone, the next line fails with "Cannot read property 'getConfig' of null", which says
8
+ // nothing about the cause - so spell out the ones that actually produce this.
9
+ throw new Error(
10
+ "react-native-config: the native module RNCConfigModule was not found.\n\n" +
11
+ "JavaScript loaded, but the native side is not registered in this build. Common causes:\n\n" +
12
+ " 1. The app was not rebuilt after installing the library. Restarting Metro is not enough -\n" +
13
+ " rebuild the native app.\n" +
14
+ " 2. Autolinking is disabled for this library. Check react-native.config.js for a\n" +
15
+ " `dependencies` entry setting `platforms.android` or `platforms.ios` to null, and\n" +
16
+ " remove it.\n" +
17
+ " 3. The library is linked manually. React Native 0.60+ autolinks it, and on the New\n" +
18
+ " Architecture a manually linked module is not registered as a TurboModule. Remove\n" +
19
+ " `include ':react-native-config'` from android/settings.gradle and the matching\n" +
20
+ " `implementation project(':react-native-config')` from android/app/build.gradle.\n" +
21
+ " 4. iOS only: `pod install` has not run since the library was installed.\n\n" +
22
+ "See https://github.com/react-native-config/react-native-config#troubleshooting"
23
+ );
24
+ }
25
+
26
+ export const Config = NativeConfigModule.getConfig().config;
5
27
  export default Config;
@@ -12,13 +12,23 @@ puts "reading env file from #{envs_root} and writing .m to #{m_output_path}"
12
12
  Encoding.default_external = Encoding::UTF_8
13
13
  Encoding.default_internal = Encoding::UTF_8
14
14
 
15
- dotenv, custom_env = read_dot_env(envs_root)
15
+ dotenv, custom_env, resolution = read_dot_env(envs_root)
16
16
  puts "read dotenv #{dotenv}"
17
17
 
18
18
  # create obj file that sets DOT_ENV as a NSDictionary
19
19
  dotenv_objc = dotenv.map { |k, v| %(@"#{k}":@"#{v.chomp}") }.join(',')
20
+
21
+ # Carry the outcome of the lookup into the binary alongside the values themselves. An empty
22
+ # DOT_ENV is otherwise indistinguishable at runtime from an env file that was never found, and
23
+ # the latter is by far the more common cause - see RNCConfig.m.
24
+ env_found = resolution && resolution[:found] ? 1 : 0
25
+ env_path = (resolution && (resolution[:path] || resolution[:tried].first)).to_s
26
+ env_path_objc = env_path.gsub('\\', '\\\\\\\\').gsub('"', '\"')
27
+
20
28
  template = <<EOF
21
29
  #define DOT_ENV @{ #{dotenv_objc} };
30
+ #define RNC_DOT_ENV_FOUND #{env_found}
31
+ #define RNC_DOT_ENV_PATH @"#{env_path_objc}"
22
32
  EOF
23
33
 
24
34
  # write it so that RNCConfig.m can return it
@@ -1 +1,3 @@
1
1
  #define DOT_ENV @{ };
2
+ #define RNC_DOT_ENV_FOUND 0
3
+ #define RNC_DOT_ENV_PATH @"(none - the Config codegen build phase has not run)"
@@ -1,10 +1,22 @@
1
1
  #import "RNCConfig.h"
2
- #import "GeneratedDotEnv.m" // written during build by BuildDotenvConfig.ruby
2
+ #import "GeneratedDotEnv.m" // written during build by BuildDotenvConfig.rb
3
+
4
+ // A GeneratedDotEnv.m produced by an older version of the library defines DOT_ENV alone. That
5
+ // copy lives in node_modules and is only replaced when the codegen build phase runs, which is
6
+ // precisely the case this file reports on - so fall back rather than failing to compile.
7
+ #ifndef RNC_DOT_ENV_FOUND
8
+ #define RNC_DOT_ENV_FOUND 0
9
+ #endif
10
+ #ifndef RNC_DOT_ENV_PATH
11
+ #define RNC_DOT_ENV_PATH @"(unknown - GeneratedDotEnv.m predates this diagnostic)"
12
+ #endif
3
13
 
4
14
  @implementation RNCConfig
5
15
 
6
16
  + (NSDictionary *)env {
7
- return (NSDictionary *)DOT_ENV;
17
+ NSDictionary *env = (NSDictionary *)DOT_ENV;
18
+ [self warnOnceIfEmpty:env];
19
+ return env;
8
20
  }
9
21
 
10
22
  + (NSString *)envFor: (NSString *)key {
@@ -12,4 +24,33 @@
12
24
  return value;
13
25
  }
14
26
 
27
+ // An empty config reaches JS as `{}` with nothing else to go on, and is the most reported
28
+ // symptom against this library. Say which file was looked for, and whether it was found, at the
29
+ // point the emptiness is first observed.
30
+ //
31
+ // NSLog rather than RCTLog: this file is also compiled into the `Extension` subspec, which has no
32
+ // React dependency.
33
+ + (void)warnOnceIfEmpty:(NSDictionary *)env {
34
+ if (env.count > 0) {
35
+ return;
36
+ }
37
+
38
+ static dispatch_once_t onceToken;
39
+ dispatch_once(&onceToken, ^{
40
+ if (RNC_DOT_ENV_FOUND) {
41
+ NSLog(@"[react-native-config] Config is empty. The env file was read from %@, and no "
42
+ @"variables were parsed out of it - check that it contains KEY=value lines.",
43
+ RNC_DOT_ENV_PATH);
44
+ } else {
45
+ NSLog(@"[react-native-config] Config is empty: no env file was found. Looked for %@.\n"
46
+ @" - If the path is wrong, set ENVFILE (e.g. `ENVFILE=.env.staging npx react-native run-ios`),\n"
47
+ @" or point the library at the right root if this project is in a monorepo.\n"
48
+ @" - If the path is right, the 'Config codegen' build phase did not run: re-run\n"
49
+ @" `pod install`, then build again.\n"
50
+ @" See https://github.com/react-native-config/react-native-config#troubleshooting",
51
+ RNC_DOT_ENV_PATH);
52
+ }
53
+ });
54
+ }
55
+
15
56
  @end
@@ -6,38 +6,100 @@
6
6
  Encoding.default_external = Encoding::UTF_8
7
7
  Encoding.default_internal = Encoding::UTF_8
8
8
 
9
+ # Expands build settings referenced in an env file name, so that a single setting can serve every
10
+ # configuration: ENVFILE=.env.$(CONFIGURATION) becomes .env.Release-Staging.
11
+ #
12
+ # Xcode expands $(FOO) itself when the value is a build setting, so this covers the cases where it
13
+ # does not - an ENVFILE exported from the shell, or a value passed through untouched. A reference
14
+ # to something unset expands to nothing and is reported, because being told that ".env." is
15
+ # missing explains very little on its own.
16
+ def expand_build_settings(value)
17
+ unset = []
18
+ expanded = value.gsub(/\$[({]([A-Za-z_][A-Za-z0-9_]*)[)}]|\$([A-Za-z_][A-Za-z0-9_]*)/) do
19
+ name = Regexp.last_match(1) || Regexp.last_match(2)
20
+ replacement = ENV[name]
21
+ unset << name if replacement.nil? || replacement.empty?
22
+ replacement.to_s
23
+ end
24
+ [expanded, unset]
25
+ end
26
+
27
+ # Which env file was asked for, and by whom. Kept separate from finding it so that a fallback can
28
+ # be reported against what was actually requested.
29
+ def select_env_file(default_env_file)
30
+ if File.exist?('/tmp/envfile')
31
+ return { name: File.read('/tmp/envfile').strip, source: :tmp_envfile, unset: [], custom: true }
32
+ end
33
+
34
+ requested = ENV['ENVFILE']
35
+ if requested.nil? || requested.empty?
36
+ return { name: default_env_file, source: :default, unset: [], custom: false }
37
+ end
38
+
39
+ expanded, unset = expand_build_settings(requested)
40
+ { name: expanded, source: :envfile, unset: unset, custom: false, requested: requested }
41
+ end
42
+
9
43
  # TODO: introduce a parameter which controls how to build relative path
10
44
  def read_dot_env(envs_root)
11
45
  defaultEnvFile = '.env'
12
46
  puts "going to read env file from root folder #{envs_root}"
13
47
 
14
- # pick a custom env file if set
15
- if File.exist?('/tmp/envfile')
16
- custom_env = true
17
- file = File.read('/tmp/envfile').strip
18
- else
19
- custom_env = false
20
- file = ENV['ENVFILE'] || defaultEnvFile
48
+ selection = select_env_file(defaultEnvFile)
49
+ file = selection[:name]
50
+ custom_env = selection[:custom]
51
+
52
+ if selection[:source] == :envfile
53
+ puts "ENVFILE=#{selection[:requested]}"
54
+ puts " expands to #{file}" if selection[:requested] != file
55
+ unless selection[:unset].empty?
56
+ puts " note: #{selection[:unset].uniq.join(', ')} " \
57
+ 'resolved to nothing. Outside Xcode these build settings are not set - pass the file ' \
58
+ 'name directly, or run the build through Xcode.'
59
+ end
21
60
  end
22
61
 
62
+ # Every path considered, in order. A miss here is not a build failure - the app compiles and
63
+ # receives an empty config - so the paths are recorded to be named in the message below and
64
+ # handed to the runtime, rather than leaving "it is empty" as the only available symptom.
65
+ tried = []
66
+ resolved_path = nil
67
+ requested_paths = []
68
+
23
69
  dotenv = begin
24
70
  # https://regex101.com/r/cbm5Tp/1
25
71
  dotenv_pattern = /^(?:export\s+|)(?<key>[[:alnum:]_]+)\s*=\s*((?<quote>["'])?(?<val>.*?[^\\])\k<quote>?|)$/
26
72
 
27
- path = File.expand_path(File.join(envs_root, file.to_s))
28
- if File.exist?(path)
29
- raw = File.read(path)
30
- elsif File.exist?(file)
31
- raw = File.read(file)
32
- else
33
- defaultEnvPath = File.expand_path(File.join(envs_root, "#{defaultEnvFile}"))
34
- unless File.exist?(defaultEnvPath)
35
- # try as absolute path
36
- defaultEnvPath = defaultEnvFile
37
- end
38
- raw = File.read(defaultEnvPath)
73
+ # The paths that satisfy what was asked for. Anything found beyond these is a fallback.
74
+ requested_paths = [
75
+ File.expand_path(File.join(envs_root, file.to_s)),
76
+ file.to_s
77
+ ]
78
+ candidates = requested_paths + [File.expand_path(File.join(envs_root, defaultEnvFile.to_s))]
79
+ # Last resort, preserving the previous behaviour: treat the default name as a path of its own.
80
+ candidates << defaultEnvFile unless File.exist?(candidates[2])
81
+
82
+ tried = candidates.uniq
83
+ resolved_path = tried.find { |candidate| File.exist?(candidate) }
84
+ raise Errno::ENOENT, tried.last if resolved_path.nil?
85
+
86
+ # Falling back is not an error - it is long-standing behaviour and some setups rely on it for
87
+ # a gitignored .env.local - but it is silent, and a build that quietly ships the wrong
88
+ # environment is worse than one that fails. Say so where the person configuring it will look.
89
+ if selection[:source] == :envfile && !requested_paths.include?(resolved_path)
90
+ puts('**********************************************')
91
+ puts('*** ENVFILE was set, but that file is missing ')
92
+ puts('**********************************************')
93
+ puts("Asked for: #{file}")
94
+ puts('Not found at:')
95
+ requested_paths.each { |candidate| puts(" - #{candidate}") }
96
+ puts("Falling back to: #{resolved_path}")
97
+ puts('The build will succeed using those values. If that is not what you want, correct')
98
+ puts('ENVFILE or add the missing file.')
39
99
  end
40
100
 
101
+ raw = File.read(resolved_path)
102
+
41
103
  raw.split("\n").inject({}) do |h, line|
42
104
  m = line.match(dotenv_pattern)
43
105
 
@@ -57,7 +119,16 @@ def read_dot_env(envs_root)
57
119
  puts('**************************')
58
120
  puts('*** Missing .env file ****')
59
121
  puts('**************************')
60
- return [{}, false] # set dotenv as an empty hash
122
+ puts('Tried, in order:')
123
+ tried.each { |candidate| puts(" - #{candidate}") }
124
+ puts('The build will succeed and Config will be empty at runtime.')
125
+ # set dotenv as an empty hash
126
+ return [{}, false, { found: false, path: nil, tried: tried, source: selection[:source],
127
+ requested: file, fell_back: false }]
61
128
  end
62
- [dotenv, custom_env]
129
+
130
+ [dotenv, custom_env, { found: true, path: resolved_path, tried: tried,
131
+ source: selection[:source], requested: file,
132
+ fell_back: selection[:source] == :envfile &&
133
+ !requested_paths.include?(resolved_path) }]
63
134
  end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-config",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "Expose config variables to React Native apps",
5
5
  "keywords": [
6
6
  "env",
@@ -12,14 +12,14 @@
12
12
  "windows",
13
13
  "12factor"
14
14
  ],
15
- "homepage": "https://github.com/luggit/react-native-config",
15
+ "homepage": "https://github.com/react-native-config/react-native-config",
16
16
  "contributors": [
17
17
  "Luan Curti <luancurti@gmail.com> (https://github.com/luancurti)",
18
18
  "Amer Lotfi Orimi <amerllica@gmail.com> (https://github.com/amerllica)"
19
19
  ],
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.com/luggit/react-native-config"
22
+ "url": "git+https://github.com/react-native-config/react-native-config.git"
23
23
  },
24
24
  "private": false,
25
25
  "author": "Pedro Belo",
@@ -35,6 +35,9 @@
35
35
  ],
36
36
  "types": "./index.d.ts",
37
37
  "license": "MIT",
38
+ "scripts": {
39
+ "test": "ruby test/run.rb"
40
+ },
38
41
  "devDependencies": {
39
42
  "@react-native-community/cli": "^20.0.2",
40
43
  "@semantic-release/git": "^10.0.1",
@@ -62,7 +65,7 @@
62
65
  },
63
66
  "ios": {
64
67
  "modules": {
65
- "RNCConfig": {
68
+ "RNCConfigModule": {
66
69
  "className": "RNCConfigModule",
67
70
  "unstableRequiresMainQueueSetup": true
68
71
  }