diff --git a/CHANGELOG.md b/CHANGELOG.md
index a09fb57..aff935d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,13 @@
All notable changes to this project are documented in this file.
-[Unreleased](https://github.com/dotenvx/dotenvx/compare/v4.0.0...main)
+## [Unreleased](https://github.com/dotenvx/react-native-dotenv/compare/v4.0.0...main)
-## [4.0.0](https://github.com/dotenvx/dotenvx/compare/v3.4.12...v4.0.0) (2026-07-28)
+### Added
+
+- Log `◇ injected env (N) from .env` to stderr on transform (same style as dotenv). Set `quiet: true` to suppress.
+
+## [4.0.0](https://github.com/dotenvx/react-native-dotenv/compare/v3.4.12...v4.0.0) (2026-07-28)
### Fixed
diff --git a/README.md b/README.md
index fe72e31..1e79b47 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,14 @@ fetch(`${API_URL}/users`)
That's it. Your environment variables from `.env` are available via `@env`.
+On transform you'll see a message on stderr (same style as dotenv):
+
+```text
+◇ injected env (2) from .env
+```
+
+Set `quiet: true` in the plugin options to suppress it.
+
## Advanced
@@ -204,7 +212,7 @@ When set to `false`, an error will be thrown.
verbose (default: false)
-Print the active dotenv mode while transforming.
+Also print the active dotenv mode to stderr while transforming.
```json
{
@@ -216,6 +224,21 @@ Print the active dotenv mode while transforming.
}
```
+
+quiet (default: false)
+
+Suppress the stderr inject message (`◇ injected env (N) from .env`).
+
+```json
+{
+ "plugins": [
+ ["module:react-native-dotenv", {
+ "quiet": true
+ }]
+ ]
+}
+```
+
process.env
@@ -233,9 +256,37 @@ For host/CI-only values, use `@env` imports — or put the key in `.env` and let
Expo
-Expo now has [built-in environment variable support](https://docs.expo.dev/guides/environment-variables/). Evaluate if you still need this plugin.
+Expo has [built-in environment variable support](https://docs.expo.dev/guides/environment-variables/). Use this plugin when you want `@env` imports or multi-env files (e.g. `.env.staging` via `APP_ENV`).
+
+```js
+// babel.config.js
+module.exports = function (api) {
+ api.cache(false)
+ return {
+ presets: ['babel-preset-expo'],
+ plugins: [
+ ['module:react-native-dotenv']
+ ]
+ }
+}
+```
+
+```ini
+# .env
+HELLO="Universe"
+```
+
+```js
+// app/index.tsx
+import { HELLO } from '@env'
+import { Text } from 'react-native'
+
+export default function HomeScreen() {
+ return Hello {HELLO}
+}
+```
-Preview [the expo test app](https://github.com/goatandsheep/react-native-dotenv-expo-test).
+Then start with a clean Metro cache: `npx expo start --clear`.
Multi-env
diff --git a/index.js b/index.js
index 59aa60c..953c0c4 100644
--- a/index.js
+++ b/index.js
@@ -3,20 +3,26 @@ const path = require('path')
const dotenv = require('dotenv')
function parseDotenvFile (filepath, verbose = false) {
- let content
-
try {
- content = fs.readFileSync(filepath)
+ const content = fs.readFileSync(filepath)
+ return { parsed: dotenv.parse(content), exists: true }
} catch (error) {
// The env file does not exist.
if (verbose) {
console.error('react-native-dotenv', error)
}
- return {}
+ return { parsed: {}, exists: false }
}
+}
- return dotenv.parse(content) //
+function logInjectedEnv (fileEnv, loadedPaths) {
+ const keysCount = Object.keys(fileEnv).length
+ if (loadedPaths.length > 0) {
+ console.error(`◇ injected env (${keysCount}) from ${loadedPaths.join(', ')}`)
+ } else {
+ console.error(`◇ injected env (${keysCount})`)
+ }
}
function undefObjectAssign (targetObject, sourceObject) {
@@ -67,6 +73,7 @@ module.exports = (api, options) => {
safe: false,
allowUndefined: true,
verbose: false,
+ quiet: false,
...options
}
const babelMode = process.env[options.envName] || (process.env.BABEL_ENV && process.env.BABEL_ENV !== 'undefined' && process.env.BABEL_ENV !== 'development' && process.env.BABEL_ENV) || process.env.NODE_ENV || 'development'
@@ -75,7 +82,7 @@ module.exports = (api, options) => {
const modeLocalFilePath = options.path + '.' + babelMode + '.local'
if (options.verbose) {
- console.log('dotenvMode', babelMode)
+ console.error(`◇ dotenvMode ${babelMode}`)
if (process.env[options.envName] === 'production' || process.env[options.envName] === 'development') {
console.error('APP_ENV error', 'cannot use APP_ENV=development or APP_ENV=production')
}
@@ -87,12 +94,18 @@ module.exports = (api, options) => {
api.cache.using(() => mtime(modeLocalFilePath))
const dotenvTemporary = undefObjectAssign({}, process.env)
- const parsed = parseDotenvFile(options.path, options.verbose)
- const localParsed = parseDotenvFile(localFilePath, options.verbose)
- const modeParsed = parseDotenvFile(modeFilePath, options.verbose)
- const modeLocalParsed = parseDotenvFile(modeLocalFilePath, options.verbose)
+ const parsedFile = parseDotenvFile(options.path, options.verbose)
+ const localFile = parseDotenvFile(localFilePath, options.verbose)
+ const modeFile = parseDotenvFile(modeFilePath, options.verbose)
+ const modeLocalFile = parseDotenvFile(modeLocalFilePath, options.verbose)
const modeExceptions = ['NODE_ENV', 'BABEL_ENV', options.envName]
- const fileEnv = undefObjectAssign(undefObjectAssign(undefObjectAssign(parsed, modeParsed), localParsed), modeLocalParsed)
+ const fileEnv = undefObjectAssign(
+ undefObjectAssign(
+ undefObjectAssign(parsedFile.parsed, modeFile.parsed),
+ localFile.parsed
+ ),
+ modeLocalFile.parsed
+ )
// process.env.X is only inlined for keys from .env files (+ mode exceptions).
// That stops build-tooling pollution (e.g. Metro jest-worker) without hardcoding
@@ -106,10 +119,24 @@ module.exports = (api, options) => {
? safeObjectAssign(undefObjectAssign({}, fileEnv), dotenvTemporary, modeExceptions)
: undefObjectAssign(undefObjectAssign({}, fileEnv), dotenvTemporary)
- api.addExternalDependency(path.resolve(options.path))
- api.addExternalDependency(path.resolve(modeFilePath))
- api.addExternalDependency(path.resolve(localFilePath))
- api.addExternalDependency(path.resolve(modeLocalFilePath))
+ if (options.verbose || !options.quiet) {
+ const loadedPaths = [
+ [options.path, parsedFile.exists],
+ [modeFilePath, modeFile.exists],
+ [localFilePath, localFile.exists],
+ [modeLocalFilePath, modeLocalFile.exists]
+ ]
+ .filter(([, exists]) => exists)
+ .map(([filepath]) => filepath)
+ logInjectedEnv(fileEnv, loadedPaths)
+ }
+
+ if (typeof api.addExternalDependency === 'function') {
+ api.addExternalDependency(path.resolve(options.path))
+ api.addExternalDependency(path.resolve(modeFilePath))
+ api.addExternalDependency(path.resolve(localFilePath))
+ api.addExternalDependency(path.resolve(modeLocalFilePath))
+ }
return ({
name: 'dotenv-import',
diff --git a/tests/index.test.js b/tests/index.test.js
index 1aba91c..55f75c6 100644
--- a/tests/index.test.js
+++ b/tests/index.test.js
@@ -8,7 +8,11 @@ describe('react-native-dotenv', () => {
}
const OLD_ENV = process.env
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => {})
+ })
afterEach(() => {
+ console.error.mockRestore()
jest.resetModules()
process.env = { ...OLD_ENV }
})
@@ -31,10 +35,29 @@ describe('react-native-dotenv', () => {
})
it('should print the environment if setting to verbose', () => {
- console.log = jest.fn()
const { code } = transformFileSync(FIXTURES + 'verbose/source.js')
expect(code).toBe('console.log("abc123");\nconsole.log("username");')
- expect(console.log.mock.calls[0][1]).toBe('test')
+ expect(console.error.mock.calls.some(call => String(call[0]).includes('dotenvMode test'))).toBe(true)
+ })
+
+ it('should log injected env to stderr', () => {
+ jest.resetModules()
+ transformSync('import { API_KEY } from "@env"; console.log(API_KEY)', {
+ configFile: false,
+ babelrc: false,
+ plugins: [[require('../index.js'), { path: FIXTURES + 'default/.env' }]]
+ })
+ expect(console.error.mock.calls.some(call => /^◇ injected env \(\d+\) from /.test(String(call[0])))).toBe(true)
+ })
+
+ it('should not log injected env when quiet', () => {
+ jest.resetModules()
+ transformSync('import { API_KEY } from "@env"', {
+ configFile: false,
+ babelrc: false,
+ plugins: [[require('../index.js'), { path: FIXTURES + 'default/.env', quiet: true }]]
+ })
+ expect(console.error.mock.calls.some(call => String(call[0]).includes('injected env'))).toBe(false)
})
it('should allow importing variables already defined in the environment', () => {
@@ -151,21 +174,19 @@ describe('react-native-dotenv', () => {
})
it('should fail to load APP_ENV development', () => {
- console.error = jest.fn()
process.env.APP_ENV = 'development'
const { code } = transformFileSync(FIXTURES + 'app-env-development/source.js')
expect(code).toBe('console.log("never");\nconsole.log("this-should-not-appear");')
- expect(console.error.mock.calls[0][0]).toBe('APP_ENV error')
+ expect(console.error.mock.calls.some(call => call[0] === 'APP_ENV error')).toBe(true)
})
it('should fail to load APP_ENV production', () => {
- console.error = jest.fn()
process.env.APP_ENV = 'production'
const { code } = transformFileSync(FIXTURES + 'app-env-production/source.js')
expect(code).toBe('console.log("never");\nconsole.log("this-should-not-appear");')
- expect(console.error.mock.calls[0][0]).toBe('APP_ENV error')
+ expect(console.error.mock.calls.some(call => call[0] === 'APP_ENV error')).toBe(true)
})
it('should load MY_ENV specific env file', () => {