@idealyst/config 1.2.12 → 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 CHANGED
@@ -1,14 +1,14 @@
1
1
  # @idealyst/config
2
2
 
3
- Cross-platform configuration and environment variable support for React and React Native applications.
3
+ Cross-platform configuration for React and React Native with env inheritance support.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - **Single API** - Same code works on web and native
8
- - **Type-safe** - Auto-generated TypeScript declarations for autocomplete
9
- - **Prefix abstraction** - Use canonical names (`API_URL`), web implementation handles `VITE_` prefix internally
10
- - **Validation** - Check required config at app startup
11
- - **Zero runtime dependencies** - Uses platform-native solutions
8
+ - **Env inheritance** - Shared config with platform-specific overrides
9
+ - **Babel plugin** - Config injected at build time, no runtime overhead
10
+ - **Type-safe** - Auto-generated TypeScript declarations
11
+ - **Monorepo friendly** - Designed for shared/web/mobile patterns
12
12
 
13
13
  ## Installation
14
14
 
@@ -22,89 +22,148 @@ cd ios && pod install
22
22
 
23
23
  ## Quick Start
24
24
 
25
- ```typescript
26
- import { config } from '@idealyst/config'
27
-
28
- // Get a config value
29
- const apiUrl = config.get('API_URL')
30
-
31
- // Get with default value
32
- const port = config.get('PORT', '3000')
33
-
34
- // Get required value (throws if missing)
35
- const secret = config.getRequired('JWT_SECRET')
25
+ ### 1. Create your .env files
36
26
 
37
- // Validate required vars at startup
38
- config.validate(['API_URL', 'AUTH_SECRET'])
27
+ ```
28
+ my-app/
29
+ ├── packages/
30
+ │ ├── shared/
31
+ │ │ └── .env # Base config (lowest priority)
32
+ │ ├── web/
33
+ │ │ └── .env # Web overrides
34
+ │ └── mobile/
35
+ │ └── .env # Mobile overrides
39
36
  ```
40
37
 
41
- ## Environment Files
42
-
43
- ### React Native (.env)
44
-
38
+ **shared/.env:**
45
39
  ```bash
46
- # No prefix needed
47
40
  API_URL=https://api.example.com
48
41
  GOOGLE_CLIENT_ID=abc123
49
- JWT_SECRET=supersecret
42
+ ANALYTICS_ENABLED=true
50
43
  ```
51
44
 
52
- ### Vite Web (.env)
45
+ **web/.env:**
46
+ ```bash
47
+ # Override API for web
48
+ API_URL=https://web-api.example.com
49
+ ```
53
50
 
51
+ **mobile/.env:**
54
52
  ```bash
55
- # Must use VITE_ prefix for client exposure
56
- VITE_API_URL=https://api.example.com
57
- VITE_GOOGLE_CLIENT_ID=abc123
58
- VITE_JWT_SECRET=supersecret
53
+ # Mobile uses shared API_URL, but different analytics
54
+ ANALYTICS_ENABLED=false
59
55
  ```
60
56
 
61
- **Important:** Your code always uses canonical names without the `VITE_` prefix. The web implementation handles this internally:
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
+ }
70
+ ```
71
+
72
+ ### 3. Use in your app
62
73
 
63
74
  ```typescript
64
- // Both platforms - same code
75
+ import { config } from '@idealyst/config'
76
+
77
+ // Values are injected at build time!
65
78
  const apiUrl = config.get('API_URL')
79
+ const analyticsEnabled = config.get('ANALYTICS_ENABLED') === 'true'
66
80
  ```
67
81
 
68
- ## Type Generation
82
+ That's it! The Babel plugin reads your .env files at compile time and injects the values automatically.
69
83
 
70
- Generate TypeScript declarations for autocomplete support:
84
+ ## Babel Plugin Options
71
85
 
72
- ```bash
73
- # Auto-detect .env file
74
- npx idealyst-config generate
86
+ ```js
87
+ ['@idealyst/config/plugin', {
88
+ // Inherit from these .env files (lowest to highest priority)
89
+ extends: ['../shared/.env', '../common/.env'],
75
90
 
76
- # Specify .env file
77
- npx idealyst-config generate --env .env.local
91
+ // Main .env file (highest priority, default: auto-detect)
92
+ envPath: '.env',
78
93
 
79
- # Custom output path
80
- npx idealyst-config generate --output types/env.d.ts
94
+ // Project root (default: process.cwd())
95
+ root: '/path/to/project'
96
+ }]
81
97
  ```
82
98
 
83
- This creates a declaration file that provides autocomplete for your config keys:
99
+ ### Auto-detection
84
100
 
85
- ```typescript
86
- // Generated: src/env.d.ts
87
- declare module '@idealyst/config' {
88
- interface ConfigKeys {
89
- API_URL: string
90
- GOOGLE_CLIENT_ID: string
91
- JWT_SECRET: string
92
- }
93
- }
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
104
+
105
+ ```js
106
+ // Minimal config - auto-detects everything
107
+ plugins: [
108
+ '@idealyst/config/plugin'
109
+ ]
110
+ ```
111
+
112
+ ## Inheritance Priority
113
+
114
+ Configs are merged in order, with later files overriding earlier ones:
115
+
116
+ ```
117
+ 1. extends[0]: ../shared/.env (lowest priority)
118
+ 2. extends[1]: ../common/.env
119
+ 3. envPath: .env (highest priority)
94
120
  ```
