@abdokouta/react-config 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.examples/01-basic-usage.ts +289 -0
  2. package/.examples/02-env-helper.ts +282 -0
  3. package/.examples/README.md +285 -0
  4. package/.prettierrc.js +1 -0
  5. package/README.md +261 -0
  6. package/__tests__/config.module.test.ts +244 -0
  7. package/__tests__/drivers/env.driver.test.ts +259 -0
  8. package/__tests__/services/config.service.test.ts +328 -0
  9. package/__tests__/setup.d.ts +11 -0
  10. package/__tests__/vitest.setup.ts +30 -0
  11. package/config/config.config.ts +62 -0
  12. package/dist/index.d.mts +474 -0
  13. package/dist/index.d.ts +474 -0
  14. package/dist/index.js +516 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/index.mjs +501 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/eslint.config.js +13 -0
  19. package/package.json +77 -0
  20. package/src/config.module.ts +154 -0
  21. package/src/constants/index.ts +5 -0
  22. package/src/constants/tokens.constant.ts +38 -0
  23. package/src/drivers/env.driver.ts +194 -0
  24. package/src/drivers/file.driver.ts +81 -0
  25. package/src/drivers/index.ts +6 -0
  26. package/src/index.ts +92 -0
  27. package/src/interfaces/config-driver.interface.ts +30 -0
  28. package/src/interfaces/config-module-options.interface.ts +84 -0
  29. package/src/interfaces/config-service.interface.ts +71 -0
  30. package/src/interfaces/index.ts +8 -0
  31. package/src/interfaces/vite-config-plugin-options.interface.ts +56 -0
  32. package/src/plugins/index.ts +5 -0
  33. package/src/plugins/vite.plugin.ts +115 -0
  34. package/src/services/config.service.ts +172 -0
  35. package/src/services/index.ts +5 -0
  36. package/src/utils/get-nested-value.util.ts +56 -0
  37. package/src/utils/index.ts +6 -0
  38. package/src/utils/load-config-file.util.ts +37 -0
  39. package/src/utils/scan-config-files.util.ts +40 -0
  40. package/tsconfig.json +28 -0
  41. package/tsup.config.ts +105 -0
  42. package/vitest.config.ts +66 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Configuration Service Interface
