@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 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
+ }
@@ -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'
@@ -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
+ }