@idealyst/config 1.2.14 → 1.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,11 +91,49 @@ That's it! The Babel plugin reads your .env files at compile time and injects th
91
91
  // Main .env file (highest priority, default: auto-detect)
92
92
  envPath: '.env',
93
93
 
94
+ // Required keys - warn if missing at build time
95
+ required: ['API_URL', 'AUTH_SECRET'],
96
+
97
+ // Fail the build if required keys are missing (default: false = warn only)
98
+ errorOnMissing: true,
99
+
100
+ // Log which .env files were loaded (default: false)
101
+ verbose: true,
102
+
94
103
  // Project root (default: process.cwd())
95
104
  root: '/path/to/project'
96
105
  }]
97
106
  ```
98
107
 
108
+ ### Build-time Validation
109
+
110
+ The plugin validates required keys at compile time:
111
+
112
+ ```js
113
+ // babel.config.js
114
+ plugins: [
115
+ ['@idealyst/config/plugin', {
116
+ required: ['API_URL', 'DATABASE_URL', 'JWT_SECRET'],
117
+ errorOnMissing: process.env.NODE_ENV === 'production' // Fail in prod, warn in dev
118
+ }]
119
+ ]
120
+ ```
121
+
122
+ **Warning output:**
123
+ ```
124
+ ⚠ @idealyst/config: Missing required config keys: JWT_SECRET
125
+ Add these keys to your .env file to suppress this warning.
126
+ Set errorOnMissing: true to fail the build instead.
127
+ ```
128
+
129
+ **Error output (with `errorOnMissing: true`):**
130
+ ```
131
+ ✖ @idealyst/config: Missing required config keys: JWT_SECRET
132
+ Add these keys to your .env file or check your plugin configuration.
133
+
134
+ Error: @idealyst/config: Missing required config keys: JWT_SECRET
135
+ ```
136
+
99
137
  ### Auto-detection
100
138
 
101
139
  If you don't specify options, the plugin will:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/config",
3
- "version": "1.2.14",
3
+ "version": "1.2.15",
4
4
  "description": "Cross-platform configuration and environment variable support for React and React Native",
5
5
  "documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/config#readme",
6
6
  "readme": "README.md",
package/plugin.js CHANGED
@@ -9,7 +9,9 @@
9
9
  * plugins: [
10
10
  * ['@idealyst/config/plugin', {
11
11
  * extends: ['../shared/.env'],
12
- * envPath: '.env'
12
+ * envPath: '.env',
13
+ * required: ['API_URL', 'AUTH_SECRET'], // Warn if missing
14
+ * errorOnMissing: true // Fail build if missing
13
15
  * }]
14
16
  * ]
15
17
  * }
@@ -19,6 +21,9 @@
19
21
  const fs = require('fs')
20
22
  const path = require('path')
21
23
 
24
+ // Track if we've already validated (to avoid duplicate warnings)
25
+ let hasValidated = false
26
+
22
27
  /**
23
28
  * Parse a .env file and extract key-value pairs.
24
29
  */
@@ -99,6 +104,7 @@ function findSharedEnv(directory) {
99
104
  */
100
105
  function loadConfig(options, projectRoot) {
101
106
  const configs = []
107
+ const sources = []
102
108
 
103
109
  // Load inherited configs first (lowest priority)
104
110
  if (options.extends) {
@@ -109,6 +115,7 @@ function loadConfig(options, projectRoot) {
109
115
 
110
116
  if (fs.existsSync(resolvedPath)) {
111
117
  configs.push(parseEnvFile(resolvedPath))
118
+ sources.push(resolvedPath)
112
119
  }
113
120
  }
114
121
  } else {
@@ -116,6 +123,7 @@ function loadConfig(options, projectRoot) {
116
123
  const sharedEnv = findSharedEnv(projectRoot)
117
124
  if (sharedEnv) {
118
125
  configs.push(parseEnvFile(sharedEnv))
126
+ sources.push(sharedEnv)
119
127
  }
120
128
  }
121
129
 
@@ -131,10 +139,44 @@ function loadConfig(options, projectRoot) {
131
139
 
132
140
  if (envPath && fs.existsSync(envPath)) {
133
141
  configs.push(parseEnvFile(envPath))
142
+ sources.push(envPath)
134
143
  }
135
144
 
136
145
  // Merge configs (later configs override earlier ones)
137
- return Object.assign({}, ...configs)
146
+ return {
147
+ config: Object.assign({}, ...configs),
148
+ sources
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Validate required config keys and emit warnings/errors.
154
+ */
155
+ function validateConfig(configValues, options, projectRoot) {
156
+ if (hasValidated) return true
157
+ hasValidated = true
158
+
159
+ const required = options.required || []
160
+ if (required.length === 0) return true
161
+
162
+ const missing = required.filter(key => !(key in configValues))
163
+
164
+ if (missing.length === 0) return true
165
+
166
+ const errorOnMissing = options.errorOnMissing ?? false
167
+ const message = `@idealyst/config: Missing required config keys: ${missing.join(', ')}`
168
+
169
+ if (errorOnMissing) {
170
+ console.error('\n\x1b[31m✖ ' + message + '\x1b[0m')
171
+ console.error('\x1b[90m Add these keys to your .env file or check your plugin configuration.\x1b[0m\n')
172
+ throw new Error(message)
173
+ } else {
174
+ console.warn('\n\x1b[33m⚠ ' + message + '\x1b[0m')
175
+ console.warn('\x1b[90m Add these keys to your .env file to suppress this warning.\x1b[0m')
176
+ console.warn('\x1b[90m Set errorOnMissing: true to fail the build instead.\x1b[0m\n')
177
+ }
178
+
179
+ return false
138
180
  }
139
181
 
140
182
  /**
@@ -146,6 +188,9 @@ module.exports = function babelPluginIdealystConfig(babel) {
146
188
  // Cache config per project root
147
189
  const configCache = new Map()
148
190
 
191
+ // Reset validation state on new compilation
192
+ hasValidated = false
193
+
149
194
  return {
150
195
  name: '@idealyst/config',
151
196
 
@@ -156,10 +201,24 @@ module.exports = function babelPluginIdealystConfig(babel) {
156
201
  const projectRoot = opts.root || state.cwd || process.cwd()
157
202
 
158
203
  // Load config (cached per project root)
204
+ let configValues
159
205
  if (!configCache.has(projectRoot)) {
160
- configCache.set(projectRoot, loadConfig(opts, projectRoot))
206
+ const { config, sources } = loadConfig(opts, projectRoot)
207
+ configCache.set(projectRoot, config)
208
+ configValues = config
209
+
210
+ // Validate required keys (only once per build)
211
+ validateConfig(config, opts, projectRoot)
212
+
213
+ // Log loaded sources in verbose mode
214
+ if (opts.verbose && sources.length > 0) {
215
+ console.log('\x1b[36m@idealyst/config loaded:\x1b[0m')
216
+ sources.forEach(s => console.log(' ← ' + path.relative(projectRoot, s)))
217
+ console.log(' Keys:', Object.keys(config).join(', '))
218
+ }
219
+ } else {
220
+ configValues = configCache.get(projectRoot)
161
221
  }
162
- const configValues = configCache.get(projectRoot)
163
222
 
164
223
  // Track if this file imports from @idealyst/config
165
224
  let hasConfigImport = false
@@ -231,3 +290,4 @@ module.exports.parseEnvFile = parseEnvFile
231
290
  module.exports.loadConfig = loadConfig
232
291
  module.exports.findEnvFile = findEnvFile
233
292
  module.exports.findSharedEnv = findSharedEnv
293
+ module.exports.validateConfig = validateConfig
@@ -1,32 +1,63 @@
1
1
  import type { IConfig } from './types'
2
2
  import { ConfigValidationError } from './types'
3
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
4
+ /**
5
+ * Config store - initialized from react-native-config, can be overridden via setConfig().
6
+ */
7
+ let configStore: Record<string, string> = {}
8
+
9
+ /**
10
+ * Initialize from react-native-config if available.
11
+ */
12
+ function initFromRNConfig(): void {
13
+ try {
14
+ // Dynamic import to handle cases where react-native-config is not installed
15
+ const RNConfig = require('react-native-config').default || require('react-native-config')
16
+ if (RNConfig && typeof RNConfig === 'object') {
17
+ configStore = { ...RNConfig }
18
+ }
19
+ } catch {
20
+ // react-native-config not available - configStore remains empty
21
+ // Values will be injected by Babel plugin via setConfig()
22
+ }
15
23
  }
16
24
 
25
+ // Initialize on module load
26
+ initFromRNConfig()
27
+
17
28
  /**
18
- * 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.
29
+ * Set config values. Called by Babel plugin at compile time,
30
+ * or can be called manually.
31
+ */
32
+ export function setConfig(config: Record<string, string>): void {
33
+ configStore = { ...configStore, ...config }
34
+ }
35
+
36
+ /**
37
+ * Clear all config values. Useful for testing.
38
+ */
39
+ export function clearConfig(): void {
40
+ configStore = {}
41
+ }
42
+
43
+ /**
44
+ * Get the raw config store. Useful for debugging.
45
+ */
46
+ export function getConfigStore(): Record<string, string> {
47
+ return { ...configStore }
48
+ }
49
+
50
+ /**
51
+ * Native implementation of IConfig.
22
52
  *
23
- * The .env file should use canonical names:
24
- * API_URL=https://api.example.com
25
- * GOOGLE_CLIENT_ID=abc123
53
+ * Config values come from:
54
+ * 1. react-native-config (if installed)
55
+ * 2. Babel plugin injection via setConfig()
56
+ * 3. Manual setConfig() calls
26
57
  */
27
58
  class NativeConfig implements IConfig {
28
59
  get(key: string, defaultValue?: string): string | undefined {
29
- return RNConfig[key] ?? defaultValue
60
+ return configStore[key] ?? defaultValue
30
61
  }
31
62
 
32
63
  getRequired(key: string): string {
@@ -41,11 +72,11 @@ class NativeConfig implements IConfig {
41
72
  }
42
73
 
43
74
  has(key: string): boolean {
44
- return RNConfig[key] !== undefined
75
+ return configStore[key] !== undefined
45
76
  }
46
77
 
47
78
  keys(): string[] {
48
- return Object.keys(RNConfig)
79
+ return Object.keys(configStore).sort()
49
80
  }
50
81
 
51
82
  validate(requiredKeys: string[]): void {
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Native entry point for @idealyst/config
3
3
  *
4
- * Uses react-native-config for environment variable access.
5
- * No prefix is required - use canonical names directly.
4
+ * Config values come from:
5
+ * 1. react-native-config (if installed)
6
+ * 2. Babel plugin injection via setConfig()
6
7
  *
7
8
  * @example
8
9
  * ```typescript
@@ -13,11 +14,11 @@
13
14
  * ```
14
15
  */
15
16
 
16
- import NativeConfig from './config.native'
17
+ import NativeConfig, { setConfig, clearConfig, getConfigStore } from './config.native'
17
18
 
18
19
  // Create singleton instance for native
19
20
  const config = new NativeConfig()
20
21
 
21
22
  export default config
22
- export { config, config as Config, NativeConfig }
23
+ export { config, config as Config, NativeConfig, setConfig, clearConfig, getConfigStore }
23
24
  export * from './types'