Skip to content

Commit 30f26eb

Browse files
authored
Merge pull request #600 from dotenvx/cli-for-react-native
add cli support
2 parents 01c62f7 + 8f1cc49 commit 30f26eb

5 files changed

Lines changed: 150 additions & 22 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
All notable changes to this project are documented in this file.
44

5-
## [Unreleased](https://github.com/dotenvx/react-native-dotenv/compare/v4.1.1...main)
5+
## [Unreleased](https://github.com/dotenvx/react-native-dotenv/compare/v5.0.0...main)
6+
7+
### Fixed
8+
9+
- Update vulnerable `brace-expansion` and `js-yaml` dependencies to patched versions and raise the `brace-expansion` override minimum to 5.0.9.
10+
11+
## [5.0.0](https://github.com/dotenvx/react-native-dotenv/compare/v4.1.1...v5.0.0) (2026-09-21)
12+
13+
### Added
14+
15+
- Add support for `dotenv run` to share environment variables between build tooling and `@env` imports, including file precedence, safe mode, and Metro restart requirements.
616

717
## [4.1.1](https://github.com/dotenvx/react-native-dotenv/compare/v4.1.0...v4.1.1) (2026-07-28)
818

‎README.md‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,51 @@ That's it. Your environment variables from `.env` are available via `@env`!
5656

5757
## Advanced
5858

59+
<details><summary>with the dotenv CLI</summary><br>
60+
61+
Use dotenv v18's `dotenv run` command when you want the same environment variables available to build tooling and your app's `@env` imports. The Babel plugin still inlines values into the app at build time.
62+
63+
Install dotenv directly so your package manager makes its CLI available to your project scripts:
64+
65+
```sh
66+
npm install --save-dev dotenv@^18.0.1
67+
```
68+
69+
Select a file when starting Metro:
70+
71+
```json
72+
{
73+
"scripts": {
74+
"start:staging": "dotenv run -f .env.staging -- react-native start --reset-cache"
75+
}
76+
}
77+
```
78+
79+
```ini
80+
# .env.staging
81+
API_URL=https://staging.example.org
82+
```
83+
84+
Keep the Babel plugin configured as shown in Usage, then import normally:
85+
86+
```js
87+
import { API_URL } from '@env'
88+
89+
fetch(`${API_URL}/users`)
90+
```
91+
92+
The CLI loads the selected file into the process environment before Metro starts. Existing shell/CI values win unless you pass `--override` to `dotenv run`. The plugin then gives non-empty process environment values priority over its own `.env` files.
93+
94+
The plugin still loads its usual files; `-f` selects the CLI's file, not the plugin's `path` or `APP_ENV`. This can change precedence: plain `dotenv run` loads `.env` into the process environment, so those values win over the plugin's `.env.local` values. Use the CLI when you intend its injected values to take priority.
95+
96+
With the default `safe: false`, keys loaded only by the CLI work through `@env` imports. With `safe: true`, those keys must also appear in files the plugin reads. Likewise, `process.env.X` is only inlined for keys in the plugin's files (plus `NODE_ENV`, `BABEL_ENV`, and `envName`).
97+
98+
Stop Metro and rerun the script after changing CLI-loaded values; its process environment is set at startup. The script resets Metro's cache when restarting.
99+
100+
The CLI is optional. For values used only by app code, the Babel plugin can continue loading `.env` files on its own.
101+
102+
</details>
103+
59104
<details><summary>with Expo 🧭</summary><br>
60105

