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 +205 -36
- package/android/src/main/java/com/lugg/RNCConfig/RNCConfigModuleImpl.java +64 -11
- package/index.js +24 -2
- package/ios/ReactNativeConfig/BuildDotenvConfig.rb +11 -1
- package/ios/ReactNativeConfig/GeneratedDotEnv.m +2 -0
- package/ios/ReactNativeConfig/RNCConfig.m +43 -2
- package/ios/ReactNativeConfig/ReadDotEnv.rb +92 -21
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="brand/logo.png" alt="" width="120" height="120" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">react-native-config</h1>
|
|
4
6
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
374
|
+
##### Per build configuration (recommended)
|
|
326
375
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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' => '
|
|
347
|
-
'Release' => '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
final List<String> candidates = buildConfigPackageCandidates();
|
|
42
|
+
Class<?> clazz = null;
|
|
43
|
+
|
|
44
|
+
for (String candidate : candidates) {
|
|
38
45
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
clazz = Class.forName(candidate + ".BuildConfig");
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
catch (ClassNotFoundException e) {
|
|
50
|
+
// Not here - try the next candidate.
|
|
42
51
|
}
|
|
43
|
-
|
|
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(
|
|
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
|
-
|
|
4
|
-
|
|
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,10 +1,22 @@
|
|
|
1
1
|
#import "RNCConfig.h"
|
|
2
|
-
#import "GeneratedDotEnv.m" // written during build by BuildDotenvConfig.
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
file
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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/
|
|
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
|
-
"
|
|
68
|
+
"RNCConfigModule": {
|
|
66
69
|
"className": "RNCConfigModule",
|
|
67
70
|
"unstableRequiresMainQueueSetup": true
|
|
68
71
|
}
|