@idealyst/config 1.2.12
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 +191 -0
- package/bin/idealyst-config.js +203 -0
- package/package.json +77 -0
- package/src/cli/generate.ts +130 -0
- package/src/cli/index.ts +94 -0
- package/src/config.native.ts +60 -0
- package/src/config.web.ts +54 -0
- package/src/index.native.ts +23 -0
- package/src/index.ts +28 -0
- package/src/index.web.ts +24 -0
- package/src/types.ts +66 -0
- package/src/vite-env.d.ts +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# @idealyst/config
|
|
2
|
+
|
|
3
|
+
Cross-platform configuration and environment variable support for React and React Native applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
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
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @idealyst/config
|
|
17
|
+
|
|
18
|
+
# For React Native, also install react-native-config
|
|
19
|
+
npm install react-native-config
|
|
20
|
+
cd ios && pod install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
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')
|
|
36
|
+
|
|
37
|
+
// Validate required vars at startup
|
|
38
|
+
config.validate(['API_URL', 'AUTH_SECRET'])
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Environment Files
|
|
42
|
+
|
|
43
|
+
### React Native (.env)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# No prefix needed
|
|
47
|
+
API_URL=https://api.example.com
|
|
48
|
+
GOOGLE_CLIENT_ID=abc123
|
|
49
|
+
JWT_SECRET=supersecret
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Vite Web (.env)
|
|
53
|
+
|
|
54
|
+
```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
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Important:** Your code always uses canonical names without the `VITE_` prefix. The web implementation handles this internally:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Both platforms - same code
|
|
65
|
+
const apiUrl = config.get('API_URL')
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Type Generation
|
|
69
|
+
|
|
70
|
+
Generate TypeScript declarations for autocomplete support:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Auto-detect .env file
|
|
74
|
+
npx idealyst-config generate
|
|
75
|
+
|
|
76
|
+
# Specify .env file
|
|
77
|
+
npx idealyst-config generate --env .env.local
|
|
78
|
+
|
|
79
|
+
# Custom output path
|
|
80
|
+
npx idealyst-config generate --output types/env.d.ts
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This creates a declaration file that provides autocomplete for your config keys:
|
|
84
|
+
|
|
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
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Now you get autocomplete when calling `config.get()`:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
config.get('API_URL') // Autocomplete shows available keys
|
|
100
|
+
config.get('INVALID') // TypeScript error - key not in ConfigKeys
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## API Reference
|
|
104
|
+
|
|
105
|
+
### `config.get(key: string): string | undefined`
|
|
106
|
+
|
|
107
|
+
Get a configuration value by key.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const apiUrl = config.get('API_URL')
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `config.get(key: string, defaultValue: string): string`
|
|
114
|
+
|
|
115
|
+
Get a configuration value with a fallback default.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
const port = config.get('PORT', '3000')
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `config.getRequired(key: string): string`
|
|
122
|
+
|
|
123
|
+
Get a required configuration value. Throws an error if not defined.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const secret = config.getRequired('JWT_SECRET')
|
|
127
|
+
// Throws: 'Required config key "JWT_SECRET" is not defined'
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `config.has(key: string): boolean`
|
|
131
|
+
|
|
132
|
+
Check if a configuration key exists.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
if (config.has('DEBUG')) {
|
|
136
|
+
enableDebugMode()
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `config.keys(): string[]`
|
|
141
|
+
|
|
142
|
+
Get all available configuration keys.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
console.log('Available config:', config.keys())
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `config.validate(requiredKeys: string[]): void`
|
|
149
|
+
|
|
150
|
+
Validate that all required keys are present. Throws `ConfigValidationError` if any are missing.
|
|
151
|
+
|
|
152
|
+
```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)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Platform Implementation Details
|
|
164
|
+
|
|
165
|
+
### Web (Vite)
|
|
166
|
+
|
|
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`
|
|
170
|
+
|
|
171
|
+
### React Native
|
|
172
|
+
|
|
173
|
+
Uses `react-native-config` for native environment variable injection:
|
|
174
|
+
- Your code: `config.get('API_URL')`
|
|
175
|
+
- Internal lookup: `Config.API_URL`
|
|
176
|
+
|
|
177
|
+
Make sure to follow [react-native-config setup](https://github.com/luggit/react-native-config#setup) for your platform.
|
|
178
|
+
|
|
179
|
+
## Best Practices
|
|
180
|
+
|
|
181
|
+
1. **Generate types after .env changes** - Run `idealyst-config generate` whenever you add/remove environment variables
|
|
182
|
+
|
|
183
|
+
2. **Validate at startup** - Call `config.validate()` early in your app to catch missing config
|
|
184
|
+
|
|
185
|
+
3. **Use .env.example** - Commit an example file with all required keys (no values)
|
|
186
|
+
|
|
187
|
+
4. **Don't commit secrets** - Add `.env` and `.env.local` to `.gitignore`
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI for @idealyst/config - Generate TypeScript types from .env files
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a .env file and extract all key names.
|
|
15
|
+
* Strips VITE_ prefix to normalize to canonical names.
|
|
16
|
+
*/
|
|
17
|
+
function parseEnvFile(content) {
|
|
18
|
+
const keys = []
|
|
19
|
+
|
|
20
|
+
for (const line of content.split('\n')) {
|
|
21
|
+
const trimmed = line.trim()
|
|
22
|
+
|
|
23
|
+
// Skip empty lines and comments
|
|
24
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Extract key name (everything before the first =)
|
|
29
|
+
const equalsIndex = trimmed.indexOf('=')
|
|
30
|
+
if (equalsIndex === -1) {
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let key = trimmed.substring(0, equalsIndex).trim()
|
|
35
|
+
|
|
36
|
+
// Strip VITE_ prefix to normalize to canonical names
|
|
37
|
+
if (key.startsWith('VITE_')) {
|
|
38
|
+
key = key.substring(5)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Only add unique keys
|
|
42
|
+
if (key && !keys.includes(key)) {
|
|
43
|
+
keys.push(key)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return keys.sort()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate TypeScript declaration content from a list of config keys.
|
|
52
|
+
*/
|
|
53
|
+
function generateDeclaration(keys, sourceFile) {
|
|
54
|
+
const keyDefinitions = keys.map(k => ` ${k}: string`).join('\n')
|
|
55
|
+
|
|
56
|
+
return `// Auto-generated by @idealyst/config - DO NOT EDIT
|
|
57
|
+
// Generated from: ${sourceFile}
|
|
58
|
+
// Run \`idealyst-config generate\` to regenerate
|
|
59
|
+
|
|
60
|
+
declare module '@idealyst/config' {
|
|
61
|
+
interface ConfigKeys {
|
|
62
|
+
${keyDefinitions}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface IConfig {
|
|
66
|
+
get<K extends keyof ConfigKeys>(key: K): string | undefined
|
|
67
|
+
get<K extends keyof ConfigKeys>(key: K, defaultValue: string): string
|
|
68
|
+
getRequired<K extends keyof ConfigKeys>(key: K): string
|
|
69
|
+
has<K extends keyof ConfigKeys>(key: K): boolean
|
|
70
|
+
validate(requiredKeys: (keyof ConfigKeys)[]): void
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {}
|
|
75
|
+
`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Find the most appropriate .env file in a directory.
|
|
80
|
+
*/
|
|
81
|
+
function findEnvFile(directory) {
|
|
82
|
+
const candidates = ['.env.local', '.env.development', '.env']
|
|
83
|
+
|
|
84
|
+
for (const candidate of candidates) {
|
|
85
|
+
const envPath = path.join(directory, candidate)
|
|
86
|
+
if (fs.existsSync(envPath)) {
|
|
87
|
+
return envPath
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function printUsage() {
|
|
95
|
+
console.log(`
|
|
96
|
+
@idealyst/config - Generate TypeScript types from .env files
|
|
97
|
+
|
|
98
|
+
Usage:
|
|
99
|
+
idealyst-config generate [options]
|
|
100
|
+
|
|
101
|
+
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
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
idealyst-config generate
|
|
108
|
+
idealyst-config generate --env .env.local
|
|
109
|
+
idealyst-config generate --env .env --output types/env.d.ts
|
|
110
|
+
`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseArgs(args) {
|
|
114
|
+
const result = {}
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < args.length; i++) {
|
|
117
|
+
const arg = args[i]
|
|
118
|
+
|
|
119
|
+
if (arg === '--help' || arg === '-h') {
|
|
120
|
+
result.help = true
|
|
121
|
+
} else if (arg === '--env' && args[i + 1]) {
|
|
122
|
+
result.env = args[++i]
|
|
123
|
+
} else if (arg === '--output' && args[i + 1]) {
|
|
124
|
+
result.output = args[++i]
|
|
125
|
+
} else if (!arg.startsWith('-') && !result.command) {
|
|
126
|
+
result.command = arg
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function main() {
|
|
134
|
+
const args = parseArgs(process.argv.slice(2))
|
|
135
|
+
|
|
136
|
+
if (args.help || (!args.command && process.argv.length <= 2)) {
|
|
137
|
+
printUsage()
|
|
138
|
+
process.exit(0)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (args.command !== 'generate') {
|
|
142
|
+
console.error(`Unknown command: ${args.command}`)
|
|
143
|
+
console.error('Run "idealyst-config --help" for usage information.')
|
|
144
|
+
process.exit(1)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const cwd = process.cwd()
|
|
148
|
+
|
|
149
|
+
// Find or use the specified .env file
|
|
150
|
+
let envPath
|
|
151
|
+
if (args.env) {
|
|
152
|
+
envPath = path.isAbsolute(args.env) ? args.env : path.join(cwd, args.env)
|
|
153
|
+
} else {
|
|
154
|
+
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
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check if env file exists
|
|
163
|
+
if (!fs.existsSync(envPath)) {
|
|
164
|
+
console.error(`Error: Environment file not found: ${envPath}`)
|
|
165
|
+
process.exit(1)
|
|
166
|
+
}
|
|
167
|
+
|
|
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)
|
|
177
|
+
|
|
178
|
+
if (keys.length === 0) {
|
|
179
|
+
console.warn('Warning: No environment variables found in', envPath)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Generate declaration
|
|
183
|
+
const declaration = generateDeclaration(keys, path.basename(envPath))
|
|
184
|
+
|
|
185
|
+
// Ensure output directory exists
|
|
186
|
+
const outputDir = path.dirname(outputPath)
|
|
187
|
+
if (!fs.existsSync(outputDir)) {
|
|
188
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Write declaration file
|
|
192
|
+
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)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idealyst/config",
|
|
3
|
+
"version": "1.2.12",
|
|
4
|
+
"description": "Cross-platform configuration and environment variable support for React and React Native",
|
|
5
|
+
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/config#readme",
|
|
6
|
+
"readme": "README.md",
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"module": "src/index.ts",
|
|
9
|
+
"types": "src/index.ts",
|
|
10
|
+
"react-native": "src/index.native.ts",
|
|
11
|
+
"bin": {
|
|
12
|
+
"idealyst-config": "./bin/idealyst-config.js"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/IdealystIO/idealyst-framework.git",
|
|
17
|
+
"directory": "packages/config"
|
|
18
|
+
},
|
|
19
|
+
"author": "Your Name <your.email@example.com>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"react-native": "./src/index.native.ts",
|
|
27
|
+
"browser": {
|
|
28
|
+
"types": "./src/index.web.ts",
|
|
29
|
+
"import": "./src/index.web.ts",
|
|
30
|
+
"require": "./src/index.web.ts"
|
|
31
|
+
},
|
|
32
|
+
"default": {
|
|
33
|
+
"types": "./src/index.ts",
|
|
34
|
+
"import": "./src/index.ts",
|
|
35
|
+
"require": "./src/index.ts"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"./generate": {
|
|
39
|
+
"types": "./src/cli/generate.ts",
|
|
40
|
+
"import": "./src/cli/generate.ts",
|
|
41
|
+
"require": "./src/cli/generate.ts"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"prepublishOnly": "echo 'Publishing TypeScript source directly'",
|
|
46
|
+
"publish:npm": "npm publish"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react-native-config": ">=1.5.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"react-native-config": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^20.0.0",
|
|
58
|
+
"react-native-config": "^1.5.0",
|
|
59
|
+
"typescript": "^5.0.0"
|
|
60
|
+
},
|
|
61
|
+
"files": [
|
|
62
|
+
"src",
|
|
63
|
+
"bin",
|
|
64
|
+
"README.md"
|
|
65
|
+
],
|
|
66
|
+
"keywords": [
|
|
67
|
+
"config",
|
|
68
|
+
"configuration",
|
|
69
|
+
"env",
|
|
70
|
+
"environment",
|
|
71
|
+
"dotenv",
|
|
72
|
+
"react",
|
|
73
|
+
"react-native",
|
|
74
|
+
"cross-platform",
|
|
75
|
+
"idealyst"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
export interface GenerateOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Path to the .env file to read
|
|
7
|
+
*/
|
|
8
|
+
envPath: string
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Path to write the generated TypeScript declaration file
|
|
12
|
+
*/
|
|
13
|
+
outputPath: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a .env file and extract all key names.
|
|
18
|
+
* Strips VITE_ prefix to normalize to canonical names.
|
|
19
|
+
*/
|
|
20
|
+
export function parseEnvFile(content: string): string[] {
|
|
21
|
+
const keys: string[] = []
|
|
22
|
+
|
|
23
|
+
for (const line of content.split('\n')) {
|
|
24
|
+
const trimmed = line.trim()
|
|
25
|
+
|
|
26
|
+
// Skip empty lines and comments
|
|
27
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extract key name (everything before the first =)
|
|
32
|
+
const equalsIndex = trimmed.indexOf('=')
|
|
33
|
+
if (equalsIndex === -1) {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let key = trimmed.substring(0, equalsIndex).trim()
|
|
38
|
+
|
|
39
|
+
// Strip VITE_ prefix to normalize to canonical names
|
|
40
|
+
if (key.startsWith('VITE_')) {
|
|
41
|
+
key = key.substring(5)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Only add unique keys
|
|
45
|
+
if (key && !keys.includes(key)) {
|
|
46
|
+
keys.push(key)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return keys.sort()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate TypeScript declaration content from a list of config keys.
|
|
55
|
+
*/
|
|
56
|
+
export function generateDeclaration(keys: string[], sourceFile: string): string {
|
|
57
|
+
const keyDefinitions = keys.map(k => ` ${k}: string`).join('\n')
|
|
58
|
+
|
|
59
|
+
return `// Auto-generated by @idealyst/config - DO NOT EDIT
|
|
60
|
+
// Generated from: ${sourceFile}
|
|
61
|
+
// Run \`idealyst-config generate\` to regenerate
|
|
62
|
+
|
|
63
|
+
declare module '@idealyst/config' {
|
|
64
|
+
interface ConfigKeys {
|
|
65
|
+
${keyDefinitions}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface IConfig {
|
|
69
|
+
get<K extends keyof ConfigKeys>(key: K): string | undefined
|
|
70
|
+
get<K extends keyof ConfigKeys>(key: K, defaultValue: string): string
|
|
71
|
+
getRequired<K extends keyof ConfigKeys>(key: K): string
|
|
72
|
+
has<K extends keyof ConfigKeys>(key: K): boolean
|
|
73
|
+
validate(requiredKeys: (keyof ConfigKeys)[]): void
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {}
|
|
78
|
+
`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generate TypeScript config types from an .env file.
|
|
83
|
+
*
|
|
84
|
+
* @param options - Generation options
|
|
85
|
+
* @returns The path to the generated file
|
|
86
|
+
*/
|
|
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}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const envContent = fs.readFileSync(options.envPath, 'utf-8')
|
|
94
|
+
const keys = parseEnvFile(envContent)
|
|
95
|
+
|
|
96
|
+
if (keys.length === 0) {
|
|
97
|
+
console.warn('Warning: No environment variables found in', options.envPath)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Generate the declaration content
|
|
101
|
+
const declaration = generateDeclaration(keys, path.basename(options.envPath))
|
|
102
|
+
|
|
103
|
+
// Ensure the output directory exists
|
|
104
|
+
const outputDir = path.dirname(options.outputPath)
|
|
105
|
+
if (!fs.existsSync(outputDir)) {
|
|
106
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Write the declaration file
|
|
110
|
+
fs.writeFileSync(options.outputPath, declaration)
|
|
111
|
+
|
|
112
|
+
return options.outputPath
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Find the most appropriate .env file in a directory.
|
|
117
|
+
* Prefers .env.local > .env.development > .env
|
|
118
|
+
*/
|
|
119
|
+
export function findEnvFile(directory: string): string | null {
|
|
120
|
+
const candidates = ['.env.local', '.env.development', '.env']
|
|
121
|
+
|
|
122
|
+
for (const candidate of candidates) {
|
|
123
|
+
const envPath = path.join(directory, candidate)
|
|
124
|
+
if (fs.existsSync(envPath)) {
|
|
125
|
+
return envPath
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null
|
|
130
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { generateConfigTypes, findEnvFile } from './generate'
|
|
5
|
+
|
|
6
|
+
function printUsage(): void {
|
|
7
|
+
console.log(`
|
|
8
|
+
@idealyst/config - Generate TypeScript types from .env files
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
idealyst-config generate [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--env <path> Path to .env file (default: auto-detect)
|
|
15
|
+
--output <path> Output path for .d.ts file (default: src/env.d.ts)
|
|
16
|
+
--help Show this help message
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
idealyst-config generate
|
|
20
|
+
idealyst-config generate --env .env.local
|
|
21
|
+
idealyst-config generate --env .env --output types/env.d.ts
|
|
22
|
+
`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseArgs(args: string[]): { command?: string; env?: string; output?: string; help?: boolean } {
|
|
26
|
+
const result: { command?: string; env?: string; output?: string; help?: boolean } = {}
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
const arg = args[i]
|
|
30
|
+
|
|
31
|
+
if (arg === '--help' || arg === '-h') {
|
|
32
|
+
result.help = true
|
|
33
|
+
} else if (arg === '--env' && args[i + 1]) {
|
|
34
|
+
result.env = args[++i]
|
|
35
|
+
} else if (arg === '--output' && args[i + 1]) {
|
|
36
|
+
result.output = args[++i]
|
|
37
|
+
} else if (!arg.startsWith('-') && !result.command) {
|
|
38
|
+
result.command = arg
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function main(): void {
|
|
46
|
+
const args = parseArgs(process.argv.slice(2))
|
|
47
|
+
|
|
48
|
+
if (args.help || (!args.command && process.argv.length <= 2)) {
|
|
49
|
+
printUsage()
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (args.command !== 'generate') {
|
|
54
|
+
console.error(`Unknown command: ${args.command}`)
|
|
55
|
+
console.error('Run "idealyst-config --help" for usage information.')
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const cwd = process.cwd()
|
|
60
|
+
|
|
61
|
+
// Find or use the specified .env file
|
|
62
|
+
let envPath: string
|
|
63
|
+
if (args.env) {
|
|
64
|
+
envPath = path.isAbsolute(args.env) ? args.env : path.join(cwd, args.env)
|
|
65
|
+
} else {
|
|
66
|
+
const foundEnv = findEnvFile(cwd)
|
|
67
|
+
if (!foundEnv) {
|
|
68
|
+
console.error('Error: No .env file found in current directory.')
|
|
69
|
+
console.error('Create a .env file or specify one with --env <path>')
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
envPath = foundEnv
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Determine output path
|
|
76
|
+
const outputPath = args.output
|
|
77
|
+
? (path.isAbsolute(args.output) ? args.output : path.join(cwd, args.output))
|
|
78
|
+
: path.join(cwd, 'src', 'env.d.ts')
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = generateConfigTypes({
|
|
82
|
+
envPath,
|
|
83
|
+
outputPath,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
console.log(`Generated config types at: ${result}`)
|
|
87
|
+
console.log(`Source: ${envPath}`)
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Error generating config types:', (error as Error).message)
|
|
90
|
+
process.exit(1)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { IConfig } from './types'
|
|
2
|
+
import { ConfigValidationError } from './types'
|
|
3
|
+
|
|
4
|
+
// react-native-config provides a Config object with all env variables
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
6
|
+
let RNConfig: Record<string, string | undefined> = {}
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
// Dynamic import to handle cases where react-native-config is not installed
|
|
10
|
+
// This allows the package to be used in web-only projects without errors
|
|
11
|
+
RNConfig = require('react-native-config').default || require('react-native-config')
|
|
12
|
+
} catch {
|
|
13
|
+
// react-native-config not available - will be empty object
|
|
14
|
+
// This is expected in web environments or when the native module isn't linked
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Native implementation of IConfig using react-native-config.
|
|
19
|
+
*
|
|
20
|
+
* This implementation provides direct access to .env variables without
|
|
21
|
+
* any prefix transformation, as react-native-config doesn't require prefixes.
|
|
22
|
+
*
|
|
23
|
+
* The .env file should use canonical names:
|
|
24
|
+
* API_URL=https://api.example.com
|
|
25
|
+
* GOOGLE_CLIENT_ID=abc123
|
|
26
|
+
*/
|
|
27
|
+
class NativeConfig implements IConfig {
|
|
28
|
+
get(key: string, defaultValue?: string): string | undefined {
|
|
29
|
+
return RNConfig[key] ?? defaultValue
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getRequired(key: string): string {
|
|
33
|
+
const value = this.get(key)
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Required config key "${key}" is not defined. ` +
|
|
37
|
+
`Make sure ${key} is set in your .env file.`
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
return value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
has(key: string): boolean {
|
|
44
|
+
return RNConfig[key] !== undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
keys(): string[] {
|
|
48
|
+
return Object.keys(RNConfig)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
validate(requiredKeys: string[]): void {
|
|
52
|
+
const missing = requiredKeys.filter(key => !this.has(key))
|
|
53
|
+
if (missing.length > 0) {
|
|
54
|
+
throw new ConfigValidationError(missing)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default NativeConfig
|
|
60
|
+
export { NativeConfig }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { IConfig } from './types'
|
|
2
|
+
import { ConfigValidationError } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Web implementation of IConfig using Vite's import.meta.env.
|
|
6
|
+
*
|
|
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
|
|
10
|
+
*
|
|
11
|
+
* This allows the same code to work on both web and native platforms.
|
|
12
|
+
*/
|
|
13
|
+
class WebConfig implements IConfig {
|
|
14
|
+
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
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getRequired(key: string): string {
|
|
24
|
+
const value = this.get(key)
|
|
25
|
+
if (value === undefined) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Required config key "${key}" is not defined. ` +
|
|
28
|
+
`Make sure VITE_${key} is set in your .env file.`
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
has(key: string): boolean {
|
|
35
|
+
return (import.meta.env as Record<string, string | undefined>)[`VITE_${key}`] !== undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
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_/, ''))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
validate(requiredKeys: string[]): void {
|
|
46
|
+
const missing = requiredKeys.filter(key => !this.has(key))
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
throw new ConfigValidationError(missing)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default WebConfig
|
|
54
|
+
export { WebConfig }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native entry point for @idealyst/config
|
|
3
|
+
*
|
|
4
|
+
* Uses react-native-config for environment variable access.
|
|
5
|
+
* No prefix is required - use canonical names directly.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { config } from '@idealyst/config'
|
|
10
|
+
*
|
|
11
|
+
* // In your .env file: API_URL=https://api.example.com
|
|
12
|
+
* const apiUrl = config.get('API_URL')
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import NativeConfig from './config.native'
|
|
17
|
+
|
|
18
|
+
// Create singleton instance for native
|
|
19
|
+
const config = new NativeConfig()
|
|
20
|
+
|
|
21
|
+
export default config
|
|
22
|
+
export { config, config as Config, NativeConfig }
|
|
23
|
+
export * from './types'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @idealyst/config - Cross-platform configuration for React and React Native
|
|
3
|
+
*
|
|
4
|
+
* This is the generic entry point that exports types and the base interface.
|
|
5
|
+
* Platform-specific entry points (index.web.ts, index.native.ts) export
|
|
6
|
+
* pre-configured instances for their respective platforms.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { config } from '@idealyst/config'
|
|
11
|
+
*
|
|
12
|
+
* // Get a config value
|
|
13
|
+
* const apiUrl = config.get('API_URL')
|
|
14
|
+
*
|
|
15
|
+
* // Get with default
|
|
16
|
+
* const port = config.get('PORT', '3000')
|
|
17
|
+
*
|
|
18
|
+
* // Get required (throws if missing)
|
|
19
|
+
* const secret = config.getRequired('JWT_SECRET')
|
|
20
|
+
*
|
|
21
|
+
* // Validate at startup
|
|
22
|
+
* config.validate(['API_URL', 'JWT_SECRET'])
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export * from './types'
|
|
27
|
+
export type { IConfig, ConfigKeys } from './types'
|
|
28
|
+
export { ConfigValidationError } from './types'
|
package/src/index.web.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web entry point for @idealyst/config
|
|
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.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { config } from '@idealyst/config'
|
|
10
|
+
*
|
|
11
|
+
* // In your .env file: VITE_API_URL=https://api.example.com
|
|
12
|
+
* // In your code: use canonical name without prefix
|
|
13
|
+
* const apiUrl = config.get('API_URL')
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import WebConfig from './config.web'
|
|
18
|
+
|
|
19
|
+
// Create singleton instance for web
|
|
20
|
+
const config = new WebConfig()
|
|
21
|
+
|
|
22
|
+
export default config
|
|
23
|
+
export { config, config as Config, WebConfig }
|
|
24
|
+
export * from './types'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base config interface for cross-platform configuration access.
|
|
3
|
+
* This interface can be augmented by generated types for autocomplete support.
|
|
4
|
+
*/
|
|
5
|
+
export interface IConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Get a configuration value by key.
|
|
8
|
+
* @param key - The canonical key name (without VITE_ prefix)
|
|
9
|
+
* @param defaultValue - Optional default value if key is not set
|
|
10
|
+
* @returns The value, the default value, or undefined if not set
|
|
11
|
+
*/
|
|
12
|
+
get(key: string, defaultValue?: string): string | undefined
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get a required configuration value. Throws if not set.
|
|
16
|
+
* @param key - The canonical key name (without VITE_ prefix)
|
|
17
|
+
* @returns The value
|
|
18
|
+
* @throws Error if the key is not defined
|
|
19
|
+
*/
|
|
20
|
+
getRequired(key: string): string
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a configuration key exists.
|
|
24
|
+
* @param key - The canonical key name (without VITE_ prefix)
|
|
25
|
+
* @returns True if the key is defined
|
|
26
|
+
*/
|
|
27
|
+
has(key: string): boolean
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get all available configuration keys.
|
|
31
|
+
* @returns Array of canonical key names
|
|
32
|
+
*/
|
|
33
|
+
keys(): string[]
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate that all required keys are present.
|
|
37
|
+
* @param requiredKeys - Array of required key names
|
|
38
|
+
* @throws ConfigValidationError if any keys are missing
|
|
39
|
+
*/
|
|
40
|
+
validate(requiredKeys: string[]): void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Error thrown when required configuration keys are missing.
|
|
45
|
+
*/
|
|
46
|
+
export class ConfigValidationError extends Error {
|
|
47
|
+
/**
|
|
48
|
+
* The list of missing configuration keys.
|
|
49
|
+
*/
|
|
50
|
+
public readonly missingKeys: string[]
|
|
51
|
+
|
|
52
|
+
constructor(missingKeys: string[]) {
|
|
53
|
+
super(`Missing required config keys: ${missingKeys.join(', ')}`)
|
|
54
|
+
this.name = 'ConfigValidationError'
|
|
55
|
+
this.missingKeys = missingKeys
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Interface for ConfigKeys - augmented by generated types.
|
|
61
|
+
* When you run `idealyst-config generate`, this interface is extended
|
|
62
|
+
* with your actual environment variable keys.
|
|
63
|
+
*/
|
|
64
|
+
export interface ConfigKeys {
|
|
65
|
+
[key: string]: string
|
|
66
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|