@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.
Files changed (3) hide show
  1. package/README.md +114 -91
  2. package/package.json +6 -1
  3. 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. Generate config
58
-
59
- ```bash
60
- # In packages/web/
61
- npx idealyst-config generate --extends ../shared/.env --env .env
62
-
63
- # In packages/mobile/
64
- npx idealyst-config generate --extends ../shared/.env --env .env
65
- ```
66
-
67
- This creates `src/config.generated.ts` with merged values:
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. Initialize and use
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
- ## CLI Reference
82
+ That's it! The Babel plugin reads your .env files at compile time and injects the values automatically.
97
83
 
98
- ```bash
99
- idealyst-config generate [options]
84
+ ## Babel Plugin Options
100
85
 
101
- Options:
102
- --env <path> Path to .env file (default: auto-detect)
103
- --extends <path> Inherit from another .env (can use multiple times)
104
- --output <path> Output path (default: src/config.generated.ts)
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
- ### Examples
91
+ // Main .env file (highest priority, default: auto-detect)
92
+ envPath: '.env',
110
93
 
111
- ```bash
112
- # Simple - auto-detect .env
113
- idealyst-config generate
94
+ // Project root (default: process.cwd())
95
+ root: '/path/to/project'
96
+ }]
97
+ ```
114
98
 
115
- # With shared inheritance
116
- idealyst-config generate --extends ../shared/.env --env .env
99
+ ### Auto-detection
117
100
 
118
- # Multiple inheritance (lowest to highest priority)
119
- idealyst-config generate \
120
- --extends ../../shared/.env \
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
- # Types only (for CI/type checking without values)
125
- idealyst-config generate --types-only --output src/env.d.ts
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. --extends ../shared/.env (lowest priority)
134
- 2. --extends ../.env.common
135
- 3. --env .env (highest priority)
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
- ### `setConfig(config: Record<string, string>)`
184
-
185
- Initialize config values (call once at app startup).
207
+ ## Type Generation (Optional)
186
208
 
187
- ```typescript
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
- npm install react-native-config
199
- cd ios && pod install
212
+ npx idealyst-config generate --extends ../shared/.env --env .env --types-only
200
213
  ```
201
214
 
202
- 2. Follow [react-native-config setup](https://github.com/luggit/react-native-config#setup)
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
- This provides autocomplete and catches typos at compile time.
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. **Generate before build** - Add to prebuild script
242
- 2. **Gitignore generated files** - Add `config.generated.ts` to `.gitignore`
243
- 3. **Commit .env.example** - Document required keys without values
244
- 4. **Validate at startup** - Use `config.validate()` early
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.13",
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