Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

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

## [Unreleased](https://github.com/dotenvx/react-native-dotenv/compare/v4.1.1...main)
## [Unreleased](https://github.com/dotenvx/react-native-dotenv/compare/v5.0.0...main)

### Fixed

- Update vulnerable `brace-expansion` and `js-yaml` dependencies to patched versions and raise the `brace-expansion` override minimum to 5.0.9.

## [5.0.0](https://github.com/dotenvx/react-native-dotenv/compare/v4.1.1...v5.0.0) (2026-09-21)

### Added

- Add support for `dotenv run` to share environment variables between build tooling and `@env` imports, including file precedence, safe mode, and Metro restart requirements.

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

Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,51 @@ That's it. Your environment variables from `.env` are available via `@env`!

## Advanced

<details><summary>with the dotenv CLI</summary><br>

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.

Install dotenv directly so your package manager makes its CLI available to your project scripts:

```sh
npm install --save-dev dotenv@^18.0.1
```

Select a file when starting Metro:

```json
{
"scripts": {
"start:staging": "dotenv run -f .env.staging -- react-native start --reset-cache"
}
}
```

```ini
# .env.staging
API_URL=https://staging.example.org
```

Keep the Babel plugin configured as shown in Usage, then import normally:

```js
import { API_URL } from '@env'

fetch(`${API_URL}/users`)
```

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.

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.

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`).

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.

The CLI is optional. For values used only by app code, the Babel plugin can continue loading `.env` files on its own.

</details>

<details><summary>with Expo 🧭</summary><br>

```js
Expand Down
39 changes: 21 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-dotenv",
"version": "4.1.1",
"version": "5.0.0",
"description": "Load .env into React Native with import statements. A Babel plugin that inlines environment variables at build time.",
"repository": {
"type": "git",
Expand All @@ -26,15 +26,15 @@
"12factor"
],
"dependencies": {
"dotenv": "^17.4.2"
"dotenv": "^18.0.1"
},
"devDependencies": {
"@babel/core": "^7.29.7",
"jest": "30.4.2",
"standard": "^17.1.2"
},
"overrides": {
"brace-expansion": "^5.0.8",
"brace-expansion": "^5.0.9",
"minimatch": "^10.2.5"
},
"author": "@motdotla",
Expand Down
70 changes: 70 additions & 0 deletions tests/cli.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const { execFileSync } = require('child_process')
const fs = require('fs')
const os = require('os')
const path = require('path')

describe('dotenv run integration', () => {
let directory
let env
const dotenvPackagePath = require.resolve('dotenv/package.json')
const cli = path.resolve(path.dirname(dotenvPackagePath), require(dotenvPackagePath).bin.dotenv)

beforeEach(() => {
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-dotenv-cli-'))
env = { ...process.env }
for (const key of Object.keys(env)) {
if (/^(DOTENV_|RN_DOTENV_CLI_)/.test(key) || ['NODE_ENV', 'BABEL_ENV', 'APP_ENV'].includes(key)) {
delete env[key]
}
}
fs.writeFileSync(path.join(directory, '.env'), 'RN_DOTENV_CLI_URL=base\n')
fs.writeFileSync(path.join(directory, '.env.local'), 'RN_DOTENV_CLI_URL=local\n')
fs.writeFileSync(path.join(directory, '.env.staging'), 'RN_DOTENV_CLI_URL=staging\nRN_DOTENV_CLI_ONLY=extra\n')
})

afterEach(() => {
fs.rmSync(directory, { recursive: true, force: true })
})

function transform (args, source, options = {}) {
const script = `
const { transformSync } = require(${JSON.stringify(require.resolve('@babel/core'))})
const result = transformSync(${JSON.stringify(source)}, {
configFile: false,
babelrc: false,
plugins: [[${JSON.stringify(require.resolve('../index.js'))}, ${JSON.stringify({ quiet: true, ...options })}]]
})
console.log(result.code)
`
return execFileSync(process.execPath, [cli, 'run', '-q', ...args, '--', process.execPath, '-e', script], {
cwd: directory,
env,
encoding: 'utf8'
}).trim()
}

it('inlines CLI values through imports while leaving CLI-only process.env references intact', () => {
expect(transform(['-f', '.env.staging'],
'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)'
)).toBe('console.log("staging", "extra", "staging", process.env.RN_DOTENV_CLI_ONLY);')
})

it('preserves safe mode restrictions on CLI-only imports', () => {
expect(transform(['-f', '.env.staging'],
'import { RN_DOTENV_CLI_URL, RN_DOTENV_CLI_ONLY } from "@env"; console.log(RN_DOTENV_CLI_URL, RN_DOTENV_CLI_ONLY)',
{ safe: true }
)).toBe('console.log("staging", undefined);')
})

it('gives shell values priority unless the CLI uses --override', () => {
env.RN_DOTENV_CLI_URL = 'shell'
const source = 'import { RN_DOTENV_CLI_URL } from "@env"; console.log(RN_DOTENV_CLI_URL)'
expect(transform(['-f', '.env.staging'], source)).toBe('console.log("shell");')
expect(transform(['--override', '-f', '.env.staging'], source)).toBe('console.log("staging");')
})

it('gives CLI-loaded .env values priority over plugin-loaded .env.local values', () => {
expect(transform([], 'import { RN_DOTENV_CLI_URL } from "@env"; console.log(RN_DOTENV_CLI_URL)'))
.toBe('console.log("base");')
})
})
Loading