61106
```js

‎package-lock.json‎

Lines changed: 21 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-dotenv",
3-
"version": "4.1.1",
3+
"version": "5.0.0",
44
"description": "Load .env into React Native with import statements. A Babel plugin that inlines environment variables at build time.",
55
"repository": {
66
"type": "git",
@@ -26,15 +26,15 @@
2626
"12factor"
2727
],
2828
"dependencies": {
29-
"dotenv": "^17.4.2"
29+
"dotenv": "^18.0.1"
3030
},
3131
"devDependencies": {
3232
"@babel/core": "^7.29.7",
3333
"jest": "30.4.2",
3434
"standard": "^17.1.2"
3535
},
3636
"overrides": {
37-
"brace-expansion": "^5.0.8",
37+
"brace-expansion": "^5.0.9",
3838
"minimatch": "^10.2.5"
3939
},
4040
"author": "@motdotla",

‎tests/cli.test.js‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
const { execFileSync } = require('child_process')
2+
const fs = require('fs')
3+
const os = require('os')
4+
const path = require('path')
5+
6+
describe('dotenv run integration', () => {
7+
let directory
8+
let env
9+
const dotenvPackagePath = require.resolve('dotenv/package.json')
10+
const cli = path.resolve(path.dirname(dotenvPackagePath), require(dotenvPackagePath).bin.dotenv)
11+
12+
beforeEach(() => {
13+
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-dotenv-cli-'))
14+
env = { ...process.env }
15+
for (const key of Object.keys(env)) {
16+
if (/^(DOTENV_|RN_DOTENV_CLI_)/.test(key) || ['NODE_ENV', 'BABEL_ENV', 'APP_ENV'].includes(key)) {
17+
delete env[key]
18+
}
19+
}
20+
fs.writeFileSync(path.join(directory, '.env'), 'RN_DOTENV_CLI_URL=base\n')
21+
fs.writeFileSync(path.join(directory, '.env.local'), 'RN_DOTENV_CLI_URL=local\n')
22+
fs.writeFileSync(path.join(directory, '.env.staging'), 'RN_DOTENV_CLI_URL=staging\nRN_DOTENV_CLI_ONLY=extra\n')
23+
})
24+
25+
afterEach(() => {
26+
fs.rmSync(directory, { recursive: true, force: true })
27+
})
28+
29+
function transform (args, source, options = {}) {
30+
const script = `
31+
const { transformSync } = require(${JSON.stringify(require.resolve('@babel/core'))})
32+
const result = transformSync(${JSON.stringify(source)}, {
33+
configFile: false,
34+
babelrc: false,
35+
plugins: [[${JSON.stringify(require.resolve('../index.js'))}, ${JSON.stringify({ quiet: true, ...options })}]]
36+
})
37+
console.log(result.code)
38+
`
39+
return execFileSync(process.execPath, [cli, 'run', '-q', ...args, '--', process.execPath, '-e', script], {
40+
cwd: directory,
41+
env,
42+
encoding: 'utf8'
43+
}).trim()
44+
}
45+
46+
it('inlines CLI values through imports while leaving CLI-only process.env references intact', () => {
47+
expect(transform(['-f', '.env.staging'],
48+
'import { RN_DOTENV_CLI_URL, RN_DOTENV_CLI_ONLY } from "@env"; console.log(RN_DOTENV_CLI_URL, RN_DOTENV_CLI_ONLY, process.env.RN_DOTENV_CLI_URL, process.env.RN_DOTENV_CLI_ONLY)'
49+
)).toBe('console.log("staging", "extra", "staging", process.env.RN_DOTENV_CLI_ONLY);')
50+
})
51+
52+
it('preserves safe mode restrictions on CLI-only imports', () => {
53+
expect(transform(['-f', '.env.staging'],
54+
'import { RN_DOTENV_CLI_URL, RN_DOTENV_CLI_ONLY } from "@env"; console.log(RN_DOTENV_CLI_URL, RN_DOTENV_CLI_ONLY)',
55+
{ safe: true }
56+
)).toBe('console.log("staging", undefined);')
57+
})
58+
59+
it('gives shell values priority unless the CLI uses --override', () => {
60+
env.RN_DOTENV_CLI_URL = 'shell'
61+
const source = 'import { RN_DOTENV_CLI_URL } from "@env"; console.log(RN_DOTENV_CLI_URL)'
62+
expect(transform(['-f', '.env.staging'], source)).toBe('console.log("shell");')
63+
expect(transform(['--override', '-f', '.env.staging'], source)).toBe('console.log("staging");')
64+
})
65+
66+
it('gives CLI-loaded .env values priority over plugin-loaded .env.local values', () => {
67+
expect(transform([], 'import { RN_DOTENV_CLI_URL } from "@env"; console.log(RN_DOTENV_CLI_URL)'))
68+
.toBe('console.log("base");')
69+
})
70+
})

0 commit comments

Comments
 (0)