Skip to content

Commit f15aa56

Browse files
authored
Merge pull request #593 from dotenvx/injecting-message
add expo example
2 parents 4a63871 + 0b6378f commit f15aa56

4 files changed

Lines changed: 129 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

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

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

7-
## [4.0.0](https://github.com/dotenvx/dotenvx/compare/v3.4.12...v4.0.0) (2026-07-28)
7+
### Added
8+
9+
- Log `◇ injected env (N) from .env` to stderr on transform (same style as dotenv). Set `quiet: true` to suppress.
10+
11+
## [4.0.0](https://github.com/dotenvx/react-native-dotenv/compare/v3.4.12...v4.0.0) (2026-07-28)
812

913
### Fixed
1014

README.md

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ fetch(`${API_URL}/users`)
4848

4949
That's it. Your environment variables from `.env` are available via `@env`.
5050

51+
On transform you'll see a message on stderr (same style as dotenv):
52+
53+
```text
54+
◇ injected env (2) from .env
55+
```
56+
57+
Set `quiet: true` in the plugin options to suppress it.
58+
5159
 
5260

5361
## Advanced
@@ -204,7 +212,7 @@ When set to `false`, an error will be thrown.
204212
</details>
205213
<details><summary><code>verbose</code> (default: <code>false</code>)</summary><br>
206214

207-
Print the active dotenv mode while transforming.
215+
Also print the active dotenv mode to stderr while transforming.
208216

209217
```json
210218
{
@@ -216,6 +224,21 @@ Print the active dotenv mode while transforming.
216224
}
217225
```
218226

227+
</details>
228+
<details><summary><code>quiet</code> (default: <code>false</code>)</summary><br>
229+
230+
Suppress the stderr inject message (`◇ injected env (N) from .env`).
231+
232+
```json
233+
{
234+
"plugins": [
235+
["module:react-native-dotenv", {
236+
"quiet": true
237+
}]
238+
]
239+
}
240+
```
241+
219242
</details>
220243
<details><summary>process.env</summary><br>
221244

@@ -233,9 +256,37 @@ For host/CI-only values, use `@env` imports — or put the key in `.env` and let
233256
</details>
234257
<details><summary>Expo</summary><br>
235258

