@idealyst/config 1.2.13 → 1.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -91
- package/package.json +6 -1
- package/plugin.js +233 -0
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@ Cross-platform configuration for React and React Native with env inheritance sup
|
|
|
6
6
|
|
|
7
7
|
- **Single API** - Same code works on web and native
|
|
8
8
|
- **Env inheritance** - Shared config with platform-specific overrides
|
|
9
|
+
- **Babel plugin** - Config injected at build time, no runtime overhead
|
|
9
10
|
- **Type-safe** - Auto-generated TypeScript declarations
|
|
10
|
-
- **Bundler agnostic** - No bundler configuration needed
|
|
11
11
|
- **Monorepo friendly** - Designed for shared/web/mobile patterns
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
@@ -54,75 +54,59 @@ API_URL=https://web-api.example.com
|
|
|
54
54
|
ANALYTICS_ENABLED=false
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
### 2.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// Web: API_URL overridden, others inherited
|
|
71
|
-
export const generatedConfig = {
|
|
72
|
-
API_URL: "https://web-api.example.com",
|
|
73
|
-
GOOGLE_CLIENT_ID: "abc123",
|
|
74
|
-
ANALYTICS_ENABLED: "true"
|
|
57
|
+
### 2. Add Babel plugin
|
|
58
|
+
|
|
59
|
+
**babel.config.js:**
|
|
60
|
+
```js
|
|
61
|
+
module.exports = {
|
|
62
|
+
presets: ['...'],
|
|
63
|
+
plugins: [
|
|
64
|
+
['@idealyst/config/plugin', {
|
|
65
|
+
extends: ['../shared/.env'],
|
|
66
|
+
envPath: '.env'
|
|
67
|
+
}]
|
|
68
|
+
]
|
|
75
69
|
}
|
|
76
70
|
```
|
|
77
71
|
|
|
78
|
-
### 3.
|
|
72
|
+
### 3. Use in your app
|
|
79
73
|
|
|
80
74
|
```typescript
|
|
81
|
-
// In your app entry point (e.g., App.tsx, main.tsx)
|
|
82
|
-
import { setConfig } from '@idealyst/config'
|
|
83
|
-
import { generatedConfig } from './config.generated'
|
|
84
|
-
|
|
85
|
-
setConfig(generatedConfig)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
// Anywhere in your app
|
|
90
75
|
import { config } from '@idealyst/config'
|
|
91
76
|
|
|
77
|
+
// Values are injected at build time!
|
|
92
78
|
const apiUrl = config.get('API_URL')
|
|
93
79
|
const analyticsEnabled = config.get('ANALYTICS_ENABLED') === 'true'
|
|
94
80
|
```
|
|
95
81
|
|
|
96
|
-
|
|
82
|
+
That's it! The Babel plugin reads your .env files at compile time and injects the values automatically.
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
idealyst-config generate [options]
|
|
84
|
+
## Babel Plugin Options
|
|
100
85
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
--types-only Generate only .d.ts file, no values
|
|
106
|
-
--help Show help
|
|
107
|
-
```
|
|
86
|
+
```js
|
|
87
|
+
['@idealyst/config/plugin', {
|
|
88
|
+
// Inherit from these .env files (lowest to highest priority)
|
|
89
|
+
extends: ['../shared/.env', '../common/.env'],
|
|
108
90
|
|
|
109
|
-
|
|
91
|
+
// Main .env file (highest priority, default: auto-detect)
|
|
92
|
+
envPath: '.env',
|
|
110
93
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
94
|
+
// Project root (default: process.cwd())
|
|
95
|
+
root: '/path/to/project'
|
|
96
|
+
}]
|
|
97
|
+
```
|
|
114
98
|
|
|
115
|
-
|
|
116
|
-
idealyst-config generate --extends ../shared/.env --env .env
|
|
99
|
+
### Auto-detection
|
|
117
100
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
--extends ../.env.common \
|
|
122
|
-
--env .env
|
|
101
|
+
If you don't specify options, the plugin will:
|
|
102
|
+
1. Auto-detect `.env.local`, `.env.development`, or `.env` in your project
|
|
103
|
+
2. Auto-detect `../shared/.env` or `../../shared/.env` for inheritance
|
|
123
104
|
|
|
124
|
-
|
|
125
|
-
|
|
105
|
+
```js
|
|
106
|
+
// Minimal config - auto-detects everything
|
|
107
|
+
plugins: [
|
|
108
|
+
'@idealyst/config/plugin'
|
|
109
|
+
]
|
|
126
110
|
```
|
|
127
111
|
|
|
128
112
|
## Inheritance Priority
|
|
@@ -130,9 +114,49 @@ idealyst-config generate --types-only --output src/env.d.ts
|
|
|
130
114
|
Configs are merged in order, with later files overriding earlier ones:
|
|
131
115
|
|
|
132
116
|
```
|
|
133
|
-
1.
|
|
134
|
-
2.
|
|
135
|
-
3.
|
|
117
|
+
1. extends[0]: ../shared/.env (lowest priority)
|
|
118
|
+
2. extends[1]: ../common/.env
|
|
119
|
+
3. envPath: .env (highest priority)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Vite Setup
|
|
123
|
+
|
|
124
|
+
For Vite projects, add to `vite.config.ts`:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { defineConfig } from 'vite'
|
|
128
|
+
import react from '@vitejs/plugin-react'
|
|
129
|
+
|
|
130
|
+
export default defineConfig({
|
|
131
|
+
plugins: [
|
|
132
|
+
react({
|
|
133
|
+
babel: {
|
|
134
|
+
plugins: [
|
|
135
|
+
['@idealyst/config/plugin', {
|
|
136
|
+
extends: ['../shared/.env'],
|
|
137
|
+
envPath: '.env'
|
|
138
|
+
}]
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
]
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## React Native / Metro Setup
|
|
147
|
+
|
|
148
|
+
Add to `babel.config.js`:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
module.exports = {
|
|
152
|
+
presets: ['module:@react-native/babel-preset'],
|
|
153
|
+
plugins: [
|
|
154
|
+
['@idealyst/config/plugin', {
|
|
155
|
+
extends: ['../shared/.env'],
|
|
156
|
+
envPath: '.env'
|
|
157
|
+
}]
|
|
158
|
+
]
|
|
159
|
+
}
|
|
136
160
|
```
|
|
137
161
|
|
|
138
162
|
## API Reference
|
|
@@ -180,38 +204,17 @@ config.validate(['API_URL', 'AUTH_SECRET'])
|
|
|
180
204
|
// Throws ConfigValidationError if any are missing
|
|
181
205
|
```
|
|
182
206
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
Initialize config values (call once at app startup).
|
|
207
|
+
## Type Generation (Optional)
|
|
186
208
|
|
|
187
|
-
|
|
188
|
-
import { setConfig } from '@idealyst/config'
|
|
189
|
-
import { generatedConfig } from './config.generated'
|
|
209
|
+
For TypeScript autocomplete, generate type declarations:
|
|
190
210
|
|
|
191
|
-
setConfig(generatedConfig)
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
## React Native Setup
|
|
195
|
-
|
|
196
|
-
1. Install react-native-config:
|
|
197
211
|
```bash
|
|
198
|
-
|
|
199
|
-
cd ios && pod install
|
|
212
|
+
npx idealyst-config generate --extends ../shared/.env --env .env --types-only
|
|
200
213
|
```
|
|
201
214
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
3. Generate and use the same way as web:
|
|
205
|
-
```bash
|
|
206
|
-
idealyst-config generate --extends ../shared/.env --env .env
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
## Type Safety
|
|
210
|
-
|
|
211
|
-
The CLI generates TypeScript declarations for autocomplete:
|
|
215
|
+
This creates `src/config.generated.d.ts`:
|
|
212
216
|
|
|
213
217
|
```typescript
|
|
214
|
-
// Generated: src/config.generated.d.ts
|
|
215
218
|
declare module '@idealyst/config' {
|
|
216
219
|
interface ConfigKeys {
|
|
217
220
|
API_URL: string
|
|
@@ -221,27 +224,47 @@ declare module '@idealyst/config' {
|
|
|
221
224
|
}
|
|
222
225
|
```
|
|
223
226
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
## Build Integration
|
|
227
|
-
|
|
228
|
-
Add to your build scripts:
|
|
227
|
+
Add to your build script for automatic updates:
|
|
229
228
|
|
|
230
229
|
```json
|
|
231
230
|
{
|
|
232
231
|
"scripts": {
|
|
233
|
-
"prebuild": "idealyst-config generate --extends ../shared/.env"
|
|
234
|
-
"build": "vite build"
|
|
232
|
+
"prebuild": "idealyst-config generate --extends ../shared/.env --types-only"
|
|
235
233
|
}
|
|
236
234
|
}
|
|
237
235
|
```
|
|
238
236
|
|
|
237
|
+
## How It Works
|
|
238
|
+
|
|
239
|
+
The Babel plugin transforms your code at compile time:
|
|
240
|
+
|
|
241
|
+
**Input:**
|
|
242
|
+
```typescript
|
|
243
|
+
import { config } from '@idealyst/config'
|
|
244
|
+
|
|
245
|
+
const apiUrl = config.get('API_URL')
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Output (after Babel):**
|
|
249
|
+
```typescript
|
|
250
|
+
import { config, setConfig as __idealyst_setConfig } from '@idealyst/config'
|
|
251
|
+
__idealyst_setConfig({ API_URL: "https://api.example.com", GOOGLE_CLIENT_ID: "abc123" })
|
|
252
|
+
|
|
253
|
+
const apiUrl = config.get('API_URL')
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
This means:
|
|
257
|
+
- **No runtime .env parsing** - values are baked in at build time
|
|
258
|
+
- **Works with any bundler** - Vite, Webpack, Metro, esbuild
|
|
259
|
+
- **Tree-shakeable** - unused config keys can be eliminated
|
|
260
|
+
- **Secure** - .env files never shipped to client
|
|
261
|
+
|
|
239
262
|
## Best Practices
|
|
240
263
|
|
|
241
|
-
1. **
|
|
242
|
-
2. **
|
|
243
|
-
3. **
|
|
244
|
-
4. **Validate at startup** -
|
|
264
|
+
1. **Gitignore .env files** - Never commit secrets
|
|
265
|
+
2. **Commit .env.example** - Document required keys
|
|
266
|
+
3. **Use shared config** - DRY principle for common values
|
|
267
|
+
4. **Validate at startup** - Catch missing config early
|
|
245
268
|
|
|
246
269
|
## License
|
|
247
270
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/config",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.14",
|
|
4
4
|
"description": "Cross-platform configuration and environment variable support for React and React Native",
|
|
5
5
|
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/config#readme",
|
|
6
6
|
"readme": "README.md",
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"require": "./src/index.ts"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
+
"./plugin": {
|
|
39
|
+
"require": "./plugin.js",
|
|
40
|
+
"default": "./plugin.js"
|
|
41
|
+
},
|
|
38
42
|
"./generate": {
|
|
39
43
|
"types": "./src/cli/generate.ts",
|
|
40
44
|
"import": "./src/cli/generate.ts",
|
|
@@ -61,6 +65,7 @@
|
|
|
61
65
|
"files": [
|
|
62
66
|
"src",
|
|
63
67
|
"bin",
|
|
68
|
+
"plugin.js",
|
|
64
69
|
"README.md"
|
|
65
70
|
],
|
|
66
71
|
"keywords": [
|
package/plugin.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @idealyst/config Babel plugin
|
|
3
|
+
*
|
|
4
|
+
* Injects config values at compile time from .env files.
|
|
5
|
+
*
|
|
6
|
+
* Usage in babel.config.js:
|
|
7
|
+
* ```js
|
|
8
|
+
* module.exports = {
|
|
9
|
+
* plugins: [
|
|
10
|
+
* ['@idealyst/config/plugin', {
|
|
11
|
+
* extends: ['../shared/.env'],
|
|
12
|
+
* envPath: '.env'
|
|
13
|
+
* }]
|
|
14
|
+
* ]
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs')
|
|
20
|
+
const path = require('path')
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse a .env file and extract key-value pairs.
|
|
24
|
+
*/
|
|
25
|
+
function parseEnvFile(filePath) {
|
|
26
|
+
if (!fs.existsSync(filePath)) {
|
|
27
|
+
return {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
31
|
+
const config = {}
|
|
32
|
+
|
|
33
|
+
for (const line of content.split('\n')) {
|
|
34
|
+
const trimmed = line.trim()
|
|
35
|
+
|
|
36
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const equalsIndex = trimmed.indexOf('=')
|
|
41
|
+
if (equalsIndex === -1) {
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let key = trimmed.substring(0, equalsIndex).trim()
|
|
46
|
+
let value = trimmed.substring(equalsIndex + 1).trim()
|
|
47
|
+
|
|
48
|
+
// Remove quotes if present
|
|
49
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
50
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
51
|
+
value = value.slice(1, -1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Strip VITE_ prefix to normalize
|
|
55
|
+
if (key.startsWith('VITE_')) {
|
|
56
|
+
key = key.substring(5)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
config[key] = value
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return config
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Find .env file in directory.
|
|
67
|
+
*/
|
|
68
|
+
function findEnvFile(directory) {
|
|
69
|
+
const candidates = ['.env.local', '.env.development', '.env']
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
const envPath = path.join(directory, candidate)
|
|
72
|
+
if (fs.existsSync(envPath)) {
|
|
73
|
+
return envPath
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Look for shared .env in common monorepo locations.
|
|
81
|
+
*/
|
|
82
|
+
function findSharedEnv(directory) {
|
|
83
|
+
const patterns = [
|
|
84
|
+
'../shared/.env',
|
|
85
|
+
'../../shared/.env',
|
|
86
|
+
'../../packages/shared/.env',
|
|
87
|
+
]
|
|
88
|
+
for (const pattern of patterns) {
|
|
89
|
+
const sharedPath = path.resolve(directory, pattern)
|
|
90
|
+
if (fs.existsSync(sharedPath)) {
|
|
91
|
+
return sharedPath
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Load and merge config from .env files.
|
|
99
|
+
*/
|
|
100
|
+
function loadConfig(options, projectRoot) {
|
|
101
|
+
const configs = []
|
|
102
|
+
|
|
103
|
+
// Load inherited configs first (lowest priority)
|
|
104
|
+
if (options.extends) {
|
|
105
|
+
for (const extendPath of options.extends) {
|
|
106
|
+
const resolvedPath = path.isAbsolute(extendPath)
|
|
107
|
+
? extendPath
|
|
108
|
+
: path.resolve(projectRoot, extendPath)
|
|
109
|
+
|
|
110
|
+
if (fs.existsSync(resolvedPath)) {
|
|
111
|
+
configs.push(parseEnvFile(resolvedPath))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// Auto-detect shared env
|
|
116
|
+
const sharedEnv = findSharedEnv(projectRoot)
|
|
117
|
+
if (sharedEnv) {
|
|
118
|
+
configs.push(parseEnvFile(sharedEnv))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Load main env file (highest priority)
|
|
123
|
+
let envPath = null
|
|
124
|
+
if (options.envPath) {
|
|
125
|
+
envPath = path.isAbsolute(options.envPath)
|
|
126
|
+
? options.envPath
|
|
127
|
+
: path.resolve(projectRoot, options.envPath)
|
|
128
|
+
} else {
|
|
129
|
+
envPath = findEnvFile(projectRoot)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (envPath && fs.existsSync(envPath)) {
|
|
133
|
+
configs.push(parseEnvFile(envPath))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Merge configs (later configs override earlier ones)
|
|
137
|
+
return Object.assign({}, ...configs)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Babel plugin that injects config values at compile time.
|
|
142
|
+
*/
|
|
143
|
+
module.exports = function babelPluginIdealystConfig(babel) {
|
|
144
|
+
const { types: t } = babel
|
|
145
|
+
|
|
146
|
+
// Cache config per project root
|
|
147
|
+
const configCache = new Map()
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
name: '@idealyst/config',
|
|
151
|
+
|
|
152
|
+
visitor: {
|
|
153
|
+
Program: {
|
|
154
|
+
enter(programPath, state) {
|
|
155
|
+
const opts = state.opts || {}
|
|
156
|
+
const projectRoot = opts.root || state.cwd || process.cwd()
|
|
157
|
+
|
|
158
|
+
// Load config (cached per project root)
|
|
159
|
+
if (!configCache.has(projectRoot)) {
|
|
160
|
+
configCache.set(projectRoot, loadConfig(opts, projectRoot))
|
|
161
|
+
}
|
|
162
|
+
const configValues = configCache.get(projectRoot)
|
|
163
|
+
|
|
164
|
+
// Track if this file imports from @idealyst/config
|
|
165
|
+
let hasConfigImport = false
|
|
166
|
+
let configImportPath = null
|
|
167
|
+
|
|
168
|
+
// Find imports from @idealyst/config
|
|
169
|
+
programPath.traverse({
|
|
170
|
+
ImportDeclaration(importPath) {
|
|
171
|
+
if (importPath.node.source.value === '@idealyst/config') {
|
|
172
|
+
hasConfigImport = true
|
|
173
|
+
configImportPath = importPath
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
if (!hasConfigImport || !configImportPath) {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if setConfig is already imported
|
|
183
|
+
let hasSetConfigImport = false
|
|
184
|
+
let setConfigLocalName = '__idealyst_setConfig'
|
|
185
|
+
|
|
186
|
+
for (const specifier of configImportPath.node.specifiers) {
|
|
187
|
+
if (t.isImportSpecifier(specifier) &&
|
|
188
|
+
t.isIdentifier(specifier.imported) &&
|
|
189
|
+
specifier.imported.name === 'setConfig') {
|
|
190
|
+
hasSetConfigImport = true
|
|
191
|
+
setConfigLocalName = specifier.local.name
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Add setConfig to imports if not already present
|
|
197
|
+
if (!hasSetConfigImport) {
|
|
198
|
+
configImportPath.node.specifiers.push(
|
|
199
|
+
t.importSpecifier(
|
|
200
|
+
t.identifier('__idealyst_setConfig'),
|
|
201
|
+
t.identifier('setConfig')
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Create the config object literal
|
|
207
|
+
const configProperties = Object.entries(configValues).map(([key, value]) =>
|
|
208
|
+
t.objectProperty(t.identifier(key), t.stringLiteral(value))
|
|
209
|
+
)
|
|
210
|
+
const configObject = t.objectExpression(configProperties)
|
|
211
|
+
|
|
212
|
+
// Create setConfig call
|
|
213
|
+
const setConfigCall = t.expressionStatement(
|
|
214
|
+
t.callExpression(
|
|
215
|
+
t.identifier(setConfigLocalName),
|
|
216
|
+
[configObject]
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
// Insert after the import statement
|
|
221
|
+
const importIndex = programPath.node.body.indexOf(configImportPath.node)
|
|
222
|
+
programPath.node.body.splice(importIndex + 1, 0, setConfigCall)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Export utilities for direct use
|
|
230
|
+
module.exports.parseEnvFile = parseEnvFile
|
|
231
|
+
module.exports.loadConfig = loadConfig
|
|
232
|
+
module.exports.findEnvFile = findEnvFile
|
|
233
|
+
module.exports.findSharedEnv = findSharedEnv
|