3
+ *
4
+ * Defines the contract for configuration service implementations.
5
+ */
6
+ export interface ConfigServiceInterface {
7
+ /**
8
+ * Get configuration value
9
+ */
10
+ get<T = any>(key: string, defaultValue?: T): T | undefined;
11
+
12
+ /**
13
+ * Get configuration value or throw if not found
14
+ */
15
+ getOrThrow<T = any>(key: string): T;
16
+
17
+ /**
18
+ * Get string value
19
+ */
20
+ getString(key: string, defaultValue?: string): string | undefined;
21
+
22
+ /**
23
+ * Get string value or throw
24
+ */
25
+ getStringOrThrow(key: string): string;
26
+
27
+ /**
28
+ * Get number value
29
+ */
30
+ getNumber(key: string, defaultValue?: number): number | undefined;
31
+
32
+ /**
33
+ * Get number value or throw
34
+ */
35
+ getNumberOrThrow(key: string): number;
36
+
37
+ /**
38
+ * Get boolean value
39
+ */
40
+ getBool(key: string, defaultValue?: boolean): boolean | undefined;
41
+
42
+ /**
43
+ * Get boolean value or throw
44
+ */
45
+ getBoolOrThrow(key: string): boolean;
46
+
47
+ /**
48
+ * Get array value
49
+ */
50
+ getArray(key: string, defaultValue?: string[]): string[] | undefined;
51
+
52
+ /**
53
+ * Get JSON value
54
+ */
55
+ getJson<T = any>(key: string, defaultValue?: T): T | undefined;
56
+
57
+ /**
58
+ * Check if configuration key exists
59
+ */
60
+ has(key: string): boolean;
61
+
62
+ /**
63
+ * Get all configuration values
64
+ */
65
+ all(): Record<string, any>;
66
+
67
+ /**
68
+ * Clear cache
69
+ */
70
+ clearCache(): void;
71
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Interfaces
3
+ */
4
+
5
+ export type { ConfigDriver } from './config-driver.interface';
6
+ export type { ConfigModuleOptions } from './config-module-options.interface';
7
+ export type { ConfigServiceInterface } from './config-service.interface';
8
+ export type { ViteConfigPluginOptions } from './vite-config-plugin-options.interface';
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Vite Config Plugin Options Interface
3
+ *
4
+ * Configuration options for the Vite config plugin that injects
5
+ * environment variables into the browser.
6
+ */
7
+
8
+ export interface ViteConfigPluginOptions {
9
+ /**
10
+ * Environment variables loaded by Vite's loadEnv
11
+ * Pass the result of loadEnv(mode, envDir, '') here
12
+ */
13
+ env?: Record<string, string>;
14
+
15
+ /**
16
+ * Scan and collect .config.ts files
17
+ * @default false (disabled by default to avoid Node.js module issues)
18
+ */
19
+ scanConfigFiles?: boolean;
20
+
21
+ /**
22
+ * Pattern to match config files
23
+ * @default 'src/**\/*.config.ts'
24
+ */
25
+ configFilePattern?: string | string[];
26
+
27
+ /**
28
+ * Directories to exclude from config file scanning
29
+ * @default ['node_modules', 'dist', 'build', '.git']
30
+ */
31
+ excludeDirs?: string[];
32
+
33
+ /**
34
+ * Root directory for scanning config files
35
+ * @default process.cwd()
36
+ */
37
+ root?: string;
38
+
39
+ /**
40
+ * Include all environment variables
41
+ * @default true
42
+ */
43
+ includeAll?: boolean;
44
+
45
+ /**
46
+ * Specific environment variables to include
47
+ * Only used if includeAll is false
48
+ */
49
+ include?: string[];
50
+
51
+ /**
52
+ * Global variable name to inject config into
53
+ * @default '__APP_CONFIG__'
54
+ */
55
+ globalName?: string;
56
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Plugins
3
+ */
4
+
5
+ export { viteConfigPlugin } from './vite.plugin';
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Vite Plugin for Config Package
3
+ *
4
+ * Simple plugin that receives env from Vite's loadEnv and injects it into the browser.
5
+ * Does NOT apply any business logic - that's handled by ConfigService.
6
+ *
7
+ * Responsibilities:
8
+ * 1. Receive environment variables from Vite's loadEnv (passed via options)
9
+ * 2. Inject them into window.__APP_CONFIG__ (or custom global name)
10
+ * 3. Optionally scan and collect .config.ts files
11
+ *
12
+ * ConfigService will then query from window.__APP_CONFIG__ and apply:
13
+ * - Prefix stripping
14
+ * - Type conversion
15
+ * - Validation
16
+ * - Defaults
17
+ *
18
+ * Usage:
19
+ * ```ts
20
+ * import { defineConfig, loadEnv } from 'vite'
21
+ * import { viteConfigPlugin } from '@abdokouta/config/vite-plugin'
22
+ *
23
+ * export default defineConfig(({ mode }) => {
24
+ * const env = loadEnv(mode, 'environments', '')
25
+ *
26
+ * return {
27
+ * plugins: [
28
+ * viteConfigPlugin({ env })
29
+ * ]
30
+ * }
31
+ * })
32
+ * ```
33
+ */
34
+
35
+ import path from 'path';
36
+ import type { Plugin } from 'vite';
37
+ import type { ViteConfigPluginOptions } from '@/interfaces/vite-config-plugin-options.interface';
38
+ import { scanConfigFiles } from '@/utils/scan-config-files.util';
39
+ import { loadConfigFile } from '@/utils/load-config-file.util';
40
+
41
+ /**
42
+ * Vite plugin that injects environment variables into the browser
43
+ *
44
+ * This plugin is DUMB - it just receives env and injects it.
45
+ * ConfigService is SMART - it queries and applies business logic.
46
+ */
47
+ export function viteConfigPlugin(options: ViteConfigPluginOptions = {}): Plugin {
48
+ const {
49
+ env = {},
50
+ includeAll = true,
51
+ include = [],
52
+ scanConfigFiles: shouldScanConfigFiles = false,
53
+ globalName = '__APP_CONFIG__',
54
+ } = options;
55
+
56
+ let collectedConfig: Record<string, any> = {};
57
+
58
+ return {
59
+ name: 'vite-plugin-config',
60
+
61
+ async configResolved(config) {
62
+ // Scan and collect config files (optional, disabled by default)
63
+ if (shouldScanConfigFiles) {
64
+ const configFiles = await scanConfigFiles({
65
+ ...options,
66
+ root: config.root,
67
+ });
68
+
69
+ console.log('[vite-plugin-config] Found config files:', configFiles.length);
70
+
71
+ // Load all config files and merge them
72
+ for (const file of configFiles) {
73
+ const fileConfig = await loadConfigFile(file);
74
+ collectedConfig = { ...collectedConfig, ...fileConfig };
75
+ console.log('[vite-plugin-config] Loaded config from:', path.relative(config.root, file));
76
+ }
77
+ }
78
+ },
79
+
80
+ transformIndexHtml(html) {
81
+ // Build the config object from provided env
82
+ const processEnv: Record<string, any> = {};
83
+
84
+ for (const [key, value] of Object.entries(env)) {
85
+ // Filter by include list if not including all
86
+ if (!includeAll && !include.includes(key)) {
87
+ continue;
88
+ }
89
+
90
+ // Just copy the value as-is (NO prefix stripping here)
91
+ processEnv[key] = value;
92
+ }
93
+
94
+ // Merge with collected config from .config.ts files
95
+ const finalConfig = { ...processEnv, ...collectedConfig };
96
+
97
+ console.log('[vite-plugin-config] Injecting environment variables into HTML');
98
+ console.log('[vite-plugin-config] Total variables:', Object.keys(finalConfig).length);
99
+ console.log('[vite-plugin-config] Sample keys:', Object.keys(finalConfig).filter(k => k.includes('APP') || k.includes('VITE')));
100
+ console.log('[vite-plugin-config] Global name:', globalName);
101
+
102
+ // Inject script into HTML head
103
+ const script = `
104
+ <script>
105
+ window.${globalName} = ${JSON.stringify(finalConfig)};
106
+ // Also set process.env for backward compatibility
107
+ window.process = window.process || {};
108
+ window.process.env = window.${globalName};
109
+ </script>
110
+ `;
111
+
112
+ return html.replace('<head>', `<head>${script}`);
113
+ },
114
+ };
115
+ }
@@ -0,0 +1,172 @@
1
+ import { Inject, Injectable } from "@abdokouta/react-di";
2
+
3
+ import { CONFIG_DRIVER } from "@/constants/tokens.constant";
4
+ import type { ConfigDriver } from "@/interfaces/config-driver.interface";
5
+ import type { ConfigServiceInterface } from "@/interfaces/config-service.interface";
6
+
7
+ /**
8
+ * Configuration Service
9
+ *
10
+ * Provides type-safe access to configuration values with various getter methods.
11
+ * Similar to NestJS ConfigService.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * class MyService {
16
+ * constructor(private config: ConfigService) {}
17
+ *
18
+ * getDbConfig() {
19
+ * return {
20
+ * host: this.config.getString('DB_HOST', 'localhost'),
21
+ * port: this.config.getNumber('DB_PORT', 5432),
22
+ * ssl: this.config.getBool('DB_SSL', false),
23
+ * };
24
+ * }
25
+ * }
26
+ * ```
27
+ */
28
+ @Injectable()
29
+ export class ConfigService implements ConfigServiceInterface {
30
+ constructor(
31
+ @Inject(CONFIG_DRIVER)
32
+ private driver: ConfigDriver,
33
+ ) {}
34
+
35
+ /**
36
+ * Get configuration value
37
+ */
38
+ get<T = any>(key: string, defaultValue?: T): T | undefined {
39
+ return this.driver.get<T>(key, defaultValue);
40
+ }
41
+
42
+ /**
43
+ * Get configuration value or throw if not found
44
+ */
45
+ getOrThrow<T = any>(key: string): T {
46
+ const value = this.get<T>(key);
47
+ if (value === undefined) {
48
+ throw new Error(`Configuration key "${key}" is required but not set`);
49
+ }
50
+ return value;
51
+ }
52
+
53
+ /**
54
+ * Get string value
55
+ */
56
+ getString(key: string, defaultValue?: string): string | undefined {
57
+ const value = this.get(key, defaultValue);
58
+ return value !== undefined ? String(value) : undefined;
59
+ }
60
+
61
+ /**
62
+ * Get string value or throw
63
+ */
64
+ getStringOrThrow(key: string): string {
65
+ return String(this.getOrThrow(key));
66
+ }
67
+
68
+ /**
69
+ * Get number value
70
+ */
71
+ getNumber(key: string, defaultValue?: number): number | undefined {
72
+ const value = this.get(key, defaultValue);
73
+ if (value === undefined) {
74
+ return undefined;
75
+ }
76
+ const parsed = Number(value);
77
+ return isNaN(parsed) ? defaultValue : parsed;
78
+ }
79
+
80
+ /**
81
+ * Get number value or throw
82
+ */
83
+ getNumberOrThrow(key: string): number {
84
+ const value = this.getNumber(key);
85
+ if (value === undefined) {
86
+ throw new Error(`Configuration key "${key}" is required but not set`);
87
+ }
88
+ return value;
89
+ }
90
+
91
+ /**
92
+ * Get boolean value
93
+ * Treats 'true', '1', 'yes', 'on' as true
94
+ */
95
+ getBool(key: string, defaultValue?: boolean): boolean | undefined {
96
+ const value = this.get(key, defaultValue);
97
+ if (value === undefined) {
98
+ return undefined;
99
+ }
100
+ if (typeof value === "boolean") {
101
+ return value;
102
+ }
103
+ return ["true", "1", "yes", "on"].includes(String(value).toLowerCase());
104
+ }
105
+
106
+ /**
107
+ * Get boolean value or throw
108
+ */
109
+ getBoolOrThrow(key: string): boolean {
110
+ const value = this.getBool(key);
111
+ if (value === undefined) {
112
+ throw new Error(`Configuration key "${key}" is required but not set`);
113
+ }
114
+ return value;
115
+ }
116
+
117
+ /**
118
+ * Get array value (comma-separated string or actual array)
119
+ */
120
+ getArray(key: string, defaultValue?: string[]): string[] | undefined {
121
+ const value = this.get(key, defaultValue);
122
+ if (value === undefined) {
123
+ return undefined;
124
+ }
125
+ if (Array.isArray(value)) {
126
+ return value.map(String);
127
+ }
128
+ return String(value)
129
+ .split(",")
130
+ .map((v) => v.trim())
131
+ .filter(Boolean);
132
+ }
133
+
134
+ /**
135
+ * Get JSON value
136
+ */
137
+ getJson<T = any>(key: string, defaultValue?: T): T | undefined {
138
+ const value = this.get(key, defaultValue);
139
+ if (value === undefined) {
140
+ return undefined;
141
+ }
142
+ if (typeof value === "object") {
143
+ return value as T;
144
+ }
145
+ try {
146
+ return JSON.parse(String(value)) as T;
147
+ } catch {
148
+ return defaultValue;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Check if configuration key exists
154
+ */
155
+ has(key: string): boolean {
156
+ return this.driver.has(key);
157
+ }
158
+
159
+ /**
160
+ * Get all configuration values
161
+ */
162
+ all(): Record<string, any> {
163
+ return this.driver.all();
164
+ }
165
+
166
+ /**
167
+ * Clear cache (no-op since we don't cache in ConfigService)
168
+ */
169
+ clearCache(): void {
170
+ // No-op - caching should be done at a higher level if needed
171
+ }
172
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Services
3
+ */
4
+
5
+ export { ConfigService } from './config.service';
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Get nested value from object using dot notation
3
+ *
4
+ * @param obj - Source object
5
+ * @param path - Dot-notated path (e.g., 'database.host')
6
+ * @param defaultValue - Default value if path not found
7
+ * @returns Value at path or default value
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const config = { database: { host: 'localhost' } };
12
+ * getNestedValue(config, 'database.host'); // 'localhost'
13
+ * getNestedValue(config, 'database.port', 5432); // 5432
14
+ * ```
15
+ */
16
+ export function getNestedValue<T = any>(
17
+ obj: Record<string, any>,
18
+ path: string,
19
+ defaultValue?: T
20
+ ): T | undefined {
21
+ const keys = path.split('.');
22
+ let current: any = obj;
23
+
24
+ for (const key of keys) {
25
+ if (current === null || current === undefined) {
26
+ return defaultValue;
27
+ }
28
+ current = current[key];
29
+ }
30
+
31
+ return current !== undefined ? current : defaultValue;
32
+ }
33
+
34
+ /**
35
+ * Check if nested path exists in object
36
+ *
37
+ * @param obj - Source object
38
+ * @param path - Dot-notated path
39
+ * @returns True if path exists
40
+ */
41
+ export function hasNestedValue(
42
+ obj: Record<string, any>,
43
+ path: string
44
+ ): boolean {
45
+ const keys = path.split('.');
46
+ let current: any = obj;
47
+
48
+ for (const key of keys) {
49
+ if (current === null || current === undefined || !(key in current)) {
50
+ return false;
51
+ }
52
+ current = current[key];
53
+ }
54
+
55
+ return true;
56
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Utils
3
+ */
4
+
5
+ export { getNestedValue, hasNestedValue } from './get-nested-value.util';
6
+ export { loadConfigFile } from './load-config-file.util';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Load Config File Utility
3
+ *
4
+ * Dynamically loads and parses a config file.
5
+ */
6
+
7
+ /**
8
+ * Load and parse config file
9
+ *
10
+ * @param filePath - Absolute path to the config file
11
+ * @returns Parsed config object
12
+ */
13
+ export async function loadConfigFile(
14
+ filePath: string
15
+ ): Promise<Record<string, any>> {
16
+ try {
17
+ // Dynamic import of the config file
18
+ // @ts-ignore - Dynamic import path
19
+ const module = await import(/* @vite-ignore */ filePath);
20
+
21
+ // Extract config object (could be default export or named export)
22
+ const config = module.default || module;
23
+
24
+ // If it's a function, call it
25
+ if (typeof config === 'function') {
26
+ return await config();
27
+ }
28
+
29
+ return config;
30
+ } catch (error) {
31
+ console.warn(
32
+ `[vite-plugin-config] Failed to load config file: ${filePath}`,
33
+ error
34
+ );
35
+ return {};
36
+ }
37
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Scan Config Files Utility
3
+ *
4
+ * Scans and collects all .config.ts files based on the provided options.
5
+ */
6
+
7
+ import { glob } from 'glob';
8
+ import type { ViteConfigPluginOptions } from '@/interfaces/vite-config-plugin-options.interface';
9
+
10
+ /**
11
+ * Scan and collect all .config.ts files
12
+ *
13
+ * @param options - Plugin options containing scan configuration
14
+ * @returns Array of absolute file paths to config files
15
+ */
16
+ export async function scanConfigFiles(
17
+ options: ViteConfigPluginOptions
18
+ ): Promise<string[]> {
19
+ const {
20
+ configFilePattern = 'src/**/*.config.ts',
21
+ excludeDirs = ['node_modules', 'dist', 'build', '.git'],
22
+ root = process.cwd(),
23
+ } = options;
24
+
25
+ const patterns = Array.isArray(configFilePattern)
26
+ ? configFilePattern
27
+ : [configFilePattern];
28
+ const configFiles: string[] = [];
29
+
30
+ for (const pattern of patterns) {
31
+ const files = await glob(pattern, {
32
+ cwd: root,
33
+ absolute: true,
34
+ ignore: excludeDirs.map((dir) => `**/${dir}/**`),
35
+ });
36
+ configFiles.push(...files);
37
+ }
38
+
39
+ return configFiles;
40
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "extends": "@nesvel/typescript-config/base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": ".",
6
+ /* Silence TypeScript deprecation warnings */
7
+ "ignoreDeprecations": "6.0",
8
+ /* Module resolution - Override Nesvel's NodeNext for compatibility */
9
+ "module": "ESNext",
10
+ "moduleResolution": "bundler",
11
+ /* Decorators - Required for DI container */
12
+ "experimentalDecorators": true,
13
+ "emitDecoratorMetadata": true,
14
+ /* Build options */
15
+ "declaration": true,
16
+ "declarationMap": true,
17
+ "sourceMap": true,
18
+ "jsx": "react-jsx",
19
+ /* Testing */
20
+ "types": ["vitest/globals", "node"],
21
+ "baseUrl": ".",
22
+ "paths": {
23
+ "@/*": ["./src/*"]
24
+ }
25
+ },
26
+ "include": ["src/**/*", "__tests__/**/*"],
27
+ "exclude": ["node_modules", "dist", "config"]
28
+ }