95
121
 
96
- Now you get autocomplete when calling `config.get()`:
122
+ ## Vite Setup
123
+
124
+ For Vite projects, add to `vite.config.ts`:
97
125
 
98
126
  ```typescript
99
- config.get('API_URL') // Autocomplete shows available keys
100
- config.get('INVALID') // TypeScript error - key not in ConfigKeys
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
+ }
101
160
  ```
102
161
 
103
162
  ## API Reference
104
163
 
105
164
  ### `config.get(key: string): string | undefined`
106
165
 
107
- Get a configuration value by key.
166
+ Get a config value.
108
167
 
109
168
  ```typescript
110
169
  const apiUrl = config.get('API_URL')
@@ -112,7 +171,7 @@ const apiUrl = config.get('API_URL')
112
171
 
113
172
  ### `config.get(key: string, defaultValue: string): string`
114
173
 
115
- Get a configuration value with a fallback default.
174
+ Get with fallback default.
116
175
 
117
176
  ```typescript
118
177
  const port = config.get('PORT', '3000')
@@ -120,16 +179,15 @@ const port = config.get('PORT', '3000')
120
179
 
121
180
  ### `config.getRequired(key: string): string`
122
181
 
123
- Get a required configuration value. Throws an error if not defined.
182
+ Get required value. Throws if not defined.
124
183
 
125
184
  ```typescript
126
185
  const secret = config.getRequired('JWT_SECRET')
127
- // Throws: 'Required config key "JWT_SECRET" is not defined'
128
186
  ```
129
187
 
130
188
  ### `config.has(key: string): boolean`
131
189
 
132
- Check if a configuration key exists.
190
+ Check if key exists.
133
191
 
134
192
  ```typescript
135
193
  if (config.has('DEBUG')) {
@@ -137,54 +195,76 @@ if (config.has('DEBUG')) {
137
195
  }
138
196
  ```
139
197
 
140
- ### `config.keys(): string[]`
198
+ ### `config.validate(requiredKeys: string[]): void`
141
199
 
142
- Get all available configuration keys.
200
+ Validate required keys at startup.
143
201
 
144
202
  ```typescript
145
- console.log('Available config:', config.keys())
203
+ config.validate(['API_URL', 'AUTH_SECRET'])
204
+ // Throws ConfigValidationError if any are missing
146
205
  ```
147
206
 
148
- ### `config.validate(requiredKeys: string[]): void`
207
+ ## Type Generation (Optional)
208
+
209
+ For TypeScript autocomplete, generate type declarations:
149
210
 
150
- Validate that all required keys are present. Throws `ConfigValidationError` if any are missing.
211
+ ```bash
212
+ npx idealyst-config generate --extends ../shared/.env --env .env --types-only
213
+ ```
214
+
215
+ This creates `src/config.generated.d.ts`:
151
216
 
152
217
  ```typescript
153
- // At app startup
154
- try {
155
- config.validate(['API_URL', 'AUTH_SECRET', 'DATABASE_URL'])
156
- } catch (error) {
157
- if (error instanceof ConfigValidationError) {
158
- console.error('Missing config:', error.missingKeys)
218
+ declare module '@idealyst/config' {
219
+ interface ConfigKeys {
220
+ API_URL: string
221
+ GOOGLE_CLIENT_ID: string
222
+ ANALYTICS_ENABLED: string
159
223
  }
160
224
  }
161
225
  ```
162
226
 
163
- ## Platform Implementation Details
227
+ Add to your build script for automatic updates:
164
228
 
165
- ### Web (Vite)
229
+ ```json
230
+ {
231
+ "scripts": {
232
+ "prebuild": "idealyst-config generate --extends ../shared/.env --types-only"
233
+ }
234
+ }
235
+ ```
166
236
 
167
- Uses `import.meta.env` with automatic `VITE_` prefix handling:
168
- - Your code: `config.get('API_URL')`
169
- - Internal lookup: `import.meta.env.VITE_API_URL`
237
+ ## How It Works
170
238
 
171
- ### React Native
239
+ The Babel plugin transforms your code at compile time:
172
240
 
173
- Uses `react-native-config` for native environment variable injection:
174
- - Your code: `config.get('API_URL')`
175
- - Internal lookup: `Config.API_URL`
241
+ **Input:**
242
+ ```typescript
243
+ import { config } from '@idealyst/config'
176
244
 
177
- Make sure to follow [react-native-config setup](https://github.com/luggit/react-native-config#setup) for your platform.
245
+ const apiUrl = config.get('API_URL')
246
+ ```
178
247
 
179
- ## Best Practices
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" })
180
252
 
