react-native-config 1.6.2 → 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
 
@@ -352,44 +368,63 @@ Also note that besides requiring lowercase, the matching is done with `buildFlav
352
368
 
353
369
  #### iOS / macOS
354
370
 
355
- 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.
356
373
 
357
- Start by creating a new scheme:
374
+ ##### Per build configuration (recommended)
358
375
 
359
- - In the Xcode menu, go to Product > Scheme > Edit Scheme
360
- - Click Duplicate Scheme on the bottom
361
- - Give it a proper name on the top left. For instance: "Myapp (staging)"
362
- - 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:
363
380
 
364
- 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'
365
385
 
366
- - Expand the "Build" settings on left
367
- - Click "Pre-actions", and under the plus sign select "New Run Script Action"
368
- - Where it says "Type a script or drag a script file", type:
369
- ```
370
- cp "${PROJECT_DIR}/../.env.staging" "${PROJECT_DIR}/../.env" # replace .env.staging for your file
371
- ```
372
- 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
+ ```
373
392
 
374
- 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:
375
399
 
376
400
  ```ruby
377
401
  ENVFILES = {
378
- 'Debug' => '$(PODS_ROOT)/../../.env.debug',
379
- 'Release' => '$(PODS_ROOT)/../../.env.production',
402
+ 'Debug' => '.env.development',
403
+ 'Release' => '.env.production',
404
+ 'Debug-Staging' => '.env.staging',
405
+ 'Release-Staging' => '.env.staging',
380
406
  }
381
407
  post_install do |installer|
382
408
  installer.pods_project.targets.each do |target|
409
+ next unless target.name == 'react-native-config'
410
+
383
411
  target.build_configurations.each do |config|
384
- if target.name == 'react-native-config'
385
- config.build_settings['ENVFILE'] = ENVFILES[config.name]
386
- end
412
+ config.build_settings['ENVFILE'] = ENVFILES[config.name]
387
413
  end
388
414
  end
389
415
  end
390
416
  ```
391
417
 
392
- 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.
393
428
 
394
429
  ```diff
395
430
  target 'MyApp' do
@@ -404,15 +439,51 @@ Note that if you have flipper enabled in your Podfile, you must move the `flippe
404
439
  + flipper_post_install(installer)
405
440
 
406
441
  installer.pods_project.targets.each do |target|
442
+ next unless target.name == 'react-native-config'
443
+
407
444
  target.build_configurations.each do |config|
408
- if target.name == 'react-native-config'
409
- config.build_settings['ENVFILE'] = ENVFILES[config.name]
410
- end
445
+ config.build_settings['ENVFILE'] = '.env.$(CONFIGURATION)'
411
446
  end
412
447
  end
413
448
  end
414
449
  ```
415
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
+
416
487
  ## Troubleshooting
417
488
 
418
489
  ### Problems with Proguard
@@ -427,6 +498,28 @@ If using Dexguard, the shrinking phase will remove resources it thinks are unuse
427
498
 
428
499
  -keepresources string/build_config_package
429
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
+
430
523
  ### TypeError: Cannot read property 'getConfig' of null
431
524
 
432
525
  The JavaScript side loaded but the native module is not registered in the build, so
@@ -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.2",
3
+ "version": "1.7.0",
4
4
  "description": "Expose config variables to React Native apps",
5
5
  "keywords": [
6
6
  "env",
@@ -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",