236-
Expo now has [built-in environment variable support](https://docs.expo.dev/guides/environment-variables/). Evaluate if you still need this plugin.
259+
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`).
260+
261+
```js
262+
// babel.config.js
263+
module.exports = function (api) {
264+
api.cache(false)
265+
return {
266+
presets: ['babel-preset-expo'],
267+
plugins: [
268+
['module:react-native-dotenv']
269+
]
270+
}
271+
}
272+
```
273+
274+
```ini
275+
# .env
276+
HELLO="Universe"
277+
```
278+
279+
```js
280+
// app/index.tsx
281+
import { HELLO } from '@env'
282+
import { Text } from 'react-native'
283+
284+
export default function HomeScreen() {
285+
return <Text>Hello {HELLO}</Text>
286+
}
287+
```
237288

238-
Preview [the expo test app](https://github.com/goatandsheep/react-native-dotenv-expo-test).
289+
Then start with a clean Metro cache: `npx expo start --clear`.
239290

240291
</details>
241292
<details><summary>Multi-env</summary><br>

index.js

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,26 @@ const path = require('path')
33
const dotenv = require('dotenv')
44

55
function parseDotenvFile (filepath, verbose = false) {
6-
let content
7-
86
try {
9-
content = fs.readFileSync(filepath)
7+
const content = fs.readFileSync(filepath)
8+
return { parsed: dotenv.parse(content), exists: true }
109
} catch (error) {
1110
// The env file does not exist.
1211
if (verbose) {
1312
console.error('react-native-dotenv', error)
1413
}
1514

16-
return {}
15+
return { parsed: {}, exists: false }
1716
}
17+
}
1818

19-
return dotenv.parse(content) //
19+
function logInjectedEnv (fileEnv, loadedPaths) {
20+
const keysCount = Object.keys(fileEnv).length
21+
if (loadedPaths.length > 0) {
22+
console.error(`◇ injected env (${keysCount}) from ${loadedPaths.join(', ')}`)
23+
} else {
24+
console.error(`◇ injected env (${keysCount})`)
25+
}
2026
}
2127

2228
function undefObjectAssign (targetObject, sourceObject) {
@@ -67,6 +73,7 @@ module.exports = (api, options) => {
6773
safe: false,
6874
allowUndefined: true,
6975
verbose: false,
76+
quiet: false,
7077
...options
7178
}
7279
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) => {
7582
const modeLocalFilePath = options.path + '.' + babelMode + '.local'
7683

7784
if (options.verbose) {
78-
console.log('dotenvMode', babelMode)
85+
console.error(`◇ dotenvMode ${babelMode}`)
7986
if (process.env[options.envName] === 'production' || process.env[options.envName] === 'development') {
8087
console.error('APP_ENV error', 'cannot use APP_ENV=development or APP_ENV=production')
8188
}
@@ -87,12 +94,18 @@ module.exports = (api, options) => {
8794
api.cache.using(() => mtime(modeLocalFilePath))
8895

8996
const dotenvTemporary = undefObjectAssign({}, process.env)
90-
const parsed = parseDotenvFile(options.path, options.verbose)
91-
const localParsed = parseDotenvFile(localFilePath, options.verbose)
92-
const modeParsed = parseDotenvFile(modeFilePath, options.verbose)
93-
const modeLocalParsed = parseDotenvFile(modeLocalFilePath, options.verbose)
97+
const parsedFile = parseDotenvFile(options.path, options.verbose)
98+
const localFile = parseDotenvFile(localFilePath, options.verbose)
99+
const modeFile = parseDotenvFile(modeFilePath, options.verbose)
100+
const modeLocalFile = parseDotenvFile(modeLocalFilePath, options.verbose)
94101
const modeExceptions = ['NODE_ENV', 'BABEL_ENV', options.envName]
95-
const fileEnv = undefObjectAssign(undefObjectAssign(undefObjectAssign(parsed, modeParsed), localParsed), modeLocalParsed)
102+
const fileEnv = undefObjectAssign(
103+
undefObjectAssign(
104+
undefObjectAssign(parsedFile.parsed, modeFile.parsed),
105+
localFile.parsed
106+
),
107+
modeLocalFile.parsed
108+
)
96109

97110
// process.env.X is only inlined for keys from .env files (+ mode exceptions).
98111
// That stops build-tooling pollution (e.g. Metro jest-worker) without hardcoding
@@ -106,10 +119,24 @@ module.exports = (api, options) => {
106119
? safeObjectAssign(undefObjectAssign({}, fileEnv), dotenvTemporary, modeExceptions)
107120
: undefObjectAssign(undefObjectAssign({}, fileEnv), dotenvTemporary)
108121

109-
api.addExternalDependency(path.resolve(options.path))
110-
api.addExternalDependency(path.resolve(modeFilePath))
111-
api.addExternalDependency(path.resolve(localFilePath))
112-
api.addExternalDependency(path.resolve(modeLocalFilePath))
122+
if (options.verbose || !options.quiet) {
123+
const loadedPaths = [
124+
[options.path, parsedFile.exists],
125+
[modeFilePath, modeFile.exists],
126+
[localFilePath, localFile.exists],
127+
[modeLocalFilePath, modeLocalFile.exists]
128+
]
129+
.filter(([, exists]) => exists)
130+
.map(([filepath]) => filepath)
131+
logInjectedEnv(fileEnv, loadedPaths)
132+
}
133+
134+
if (typeof api.addExternalDependency === 'function') {
135+
api.addExternalDependency(path.resolve(options.path))
136+
api.addExternalDependency(path.resolve(modeFilePath))
137+
api.addExternalDependency(path.resolve(localFilePath))
138+
api.addExternalDependency(path.resolve(modeLocalFilePath))
139+
}
113140

114141
return ({
115142
name: 'dotenv-import',

tests/index.test.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ describe('react-native-dotenv', () => {
88
}
99

1010
const OLD_ENV = process.env
11+
beforeEach(() => {
12+
jest.spyOn(console, 'error').mockImplementation(() => {})
13+
})
1114
afterEach(() => {
15+
console.error.mockRestore()
1216
jest.resetModules()
1317
process.env = { ...OLD_ENV }
1418
})
@@ -31,10 +35,29 @@ describe('react-native-dotenv', () => {
3135
})
3236

3337
it('should print the environment if setting to verbose', () => {
34-
console.log = jest.fn()
3538
const { code } = transformFileSync(FIXTURES + 'verbose/source.js')
3639
expect(code).toBe('console.log("abc123");\nconsole.log("username");')
37-
expect(console.log.mock.calls[0][1]).toBe('test')
40+
expect(console.error.mock.calls.some(call => String(call[0]).includes('dotenvMode test'))).toBe(true)
41+
})
42+
43+
it('should log injected env to stderr', () => {
44+
jest.resetModules()
45+
transformSync('import { API_KEY } from "@env"; console.log(API_KEY)', {
46+
configFile: false,
47+
babelrc: false,
48+
plugins: [[require('../index.js'), { path: FIXTURES + 'default/.env' }]]
49+
})
50+
expect(console.error.mock.calls.some(call => /^ injected env \(\d+\) from /.test(String(call[0])))).toBe(true)
51+
})
52+
53+
it('should not log injected env when quiet', () => {
54+
jest.resetModules()
55+
transformSync('import { API_KEY } from "@env"', {
56+
configFile: false,
57+
babelrc: false,
58+
plugins: [[require('../index.js'), { path: FIXTURES + 'default/.env', quiet: true }]]
59+
})
60+
expect(console.error.mock.calls.some(call => String(call[0]).includes('injected env'))).toBe(false)
3861
})
3962

4063
it('should allow importing variables already defined in the environment', () => {
@@ -151,21 +174,19 @@ describe('react-native-dotenv', () => {
151174
})
152175

153176
it('should fail to load APP_ENV development', () => {
154-
console.error = jest.fn()
155177
process.env.APP_ENV = 'development'
156178

157179
const { code } = transformFileSync(FIXTURES + 'app-env-development/source.js')
158180
expect(code).toBe('console.log("never");\nconsole.log("this-should-not-appear");')
159-
expect(console.error.mock.calls[0][0]).toBe('APP_ENV error')
181+
expect(console.error.mock.calls.some(call => call[0] === 'APP_ENV error')).toBe(true)
160182
})
161183

162184
it('should fail to load APP_ENV production', () => {
163-
console.error = jest.fn()
164185
process.env.APP_ENV = 'production'
165186

166187
const { code } = transformFileSync(FIXTURES + 'app-env-production/source.js')
167188
expect(code).toBe('console.log("never");\nconsole.log("this-should-not-appear");')
168-
expect(console.error.mock.calls[0][0]).toBe('APP_ENV error')
189+
expect(console.error.mock.calls.some(call => call[0] === 'APP_ENV error')).toBe(true)
169190
})
170191

171192
it('should load MY_ENV specific env file', () => {

0 commit comments

Comments
 (0)