diff --git a/CHANGELOG.md b/CHANGELOG.md
index 08b6d16..e2e0eb1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/README.md b/README.md
index c5bdc5f..f1bdab8 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,51 @@ That's it. Your environment variables from `.env` are available via `@env`!
## Advanced
+with the dotenv CLI
+
+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.
+
+
+
with Expo ðŸ§
```js
diff --git a/package-lock.json b/package-lock.json
index d0d9c60..74eacfe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,15 @@
{
"name": "react-native-dotenv",
- "version": "4.1.1",
+ "version": "5.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "react-native-dotenv",
- "version": "4.1.1",
+ "version": "5.0.0",
"license": "MIT",
"dependencies": {
- "dotenv": "^17.4.2"
+ "dotenv": "^18.0.1"
},
"devDependencies": {
"@babel/core": "^7.29.7",
@@ -611,9 +611,9 @@
"license": "Python-2.0"
},
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
- "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
"dev": true,
"funding": [
{
@@ -2174,9 +2174,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
- "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2703,10 +2703,13 @@
}
},
"node_modules/dotenv": {
- "version": "17.4.2",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
- "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "version": "18.0.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.1.tgz",
+ "integrity": "sha512-0eR4m4D/jH5eaI3evo2ZqqMii5mrTCR12v12VChdie6O1gPHs7XAjzTep0CWc1Bd+oNx3vVfbi734dK4zghigw==",
"license": "BSD-2-Clause",
+ "bin": {
+ "dotenv": "dist/index.cjs"
+ },
"engines": {
"node": ">=12"
},
@@ -3487,9 +3490,9 @@
}
},
"node_modules/eslint/node_modules/js-yaml": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
- "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
"dev": true,
"funding": [
{
@@ -5504,9 +5507,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
- "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
+ "version": "3.15.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz",
+ "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 3d6ec9a..0ee5e39 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -26,7 +26,7 @@
"12factor"
],
"dependencies": {
- "dotenv": "^17.4.2"
+ "dotenv": "^18.0.1"
},
"devDependencies": {
"@babel/core": "^7.29.7",
@@ -34,7 +34,7 @@
"standard": "^17.1.2"
},
"overrides": {
- "brace-expansion": "^5.0.8",
+ "brace-expansion": "^5.0.9",
"minimatch": "^10.2.5"
},
"author": "@motdotla",
diff --git a/tests/cli.test.js b/tests/cli.test.js
new file mode 100644
index 0000000..bde4d68
--- /dev/null
+++ b/tests/cli.test.js
@@ -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");')
+ })
+})