181
- 1. **Generate types after .env changes** - Run `idealyst-config generate` whenever you add/remove environment variables
253
+ const apiUrl = config.get('API_URL')
254
+ ```
182
255
 
183
- 2. **Validate at startup** - Call `config.validate()` early in your app to catch missing config
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
184
261
 
185
- 3. **Use .env.example** - Commit an example file with all required keys (no values)
262
+ ## Best Practices
186
263
 
187
- 4. **Don't commit secrets** - Add `.env` and `.env.local` to `.gitignore`
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
188
268
 
189
269
  ## License
190
270
 
@@ -1,60 +1,93 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * CLI for @idealyst/config - Generate TypeScript types from .env files
4
+ * CLI for @idealyst/config - Generate config from .env files with inheritance
5
5
  *
6
- * This is a self-contained JavaScript CLI that doesn't require TypeScript
7
- * compilation at runtime. It implements the same logic as src/cli/generate.ts.
6
+ * Supports monorepo patterns with shared config:
7
+ * shared/.env → base config (lowest priority)
8
+ * web/.env → web overrides
9
+ * mobile/.env → mobile overrides
8
10
  */
9
11
 
10
12
  const fs = require('fs')
11
13
  const path = require('path')
12
14
 
13
15
  /**
14
- * Parse a .env file and extract all key names.
15
- * Strips VITE_ prefix to normalize to canonical names.
16
+ * Parse a .env file and extract key-value pairs.
16
17
  */
17
- function parseEnvFile(content) {
18
- const keys = []
18
+ function parseEnvFile(filePath) {
19
+ if (!fs.existsSync(filePath)) {
20
+ return {}
21
+ }
22
+
23
+ const content = fs.readFileSync(filePath, 'utf-8')
24
+ const config = {}
19
25
 
20
26
  for (const line of content.split('\n')) {
21
27
  const trimmed = line.trim()
22
28
 
23
- // Skip empty lines and comments
24
29
  if (!trimmed || trimmed.startsWith('#')) {
25
30
  continue
26
31
  }
27
32
 
28
- // Extract key name (everything before the first =)
29
33
  const equalsIndex = trimmed.indexOf('=')
30
34
  if (equalsIndex === -1) {
31
35
  continue
32
36
  }
33
37
 
34
38
  let key = trimmed.substring(0, equalsIndex).trim()
39
+ let value = trimmed.substring(equalsIndex + 1).trim()
40
+
41
+ // Remove quotes if present
42
+ if ((value.startsWith('"') && value.endsWith('"')) ||
43
+ (value.startsWith("'") && value.endsWith("'"))) {
44
+ value = value.slice(1, -1)
45
+ }
35
46
 
36
- // Strip VITE_ prefix to normalize to canonical names
47
+ // Strip VITE_ prefix to normalize
37
48
  if (key.startsWith('VITE_')) {
38
49
  key = key.substring(5)
39
50
  }
40
51
 
41
- // Only add unique keys
42
- if (key && !keys.includes(key)) {
43
- keys.push(key)
44
- }
52
+ config[key] = value
45
53
  }
46
54
 
47
- return keys.sort()
55
+ return config
48
56
  }
49
57
 
50
58
  /**
51
- * Generate TypeScript declaration content from a list of config keys.
59
+ * Generate TypeScript config module with actual values.
52
60
  */
53
- function generateDeclaration(keys, sourceFile) {
61
+ function generateConfigModule(config, sourceFiles) {
62
+ const sources = sourceFiles.join(', ')
63
+ const entries = Object.entries(config)
64
+ .sort(([a], [b]) => a.localeCompare(b))
65
+ .map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`)
66
+ .join(',\n')
67
+
68
+ return `// Auto-generated by @idealyst/config - DO NOT EDIT
69
+ // Sources: ${sources}
70
+ // Run \`idealyst-config generate\` to regenerate
71
+
72
+ /**
73
+ * Generated configuration values.
74
+ * Merged from: ${sources}
75
+ */
76
+ export const generatedConfig: Record<string, string> = {
77
+ ${entries}
78
+ }
79
+ `
80
+ }
81
+
82
+ /**
83
+ * Generate TypeScript declaration file.
84
+ */
85
+ function generateDeclaration(keys, sourceFiles) {
54
86
  const keyDefinitions = keys.map(k => ` ${k}: string`).join('\n')
87
+ const sources = sourceFiles.join(', ')
55
88
 
56
89
  return `// Auto-generated by @idealyst/config - DO NOT EDIT
57
- // Generated from: ${sourceFile}
90
+ // Sources: ${sources}
58
91
  // Run \`idealyst-config generate\` to regenerate
59
92
 
60
93
  declare module '@idealyst/config' {
@@ -76,42 +109,78 @@ export {}
76
109
  }
77
110
 
78
111
  /**
79
- * Find the most appropriate .env file in a directory.
112
+ * Find .env file in directory.
80
113
  */
81
114
  function findEnvFile(directory) {
82
115
  const candidates = ['.env.local', '.env.development', '.env']
83
-
84
116
  for (const candidate of candidates) {
85
117
  const envPath = path.join(directory, candidate)
86
118
  if (fs.existsSync(envPath)) {
87
119
  return envPath
88
120
  }
89
121
  }
122
+ return null
123
+ }
90
124
 
125
+ /**
126
+ * Look for shared .env in common monorepo locations.
127
+ */
128
+ function findSharedEnv(directory) {
129
+ const patterns = [
130
+ '../shared/.env',
131
+ '../../shared/.env',
132
+ '../../packages/shared/.env',
133
+ '../.env.shared',
134
+ ]
135
+ for (const pattern of patterns) {
136
+ const sharedPath = path.resolve(directory, pattern)
137
+ if (fs.existsSync(sharedPath)) {
138
+ return sharedPath
139
+ }
140
+ }
91
141
  return null
92
142
  }
93
143
 
94
144
  function printUsage() {
95
145
  console.log(`
96
- @idealyst/config - Generate TypeScript types from .env files
146
+ @idealyst/config - Generate config from .env files with inheritance
97
147
 
98
148
  Usage:
99
149
  idealyst-config generate [options]
100
150
 
101
151
  Options:
102
- --env <path> Path to .env file (default: auto-detect)
103
- --output <path> Output path for .d.ts file (default: src/env.d.ts)
104
- --help Show this help message
152
+ --env <path> Path to .env file (default: auto-detect)
153
+ --extends <path> Inherit from another .env file (can use multiple times)
154
+ --output <path> Output path for generated config (default: src/config.generated.ts)
155
+ --types-only Generate only .d.ts file, no values
156
+ --help Show this help message
105
157
 
106
158
  Examples:
159
+ # Simple usage - auto-detect .env
107
160
  idealyst-config generate
108
- idealyst-config generate --env .env.local
109
- idealyst-config generate --env .env --output types/env.d.ts
161
+
162
+ # With shared config inheritance
163
+ idealyst-config generate --extends ../shared/.env --env .env
164
+
165
+ # Multiple inheritance (lowest to highest priority)
166
+ idealyst-config generate --extends ../../shared/.env --extends ../.env.common --env .env
167
+
168
+ # Types only (for type checking without exposing values)
169
+ idealyst-config generate --types-only --output src/env.d.ts
170
+
171
+ Inheritance:
172
+ In a monorepo with shared/web/mobile packages, you can set up inheritance:
173
+
174
+ shared/.env → API_URL=https://api.example.com
175
+ web/.env → API_URL=https://web-api.example.com (overrides shared)
176
+ mobile/.env → (inherits API_URL from shared)
177
+
178
+ The --extends flag loads configs in order, with later files taking priority.
110
179
  `)
111
180
  }
112
181
 
113
182
  function parseArgs(args) {
114
- const result = {}
183
+ const result = { extends: [] }
115
184
 
116
185
  for (let i = 0; i < args.length; i++) {
117
186
  const arg = args[i]
@@ -120,8 +189,12 @@ function parseArgs(args) {
120
189
  result.help = true
121
190
  } else if (arg === '--env' && args[i + 1]) {
122
191
  result.env = args[++i]
192
+ } else if (arg === '--extends' && args[i + 1]) {
193
+ result.extends.push(args[++i])
123
194
  } else if (arg === '--output' && args[i + 1]) {
124
195
  result.output = args[++i]
196
+ } else if (arg === '--types-only') {
197
+ result.typesOnly = true
125
198
  } else if (!arg.startsWith('-') && !result.command) {
126
199
  result.command = arg
127
200
  }
@@ -145,59 +218,91 @@ function main() {
145
218
  }
146
219
 
147
220
  const cwd = process.cwd()
221
+ const sourceFiles = []
222
+ const configs = []
223
+
224
+ // Load inherited configs first (lowest priority)
225
+ for (const extendPath of args.extends) {
226
+ const resolvedPath = path.isAbsolute(extendPath)
227
+ ? extendPath
228
+ : path.resolve(cwd, extendPath)
229
+
230
+ if (fs.existsSync(resolvedPath)) {
231
+ configs.push(parseEnvFile(resolvedPath))
232
+ sourceFiles.push(path.relative(cwd, resolvedPath))
233
+ console.log(` ← ${path.relative(cwd, resolvedPath)}`)
234
+ } else {
235
+ console.warn(`Warning: Inherited env file not found: ${resolvedPath}`)
236
+ }
237
+ }
148
238
 
149
- // Find or use the specified .env file
239
+ // Auto-detect shared env if no extends specified
240
+ if (args.extends.length === 0) {
241
+ const sharedEnv = findSharedEnv(cwd)
242
+ if (sharedEnv) {
243
+ configs.push(parseEnvFile(sharedEnv))
244
+ sourceFiles.push(path.relative(cwd, sharedEnv))
245
+ console.log(` ← ${path.relative(cwd, sharedEnv)} (auto-detected shared)`)
246
+ }
247
+ }
248
+
249
+ // Load main env file (highest priority)
150
250
  let envPath
151
251
  if (args.env) {
152
- envPath = path.isAbsolute(args.env) ? args.env : path.join(cwd, args.env)
252
+ envPath = path.isAbsolute(args.env) ? args.env : path.resolve(cwd, args.env)
153
253
  } else {
154
254
  envPath = findEnvFile(cwd)
155
- if (!envPath) {
156
- console.error('Error: No .env file found in current directory.')
157
- console.error('Create a .env file or specify one with --env <path>')
158
- process.exit(1)
159
- }
160
255
  }
161
256
 
162
- // Check if env file exists
163
- if (!fs.existsSync(envPath)) {
164
- console.error(`Error: Environment file not found: ${envPath}`)
257
+ if (envPath && fs.existsSync(envPath)) {
258
+ configs.push(parseEnvFile(envPath))
259
+ sourceFiles.push(path.relative(cwd, envPath))
260
+ console.log(` ← ${path.relative(cwd, envPath)}`)
261
+ } else if (args.env) {
262
+ console.error(`Error: Environment file not found: ${args.env}`)
165
263
  process.exit(1)
166
264
  }
167
265
 
168
- // Determine output path
169
- const outputPath = args.output
170
- ? (path.isAbsolute(args.output) ? args.output : path.join(cwd, args.output))
171
- : path.join(cwd, 'src', 'env.d.ts')
172
-
173
- try {
174
- // Read and parse env file
175
- const envContent = fs.readFileSync(envPath, 'utf-8')
176
- const keys = parseEnvFile(envContent)
266
+ if (configs.length === 0) {
267
+ console.error('Error: No .env files found.')
268
+ console.error('Create a .env file or specify one with --env <path>')
269
+ process.exit(1)
270
+ }
177
271
 
178
- if (keys.length === 0) {
179
- console.warn('Warning: No environment variables found in', envPath)
180
- }
272
+ // Merge configs (later configs override earlier ones)
273
+ const mergedConfig = Object.assign({}, ...configs)
274
+ const keys = Object.keys(mergedConfig).sort()
181
275
 
182
- // Generate declaration
183
- const declaration = generateDeclaration(keys, path.basename(envPath))
276
+ // Determine output path
277
+ const outputPath = args.output
278
+ ? (path.isAbsolute(args.output) ? args.output : path.resolve(cwd, args.output))
279
+ : path.resolve(cwd, 'src', 'config.generated.ts')
184
280
 
185
- // Ensure output directory exists
186
- const outputDir = path.dirname(outputPath)
187
- if (!fs.existsSync(outputDir)) {
188
- fs.mkdirSync(outputDir, { recursive: true })
189
- }
281
+ // Ensure output directory exists
282
+ const outputDir = path.dirname(outputPath)
283
+ if (!fs.existsSync(outputDir)) {
284
+ fs.mkdirSync(outputDir, { recursive: true })
285
+ }
190
286
 
191
- // Write declaration file
287
+ if (args.typesOnly) {
288
+ // Generate declaration file only
289
+ const declaration = generateDeclaration(keys, sourceFiles)
192
290
  fs.writeFileSync(outputPath, declaration)
193
-
194
- console.log(`Generated config types at: ${outputPath}`)
195
- console.log(`Source: ${envPath}`)
196
- console.log(`Keys: ${keys.join(', ')}`)
197
- } catch (error) {
198
- console.error('Error generating config types:', error.message)
199
- process.exit(1)
291
+ console.log(`\nGenerated types at: ${path.relative(cwd, outputPath)}`)
292
+ } else {
293
+ // Generate config module
294
+ const module = generateConfigModule(mergedConfig, sourceFiles)
295
+ fs.writeFileSync(outputPath, module)
296
+ console.log(`\nGenerated config at: ${path.relative(cwd, outputPath)}`)
297
+
298
+ // Also generate declaration file
299
+ const declPath = outputPath.replace(/\.ts$/, '.d.ts')
300
+ const declaration = generateDeclaration(keys, sourceFiles)
301
+ fs.writeFileSync(declPath, declaration)
302
+ console.log(`Generated types at: ${path.relative(cwd, declPath)}`)
200
303
  }
304
+
305
+ console.log(`Keys: ${keys.join(', ')}`)
201
306
  }
202
307
 
203
308
  main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/config",
3
- "version": "1.2.12",
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
@@ -3,22 +3,38 @@ import path from 'path'
3
3
 
4
4
  export interface GenerateOptions {
5
5
  /**
6
- * Path to the .env file to read
6
+ * Path to the platform-specific .env file (highest priority)
7
7
  */
8
- envPath: string
8
+ envPath?: string
9
9
 
10
10
  /**
11
- * Path to write the generated TypeScript declaration file
11
+ * Paths to inherited .env files (lowest to highest priority)
12
+ * e.g., ['../shared/.env'] - shared is loaded first, then envPath overrides
13
+ */
14
+ inheritFrom?: string[]
15
+
16
+ /**
17
+ * Path to write the generated TypeScript config file
12
18
  */
13
19
  outputPath: string
20
+
21
+ /**
22
+ * Whether to generate types only (declaration file) or full config module
23
+ */
24
+ typesOnly?: boolean
14
25
  }
15
26
 
16
27
  /**
17
- * Parse a .env file and extract all key names.
28
+ * Parse a .env file and extract key-value pairs.
18
29
  * Strips VITE_ prefix to normalize to canonical names.
19
30
  */
20
- export function parseEnvFile(content: string): string[] {
21
- const keys: string[] = []
31
+ export function parseEnvFile(filePath: string): Record<string, string> {
32
+ if (!fs.existsSync(filePath)) {
33
+ return {}
34
+ }
35
+
36
+ const content = fs.readFileSync(filePath, 'utf-8')
37
+ const config: Record<string, string> = {}
22
38
 
23
39
  for (const line of content.split('\n')) {
24
40
  const trimmed = line.trim()
@@ -28,36 +44,48 @@ export function parseEnvFile(content: string): string[] {
28
44
  continue
29
45
  }
30
46
 
31
- // Extract key name (everything before the first =)
47
+ // Extract key=value
32
48
  const equalsIndex = trimmed.indexOf('=')
33
49
  if (equalsIndex === -1) {
34
50
  continue
35
51
  }
36
52
 
37
53
  let key = trimmed.substring(0, equalsIndex).trim()
54
+ let value = trimmed.substring(equalsIndex + 1).trim()
55
+
56
+ // Remove quotes if present
57
+ if ((value.startsWith('"') && value.endsWith('"')) ||
58
+ (value.startsWith("'") && value.endsWith("'"))) {
59
+ value = value.slice(1, -1)
60
+ }
38
61
 
39
62
  // Strip VITE_ prefix to normalize to canonical names
40
63
  if (key.startsWith('VITE_')) {
41
64
  key = key.substring(5)
42
65
  }
43
66
 
44
- // Only add unique keys
45
- if (key && !keys.includes(key)) {
46
- keys.push(key)
47
- }
67
+ config[key] = value
48
68
  }
49
69
 
50
- return keys.sort()
70
+ return config
51
71
  }
52
72
 
53
73
  /**
54
- * Generate TypeScript declaration content from a list of config keys.
74
+ * Merge multiple env configs with later configs taking priority.
55
75
  */
56
- export function generateDeclaration(keys: string[], sourceFile: string): string {
76
+ export function mergeEnvConfigs(...configs: Record<string, string>[]): Record<string, string> {
77
+ return Object.assign({}, ...configs)
78
+ }
79
+
80
+ /**
81
+ * Generate TypeScript declaration content from config keys.
82
+ */
83
+ export function generateDeclaration(keys: string[], sourceFiles: string[]): string {
57
84
  const keyDefinitions = keys.map(k => ` ${k}: string`).join('\n')
85
+ const sources = sourceFiles.join(', ')
58
86
 
59
87
  return `// Auto-generated by @idealyst/config - DO NOT EDIT
60
- // Generated from: ${sourceFile}
88
+ // Sources: ${sources}
61
89
  // Run \`idealyst-config generate\` to regenerate
62
90
 
63
91
  declare module '@idealyst/config' {
@@ -79,42 +107,98 @@ export {}
79
107
  }
80
108
 
81
109
  /**
82
- * Generate TypeScript config types from an .env file.
83
- *
84
- * @param options - Generation options
85
- * @returns The path to the generated file
110
+ * Generate a TypeScript config module with actual values.
86
111
  */
87
- export function generateConfigTypes(options: GenerateOptions): string {
88
- // Read the .env file
89
- if (!fs.existsSync(options.envPath)) {
90
- throw new Error(`Environment file not found: ${options.envPath}`)
112
+ export function generateConfigModule(config: Record<string, string>, sourceFiles: string[]): string {
113
+ const sources = sourceFiles.join(', ')
114
+ const entries = Object.entries(config)
115
+ .sort(([a], [b]) => a.localeCompare(b))
116
+ .map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`)
117
+ .join(',\n')
118
+
119
+ return `// Auto-generated by @idealyst/config - DO NOT EDIT
120
+ // Sources: ${sources}
121
+ // Run \`idealyst-config generate\` to regenerate
122
+
123
+ /**
124
+ * Generated configuration values.
125
+ * Merged from: ${sources}
126
+ */
127
+ export const generatedConfig: Record<string, string> = {
128
+ ${entries}
129
+ }
130
+ `
131
+ }
132
+
133
+ /**
134
+ * Generate config from .env files with inheritance support.
135
+ */
136
+ export function generateConfigTypes(options: GenerateOptions): { outputPath: string; keys: string[] } {
137
+ const sourceFiles: string[] = []
138
+ const configs: Record<string, string>[] = []
139
+
140
+ // Load inherited configs first (lowest priority)
141
+ if (options.inheritFrom) {
142
+ for (const inheritPath of options.inheritFrom) {
143
+ const resolvedPath = path.isAbsolute(inheritPath)
144
+ ? inheritPath
145
+ : path.resolve(path.dirname(options.outputPath), inheritPath)
146
+
147
+ if (fs.existsSync(resolvedPath)) {
148
+ configs.push(parseEnvFile(resolvedPath))
149
+ sourceFiles.push(path.basename(resolvedPath))
150
+ }
151
+ }
91
152
  }
92
153
 
93
- const envContent = fs.readFileSync(options.envPath, 'utf-8')
94
- const keys = parseEnvFile(envContent)
154
+ // Load main env file (highest priority)
155
+ if (options.envPath) {
156
+ const resolvedEnvPath = path.isAbsolute(options.envPath)
157
+ ? options.envPath
158
+ : path.resolve(process.cwd(), options.envPath)
95
159
 
96
- if (keys.length === 0) {
97
- console.warn('Warning: No environment variables found in', options.envPath)
160
+ if (fs.existsSync(resolvedEnvPath)) {
161
+ configs.push(parseEnvFile(resolvedEnvPath))
162
+ sourceFiles.push(path.basename(resolvedEnvPath))
163
+ }
98
164
  }
99
165
 
100
- // Generate the declaration content
101
- const declaration = generateDeclaration(keys, path.basename(options.envPath))
166
+ // Merge configs
167
+ const mergedConfig = mergeEnvConfigs(...configs)
168
+ const keys = Object.keys(mergedConfig).sort()
169
+
170
+ if (keys.length === 0) {
171
+ console.warn('Warning: No environment variables found')
172
+ }
102
173
 
103
- // Ensure the output directory exists
174
+ // Ensure output directory exists
104
175
  const outputDir = path.dirname(options.outputPath)
105
176
  if (!fs.existsSync(outputDir)) {
106
177
  fs.mkdirSync(outputDir, { recursive: true })
107
178
  }
108
179
 
109
- // Write the declaration file
110
- fs.writeFileSync(options.outputPath, declaration)
180
+ if (options.typesOnly) {
181
+ // Generate declaration file only
182
+ const declaration = generateDeclaration(keys, sourceFiles)
183
+ fs.writeFileSync(options.outputPath, declaration)
184
+ } else {
185
+ // Generate full config module
186
+ const module = generateConfigModule(mergedConfig, sourceFiles)
187
+ fs.writeFileSync(options.outputPath, module)
188
+
189
+ // Also generate declaration file alongside
190
+ const declPath = options.outputPath.replace(/\.ts$/, '.d.ts')
191
+ if (declPath !== options.outputPath) {
192
+ const declaration = generateDeclaration(keys, sourceFiles)
193
+ fs.writeFileSync(declPath, declaration)
194
+ }
195
+ }
111
196
 
112
- return options.outputPath
197
+ return { outputPath: options.outputPath, keys }
113
198
  }
114
199
 
115
200
  /**
116
201
  * Find the most appropriate .env file in a directory.
117
- * Prefers .env.local > .env.development > .env
118
202
  */
119
203
  export function findEnvFile(directory: string): string | null {
120
204
  const candidates = ['.env.local', '.env.development', '.env']
@@ -128,3 +212,25 @@ export function findEnvFile(directory: string): string | null {
128
212
 
129
213
  return null
130
214
  }
215
+
216
+ /**
217
+ * Look for a shared .env file in parent directories.
218
+ */
219
+ export function findSharedEnv(directory: string): string | null {
220
+ // Common patterns for shared env in monorepos
221
+ const patterns = [
222
+ '../shared/.env',
223
+ '../../shared/.env',
224
+ '../.env.shared',
225
+ '../../.env.shared',
226
+ ]
227
+
228
+ for (const pattern of patterns) {
229
+ const sharedPath = path.resolve(directory, pattern)
230
+ if (fs.existsSync(sharedPath)) {
231
+ return sharedPath
232
+ }
233
+ }
234
+
235
+ return null
236
+ }
package/src/config.web.ts CHANGED
@@ -2,22 +2,63 @@ import type { IConfig } from './types'
2
2
  import { ConfigValidationError } from './types'
3
3
 
4
4
  /**
5
- * Web implementation of IConfig using Vite's import.meta.env.
5
+ * Config store - populated by the generated config module or manually via setConfig().
6
+ */
7
+ let configStore: Record<string, string> = {}
8
+
9
+ /**
10
+ * Set config values. Called automatically when importing from a project that
11
+ * has generated config, or can be called manually.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // In your app entry point:
16
+ * import { setConfig } from '@idealyst/config'
17
+ * import { generatedConfig } from './config.generated'
18
+ *
19
+ * setConfig(generatedConfig)
20
+ * ```
21
+ */
22
+ export function setConfig(config: Record<string, string>): void {
23
+ configStore = { ...configStore, ...config }
24
+ }
25
+
26
+ /**
27
+ * Clear all config values. Useful for testing.
28
+ */
29
+ export function clearConfig(): void {
30
+ configStore = {}
31
+ }
32
+
33
+ /**
34
+ * Get the raw config store. Useful for debugging.
35
+ */
36
+ export function getConfigStore(): Record<string, string> {
37
+ return { ...configStore }
38
+ }
39
+
40
+ /**
41
+ * Web implementation of IConfig.
42
+ *
43
+ * Config values come from:
44
+ * 1. Generated config module (via setConfig)
45
+ * 2. Manual setConfig() calls
46
+ *
47
+ * Usage:
48
+ * ```typescript
49
+ * import { config, setConfig } from '@idealyst/config'
50
+ * import { generatedConfig } from './config.generated'
6
51
  *
7
- * This implementation automatically handles the VITE_ prefix:
8
- * - User code uses canonical names: config.get('API_URL')
9
- * - Internally we look up: import.meta.env.VITE_API_URL
52
+ * // Initialize config (do this once at app startup)
53
+ * setConfig(generatedConfig)
10
54
  *
11
- * This allows the same code to work on both web and native platforms.
55
+ * // Then use anywhere
56
+ * const apiUrl = config.get('API_URL')
57
+ * ```
12
58
  */
13
59
  class WebConfig implements IConfig {
14
60
  get(key: string, defaultValue?: string): string | undefined {
15
- // Always use VITE_ prefix - this is the Vite convention for exposing
16
- // environment variables to client-side code.
17
- // User code uses canonical names: config.get('API_URL')
18
- // Internally we look up: import.meta.env.VITE_API_URL
19
- const value = (import.meta.env as Record<string, string | undefined>)[`VITE_${key}`]
20
- return value ?? defaultValue
61
+ return configStore[key] ?? defaultValue
21
62
  }
22
63
 
23
64
  getRequired(key: string): string {
@@ -25,21 +66,18 @@ class WebConfig implements IConfig {
25
66
  if (value === undefined) {
26
67
  throw new Error(
27
68
  `Required config key "${key}" is not defined. ` +
28
- `Make sure VITE_${key} is set in your .env file.`
69
+ `Make sure you've run "idealyst-config generate" and imported the generated config.`
29
70
  )
30
71
  }
31
72
  return value
32
73
  }
33
74
 
34
75
  has(key: string): boolean {
35
- return (import.meta.env as Record<string, string | undefined>)[`VITE_${key}`] !== undefined
76
+ return configStore[key] !== undefined
36
77
  }
37
78
 
38
79
  keys(): string[] {
39
- // Return canonical names (strip VITE_ prefix)
40
- return Object.keys(import.meta.env)
41
- .filter(k => k.startsWith('VITE_'))
42
- .map(k => k.replace(/^VITE_/, ''))
80
+ return Object.keys(configStore).sort()
43
81
  }
44
82
 
45
83
  validate(requiredKeys: string[]): void {
package/src/index.web.ts CHANGED
@@ -1,24 +1,29 @@
1
1
  /**
2
2
  * Web entry point for @idealyst/config
3
3
  *
4
- * Uses Vite's import.meta.env for environment variable access.
5
- * The VITE_ prefix is handled automatically - use canonical names in your code.
4
+ * Config values come from a generated module created by the CLI.
5
+ * This approach works with any bundler and supports env inheritance.
6
6
  *
7
7
  * @example
8
8
  * ```typescript
9
- * import { config } from '@idealyst/config'
9
+ * // 1. Generate config (run in terminal)
10
+ * // idealyst-config generate --extends ../shared/.env --env .env
10
11
  *
11
- * // In your .env file: VITE_API_URL=https://api.example.com
12
- * // In your code: use canonical name without prefix
12
+ * // 2. Initialize in your app entry point
13
+ * import { config, setConfig } from '@idealyst/config'
14
+ * import { generatedConfig } from './config.generated'
15
+ * setConfig(generatedConfig)
16
+ *
17
+ * // 3. Use anywhere
13
18
  * const apiUrl = config.get('API_URL')
14
19
  * ```
15
20
  */
16
21
 
17
- import WebConfig from './config.web'
22
+ import WebConfig, { setConfig, clearConfig, getConfigStore } from './config.web'
18
23
 
19
24
  // Create singleton instance for web
20
25
  const config = new WebConfig()
21
26
 
22
27
  export default config
23
- export { config, config as Config, WebConfig }
28
+ export { config, config as Config, WebConfig, setConfig, clearConfig, getConfigStore }
24
29
  export * from './types'
package/src/vite-env.d.ts DELETED
@@ -1,22 +0,0 @@
1
- /// <reference types="vite/client" />
2
-
3
- /**
4
- * Type declarations for Vite's import.meta.env
5
- *
6
- * This file provides type information for import.meta.env when Vite types
7
- * are not available. In projects using Vite, these types are provided by
8
- * vite/client.
9
- */
10
-
11
- interface ImportMetaEnv {
12
- [key: string]: string | undefined
13
- readonly MODE: string
14
- readonly BASE_URL: string
15
- readonly PROD: boolean
16
- readonly DEV: boolean
17
- readonly SSR: boolean
18
- }
19
-
20
- interface ImportMeta {
21
- readonly env: ImportMetaEnv
22
- }