@idealyst/config 1.2.13 → 1.2.15
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 +150 -89
- package/package.json +6 -1
- package/plugin.js +293 -0
- package/src/config.native.ts +52 -21
- package/src/index.native.ts +5 -4
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,97 @@ 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
|
-
|
|
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
|
+
]
|
|
69
|
+
}
|
|
65
70
|
```
|
|
66
71
|
|
|
67
|
-
|
|
72
|
+
### 3. Use in your app
|
|
68
73
|
|
|
69
74
|
```typescript
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
75
|
+
import { config } from '@idealyst/config'
|
|
76
|
+
|
|
77
|
+
// Values are injected at build time!
|
|
78
|
+
const apiUrl = config.get('API_URL')
|
|
79
|
+
const analyticsEnabled = config.get('ANALYTICS_ENABLED') === 'true'
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
That's it! The Babel plugin reads your .env files at compile time and injects the values automatically.
|
|
79
83
|
|
|
80
|
-
|
|
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
|
+
## Babel Plugin Options
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
```js
|
|
87
|
+
['@idealyst/config/plugin', {
|
|
88
|
+
// Inherit from these .env files (lowest to highest priority)
|
|
89
|
+
extends: ['../shared/.env', '../common/.env'],
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
import { config } from '@idealyst/config'
|
|
91
|
+
// Main .env file (highest priority, default: auto-detect)
|
|
92
|
+
envPath: '.env',
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
// Required keys - warn if missing at build time
|
|
95
|
+
required: ['API_URL', 'AUTH_SECRET'],
|
|
96
|
+
|
|
97
|
+
// Fail the build if required keys are missing (default: false = warn only)
|
|
98
|
+
errorOnMissing: true,
|
|
99
|
+
|
|
100
|
+
// Log which .env files were loaded (default: false)
|
|
101
|
+
verbose: true,
|
|
102
|
+
|
|
103
|
+
// Project root (default: process.cwd())
|
|
104
|
+
root: '/path/to/project'
|
|
105
|
+
}]
|
|
94
106
|
```
|
|
95
107
|
|
|
96
|
-
|
|
108
|
+
### Build-time Validation
|
|
97
109
|
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
The plugin validates required keys at compile time:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
// babel.config.js
|
|
114
|
+
plugins: [
|
|
115
|
+
['@idealyst/config/plugin', {
|
|
116
|
+
required: ['API_URL', 'DATABASE_URL', 'JWT_SECRET'],
|
|
117
|
+
errorOnMissing: process.env.NODE_ENV === 'production' // Fail in prod, warn in dev
|
|
118
|
+
}]
|
|
119
|
+
]
|
|
120
|
+
```
|
|
100
121
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
--help Show help
|
|
122
|
+
**Warning output:**
|
|
123
|
+
```
|
|
124
|
+
⚠ @idealyst/config: Missing required config keys: JWT_SECRET
|
|
125
|
+
Add these keys to your .env file to suppress this warning.
|
|
126
|
+
Set errorOnMissing: true to fail the build instead.
|
|
107
127
|
```
|
|
108
128
|
|
|
109
|
-
|
|
129
|
+
**Error output (with `errorOnMissing: true`):**
|
|
130
|
+
```
|
|
131
|
+
✖ @idealyst/config: Missing required config keys: JWT_SECRET
|
|
132
|
+
Add these keys to your .env file or check your plugin configuration.
|
|
110
133
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
idealyst-config generate
|
|
134
|
+
Error: @idealyst/config: Missing required config keys: JWT_SECRET
|
|
135
|
+
```
|
|
114
136
|
|
|
115
|
-
|
|
116
|
-
idealyst-config generate --extends ../shared/.env --env .env
|
|
137
|
+
### Auto-detection
|
|
117
138
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
--extends ../.env.common \
|
|
122
|
-
--env .env
|
|
139
|
+
If you don't specify options, the plugin will:
|
|
140
|
+
1. Auto-detect `.env.local`, `.env.development`, or `.env` in your project
|
|
141
|
+
2. Auto-detect `../shared/.env` or `../../shared/.env` for inheritance
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
```js
|
|
144
|
+
// Minimal config - auto-detects everything
|
|
145
|
+
plugins: [
|
|
146
|
+
'@idealyst/config/plugin'
|
|
147
|
+
]
|
|
126
148
|
```
|
|
127
149
|
|
|
128
150
|
## Inheritance Priority
|
|
@@ -130,9 +152,49 @@ idealyst-config generate --types-only --output src/env.d.ts
|
|
|
130
152
|
Configs are merged in order, with later files overriding earlier ones:
|
|
131
153
|
|
|
132
154
|
```
|
|
133
|
-
1.
|
|
134
|
-
2.
|
|
135
|
-
3.
|
|
155
|
+
1. extends[0]: ../shared/.env (lowest priority)
|
|
156
|
+
2. extends[1]: ../common/.env
|
|
157
|
+
3. envPath: .env (highest priority)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Vite Setup
|
|
161
|
+
|
|
162
|
+
For Vite projects, add to `vite.config.ts`:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { defineConfig } from 'vite'
|
|
166
|
+
import react from '@vitejs/plugin-react'
|
|
167
|
+
|
|
168
|
+
export default defineConfig({
|
|
169
|
+
plugins: [
|
|
170
|
+
react({
|
|
171
|
+
babel: {
|
|
172
|
+
plugins: [
|
|
173
|
+
['@idealyst/config/plugin', {
|
|
174
|
+
extends: ['../shared/.env'],
|
|
175
|
+
envPath: '.env'
|
|
176
|
+
}]
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
]
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## React Native / Metro Setup
|
|
185
|
+
|
|
186
|
+
Add to `babel.config.js`:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
module.exports = {
|
|
190
|
+
presets: ['module:@react-native/babel-preset'],
|
|
191
|
+
plugins: [
|
|
192
|
+
['@idealyst/config/plugin', {
|
|
193
|
+
extends: ['../shared/.env'],
|
|
194
|
+
envPath: '.env'
|
|
195
|
+
}]
|
|
196
|
+
]
|
|
197
|
+
}
|
|
136
198
|
```
|
|
137
199
|
|
|
138
200
|
## API Reference
|
|
@@ -180,38 +242,17 @@ config.validate(['API_URL', 'AUTH_SECRET'])
|
|
|
180
242
|
// Throws ConfigValidationError if any are missing
|
|
181
243
|
```
|
|
182
244
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
Initialize config values (call once at app startup).
|
|
186
|
-
|
|
187
|
-
```typescript
|
|
188
|
-
import { setConfig } from '@idealyst/config'
|
|
189
|
-
import { generatedConfig } from './config.generated'
|
|
190
|
-
|
|
191
|
-
setConfig(generatedConfig)
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
## React Native Setup
|
|
195
|
-
|
|
196
|
-
1. Install react-native-config:
|
|
197
|
-
```bash
|
|
198
|
-
npm install react-native-config
|
|
199
|
-
cd ios && pod install
|
|
200
|
-
```
|
|
245
|
+
## Type Generation (Optional)
|
|
201
246
|
|
|
202
|
-
|
|
247
|
+
For TypeScript autocomplete, generate type declarations:
|
|
203
248
|
|
|
204
|
-
3. Generate and use the same way as web:
|
|
205
249
|
```bash
|
|
206
|
-
idealyst-config generate --extends ../shared/.env --env .env
|
|
250
|
+
npx idealyst-config generate --extends ../shared/.env --env .env --types-only
|
|
207
251
|
```
|
|
208
252
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
The CLI generates TypeScript declarations for autocomplete:
|
|
253
|
+
This creates `src/config.generated.d.ts`:
|
|
212
254
|
|
|
213
255
|
```typescript
|
|
214
|
-
// Generated: src/config.generated.d.ts
|
|
215
256
|
declare module '@idealyst/config' {
|
|
216
257
|
interface ConfigKeys {
|
|
217
258
|
API_URL: string
|
|
@@ -221,27 +262,47 @@ declare module '@idealyst/config' {
|
|
|
221
262
|
}
|
|
222
263
|
```
|
|
223
264
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
## Build Integration
|
|
227
|
-
|
|
228
|
-
Add to your build scripts:
|
|
265
|
+
Add to your build script for automatic updates:
|
|
229
266
|
|
|
230
267
|
```json
|
|
231
268
|
{
|
|
232
269
|
"scripts": {
|
|
233
|
-
"prebuild": "idealyst-config generate --extends ../shared/.env"
|
|
234
|
-
"build": "vite build"
|
|
270
|
+
"prebuild": "idealyst-config generate --extends ../shared/.env --types-only"
|
|
235
271
|
}
|
|
236
272
|
}
|
|
237
273
|
```
|
|
238
274
|
|
|
275
|
+
## How It Works
|
|
276
|
+
|
|
277
|
+
The Babel plugin transforms your code at compile time:
|
|
278
|
+
|
|
279
|
+
**Input:**
|
|
280
|
+
```typescript
|
|
281
|
+
import { config } from '@idealyst/config'
|
|
282
|
+
|
|
283
|
+
const apiUrl = config.get('API_URL')
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Output (after Babel):**
|
|
287
|
+
```typescript
|
|
288
|
+
import { config, setConfig as __idealyst_setConfig } from '@idealyst/config'
|
|
289
|
+
__idealyst_setConfig({ API_URL: "https://api.example.com", GOOGLE_CLIENT_ID: "abc123" })
|
|
290
|
+
|
|
291
|
+
const apiUrl = config.get('API_URL')
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
This means:
|
|
295
|
+
- **No runtime .env parsing** - values are baked in at build time
|
|
296
|
+
- **Works with any bundler** - Vite, Webpack, Metro, esbuild
|
|
297
|
+
- **Tree-shakeable** - unused config keys can be eliminated
|
|
298
|
+
- **Secure** - .env files never shipped to client
|
|
299
|
+
|
|
239
300
|
## Best Practices
|
|
240
301
|
|
|
241
|
-
1. **
|
|
242
|
-
2. **
|
|
243
|
-
3. **
|
|
244
|
-
4. **Validate at startup** -
|
|
302
|
+
1. **Gitignore .env files** - Never commit secrets
|
|
303
|
+
2. **Commit .env.example** - Document required keys
|
|
304
|
+
3. **Use shared config** - DRY principle for common values
|
|
305
|
+
4. **Validate at startup** - Catch missing config early
|
|
245
306
|
|
|
246
307
|
## License
|
|
247
308
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/config",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.15",
|
|
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,293 @@
|
|
|
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
|
+
* required: ['API_URL', 'AUTH_SECRET'], // Warn if missing
|
|
14
|
+
* errorOnMissing: true // Fail build if missing
|
|
15
|
+
* }]
|
|
16
|
+
* ]
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs')
|
|
22
|
+
const path = require('path')
|
|
23
|
+
|
|
24
|
+
// Track if we've already validated (to avoid duplicate warnings)
|
|
25
|
+
let hasValidated = false
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse a .env file and extract key-value pairs.
|
|
29
|
+
*/
|
|
30
|
+
function parseEnvFile(filePath) {
|
|
31
|
+
if (!fs.existsSync(filePath)) {
|
|
32
|
+
return {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
36
|
+
const config = {}
|
|
37
|
+
|
|
38
|
+
for (const line of content.split('\n')) {
|
|
39
|
+
const trimmed = line.trim()
|
|
40
|
+
|
|
41
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const equalsIndex = trimmed.indexOf('=')
|
|
46
|
+
if (equalsIndex === -1) {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let key = trimmed.substring(0, equalsIndex).trim()
|
|
51
|
+
let value = trimmed.substring(equalsIndex + 1).trim()
|
|
52
|
+
|
|
53
|
+
// Remove quotes if present
|
|
54
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
55
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
56
|
+
value = value.slice(1, -1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Strip VITE_ prefix to normalize
|
|
60
|
+
if (key.startsWith('VITE_')) {
|
|
61
|
+
key = key.substring(5)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
config[key] = value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return config
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Find .env file in directory.
|
|
72
|
+
*/
|
|
73
|
+
function findEnvFile(directory) {
|
|
74
|
+
const candidates = ['.env.local', '.env.development', '.env']
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
const envPath = path.join(directory, candidate)
|
|
77
|
+
if (fs.existsSync(envPath)) {
|
|
78
|
+
return envPath
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Look for shared .env in common monorepo locations.
|
|
86
|
+
*/
|
|
87
|
+
function findSharedEnv(directory) {
|
|
88
|
+
const patterns = [
|
|
89
|
+
'../shared/.env',
|
|
90
|
+
'../../shared/.env',
|
|
91
|
+
'../../packages/shared/.env',
|
|
92
|
+
]
|
|
93
|
+
for (const pattern of patterns) {
|
|
94
|
+
const sharedPath = path.resolve(directory, pattern)
|
|
95
|
+
if (fs.existsSync(sharedPath)) {
|
|
96
|
+
return sharedPath
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Load and merge config from .env files.
|
|
104
|
+
*/
|
|
105
|
+
function loadConfig(options, projectRoot) {
|
|
106
|
+
const configs = []
|
|
107
|
+
const sources = []
|
|
108
|
+
|
|
109
|
+
// Load inherited configs first (lowest priority)
|
|
110
|
+
if (options.extends) {
|
|
111
|
+
for (const extendPath of options.extends) {
|
|
112
|
+
const resolvedPath = path.isAbsolute(extendPath)
|
|
113
|
+
? extendPath
|
|
114
|
+
: path.resolve(projectRoot, extendPath)
|
|
115
|
+
|
|
116
|
+
if (fs.existsSync(resolvedPath)) {
|
|
117
|
+
configs.push(parseEnvFile(resolvedPath))
|
|
118
|
+
sources.push(resolvedPath)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
// Auto-detect shared env
|
|
123
|
+
const sharedEnv = findSharedEnv(projectRoot)
|
|
124
|
+
if (sharedEnv) {
|
|
125
|
+
configs.push(parseEnvFile(sharedEnv))
|
|
126
|
+
sources.push(sharedEnv)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Load main env file (highest priority)
|
|
131
|
+
let envPath = null
|
|
132
|
+
if (options.envPath) {
|
|
133
|
+
envPath = path.isAbsolute(options.envPath)
|
|
134
|
+
? options.envPath
|
|
135
|
+
: path.resolve(projectRoot, options.envPath)
|
|
136
|
+
} else {
|
|
137
|
+
envPath = findEnvFile(projectRoot)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (envPath && fs.existsSync(envPath)) {
|
|
141
|
+
configs.push(parseEnvFile(envPath))
|
|
142
|
+
sources.push(envPath)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Merge configs (later configs override earlier ones)
|
|
146
|
+
return {
|
|
147
|
+
config: Object.assign({}, ...configs),
|
|
148
|
+
sources
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validate required config keys and emit warnings/errors.
|
|
154
|
+
*/
|
|
155
|
+
function validateConfig(configValues, options, projectRoot) {
|
|
156
|
+
if (hasValidated) return true
|
|
157
|
+
hasValidated = true
|
|
158
|
+
|
|
159
|
+
const required = options.required || []
|
|
160
|
+
if (required.length === 0) return true
|
|
161
|
+
|
|
162
|
+
const missing = required.filter(key => !(key in configValues))
|
|
163
|
+
|
|
164
|
+
if (missing.length === 0) return true
|
|
165
|
+
|
|
166
|
+
const errorOnMissing = options.errorOnMissing ?? false
|
|
167
|
+
const message = `@idealyst/config: Missing required config keys: ${missing.join(', ')}`
|
|
168
|
+
|
|
169
|
+
if (errorOnMissing) {
|
|
170
|
+
console.error('\n\x1b[31m✖ ' + message + '\x1b[0m')
|
|
171
|
+
console.error('\x1b[90m Add these keys to your .env file or check your plugin configuration.\x1b[0m\n')
|
|
172
|
+
throw new Error(message)
|
|
173
|
+
} else {
|
|
174
|
+
console.warn('\n\x1b[33m⚠ ' + message + '\x1b[0m')
|
|
175
|
+
console.warn('\x1b[90m Add these keys to your .env file to suppress this warning.\x1b[0m')
|
|
176
|
+
console.warn('\x1b[90m Set errorOnMissing: true to fail the build instead.\x1b[0m\n')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Babel plugin that injects config values at compile time.
|
|
184
|
+
*/
|
|
185
|
+
module.exports = function babelPluginIdealystConfig(babel) {
|
|
186
|
+
const { types: t } = babel
|
|
187
|
+
|
|
188
|
+
// Cache config per project root
|
|
189
|
+
const configCache = new Map()
|
|
190
|
+
|
|
191
|
+
// Reset validation state on new compilation
|
|
192
|
+
hasValidated = false
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
name: '@idealyst/config',
|
|
196
|
+
|
|
197
|
+
visitor: {
|
|
198
|
+
Program: {
|
|
199
|
+
enter(programPath, state) {
|
|
200
|
+
const opts = state.opts || {}
|
|
201
|
+
const projectRoot = opts.root || state.cwd || process.cwd()
|
|
202
|
+
|
|
203
|
+
// Load config (cached per project root)
|
|
204
|
+
let configValues
|
|
205
|
+
if (!configCache.has(projectRoot)) {
|
|
206
|
+
const { config, sources } = loadConfig(opts, projectRoot)
|
|
207
|
+
configCache.set(projectRoot, config)
|
|
208
|
+
configValues = config
|
|
209
|
+
|
|
210
|
+
// Validate required keys (only once per build)
|
|
211
|
+
validateConfig(config, opts, projectRoot)
|
|
212
|
+
|
|
213
|
+
// Log loaded sources in verbose mode
|
|
214
|
+
if (opts.verbose && sources.length > 0) {
|
|
215
|
+
console.log('\x1b[36m@idealyst/config loaded:\x1b[0m')
|
|
216
|
+
sources.forEach(s => console.log(' ← ' + path.relative(projectRoot, s)))
|
|
217
|
+
console.log(' Keys:', Object.keys(config).join(', '))
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
configValues = configCache.get(projectRoot)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Track if this file imports from @idealyst/config
|
|
224
|
+
let hasConfigImport = false
|
|
225
|
+
let configImportPath = null
|
|
226
|
+
|
|
227
|
+
// Find imports from @idealyst/config
|
|
228
|
+
programPath.traverse({
|
|
229
|
+
ImportDeclaration(importPath) {
|
|
230
|
+
if (importPath.node.source.value === '@idealyst/config') {
|
|
231
|
+
hasConfigImport = true
|
|
232
|
+
configImportPath = importPath
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
if (!hasConfigImport || !configImportPath) {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if setConfig is already imported
|
|
242
|
+
let hasSetConfigImport = false
|
|
243
|
+
let setConfigLocalName = '__idealyst_setConfig'
|
|
244
|
+
|
|
245
|
+
for (const specifier of configImportPath.node.specifiers) {
|
|
246
|
+
if (t.isImportSpecifier(specifier) &&
|
|
247
|
+
t.isIdentifier(specifier.imported) &&
|
|
248
|
+
specifier.imported.name === 'setConfig') {
|
|
249
|
+
hasSetConfigImport = true
|
|
250
|
+
setConfigLocalName = specifier.local.name
|
|
251
|
+
break
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Add setConfig to imports if not already present
|
|
256
|
+
if (!hasSetConfigImport) {
|
|
257
|
+
configImportPath.node.specifiers.push(
|
|
258
|
+
t.importSpecifier(
|
|
259
|
+
t.identifier('__idealyst_setConfig'),
|
|
260
|
+
t.identifier('setConfig')
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Create the config object literal
|
|
266
|
+
const configProperties = Object.entries(configValues).map(([key, value]) =>
|
|
267
|
+
t.objectProperty(t.identifier(key), t.stringLiteral(value))
|
|
268
|
+
)
|
|
269
|
+
const configObject = t.objectExpression(configProperties)
|
|
270
|
+
|
|
271
|
+
// Create setConfig call
|
|
272
|
+
const setConfigCall = t.expressionStatement(
|
|
273
|
+
t.callExpression(
|
|
274
|
+
t.identifier(setConfigLocalName),
|
|
275
|
+
[configObject]
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
// Insert after the import statement
|
|
280
|
+
const importIndex = programPath.node.body.indexOf(configImportPath.node)
|
|
281
|
+
programPath.node.body.splice(importIndex + 1, 0, setConfigCall)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Export utilities for direct use
|
|
289
|
+
module.exports.parseEnvFile = parseEnvFile
|
|
290
|
+
module.exports.loadConfig = loadConfig
|
|
291
|
+
module.exports.findEnvFile = findEnvFile
|
|
292
|
+
module.exports.findSharedEnv = findSharedEnv
|
|
293
|
+
module.exports.validateConfig = validateConfig
|
package/src/config.native.ts
CHANGED
|
@@ -1,32 +1,63 @@
|
|
|
1
1
|
import type { IConfig } from './types'
|
|
2
2
|
import { ConfigValidationError } from './types'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Config store - initialized from react-native-config, can be overridden via setConfig().
|
|
6
|
+
*/
|
|
7
|
+
let configStore: Record<string, string> = {}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initialize from react-native-config if available.
|
|
11
|
+
*/
|
|
12
|
+
function initFromRNConfig(): void {
|
|
13
|
+
try {
|
|
14
|
+
// Dynamic import to handle cases where react-native-config is not installed
|
|
15
|
+
const RNConfig = require('react-native-config').default || require('react-native-config')
|
|
16
|
+
if (RNConfig && typeof RNConfig === 'object') {
|
|
17
|
+
configStore = { ...RNConfig }
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// react-native-config not available - configStore remains empty
|
|
21
|
+
// Values will be injected by Babel plugin via setConfig()
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
24
|
|
|
25
|
+
// Initialize on module load
|
|
26
|
+
initFromRNConfig()
|
|
27
|
+
|
|
17
28
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
* Set config values. Called by Babel plugin at compile time,
|
|
30
|
+
* or can be called manually.
|
|
31
|
+
*/
|
|
32
|
+
export function setConfig(config: Record<string, string>): void {
|
|
33
|
+
configStore = { ...configStore, ...config }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Clear all config values. Useful for testing.
|
|
38
|
+
*/
|
|
39
|
+
export function clearConfig(): void {
|
|
40
|
+
configStore = {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the raw config store. Useful for debugging.
|
|
45
|
+
*/
|
|
46
|
+
export function getConfigStore(): Record<string, string> {
|
|
47
|
+
return { ...configStore }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Native implementation of IConfig.
|
|
22
52
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
53
|
+
* Config values come from:
|
|
54
|
+
* 1. react-native-config (if installed)
|
|
55
|
+
* 2. Babel plugin injection via setConfig()
|
|
56
|
+
* 3. Manual setConfig() calls
|
|
26
57
|
*/
|
|
27
58
|
class NativeConfig implements IConfig {
|
|
28
59
|
get(key: string, defaultValue?: string): string | undefined {
|
|
29
|
-
return
|
|
60
|
+
return configStore[key] ?? defaultValue
|
|
30
61
|
}
|
|
31
62
|
|
|
32
63
|
getRequired(key: string): string {
|
|
@@ -41,11 +72,11 @@ class NativeConfig implements IConfig {
|
|
|
41
72
|
}
|
|
42
73
|
|
|
43
74
|
has(key: string): boolean {
|
|
44
|
-
return
|
|
75
|
+
return configStore[key] !== undefined
|
|
45
76
|
}
|
|
46
77
|
|
|
47
78
|
keys(): string[] {
|
|
48
|
-
return Object.keys(
|
|
79
|
+
return Object.keys(configStore).sort()
|
|
49
80
|
}
|
|
50
81
|
|
|
51
82
|
validate(requiredKeys: string[]): void {
|
package/src/index.native.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Native entry point for @idealyst/config
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Config values come from:
|
|
5
|
+
* 1. react-native-config (if installed)
|
|
6
|
+
* 2. Babel plugin injection via setConfig()
|
|
6
7
|
*
|
|
7
8
|
* @example
|
|
8
9
|
* ```typescript
|
|
@@ -13,11 +14,11 @@
|
|
|
13
14
|
* ```
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
import NativeConfig from './config.native'
|
|
17
|
+
import NativeConfig, { setConfig, clearConfig, getConfigStore } from './config.native'
|
|
17
18
|
|
|
18
19
|
// Create singleton instance for native
|
|
19
20
|
const config = new NativeConfig()
|
|
20
21
|
|
|
21
22
|
export default config
|
|
22
|
-
export { config, config as Config, NativeConfig }
|
|
23
|
+
export { config, config as Config, NativeConfig, setConfig, clearConfig, getConfigStore }
|
|
23
24
|
export * from './types'
|