@zamatica/configuration 0.1.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.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @zamatica/configuration
2
+
3
+ Framework-agnostic runtime configuration for TypeScript apps. Static-method service, builder-pattern parameter definitions, layered resolution (CLI > env > default), directory access checks, and Zod-validated YAML.
4
+
5
+ Designed to be available **before** any framework (NestJS, Express, etc.) is bootstrapped — no DI container required.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @zamatica/configuration
11
+ ```
12
+
13
+ ## Quick example
14
+
15
+ ```ts
16
+ import {
17
+ Configuration,
18
+ ConfigurationParameter,
19
+ ConfigurationService,
20
+ } from '@zamatica/configuration';
21
+ import { z } from 'zod';
22
+
23
+ @Configuration(
24
+ 'MyApp',
25
+ ConfigurationParameter.named('PORT')
26
+ .number()
27
+ .default(8080)
28
+ .description('HTTP listen port')
29
+ .required()
30
+ .build(),
31
+ ConfigurationParameter.named('CONFIG_DIR')
32
+ .directory()
33
+ .default('/etc/myapp')
34
+ .createIfMissing()
35
+ .build(),
36
+ ConfigurationParameter.named('SERVICES')
37
+ .yaml()
38
+ .schema(z.object({ services: z.array(z.string()) }))
39
+ .default('services.yaml')
40
+ .build(),
41
+ )
42
+ class MyApp {
43
+ static async start() {
44
+ ConfigurationService.setAppEnvVarPrefix('MYAPP');
45
+ ConfigurationService.initialize(); // throws if any required is missing
46
+
47
+ const port = ConfigurationService.get<number>('PORT');
48
+ const dir = ConfigurationService.resolveDirectory('CONFIG_DIR');
49
+ const services = ConfigurationService.resolveYaml<{ services: string[] }>('SERVICES');
50
+ /* ... */
51
+ }
52
+ }
53
+ ```
54
+
55
+ ## Resolution priority
56
+
57
+ For each registered parameter, `ConfigurationService.get()` consults:
58
+
59
+ 1. **CLI argument** — set externally via `ConfigurationService.setCliArgs({...})`. The argument key is the configuration key in `camelCase` (`PORT` → `port`, `LOG_LEVEL` → `logLevel`).
60
+ 2. **Environment variable** — `<APP_PREFIX>_<KEY>` (e.g. `MYAPP_PORT`). Set the prefix via `setAppEnvVarPrefix()`; defaults to empty (bare key) if not set.
61
+ 3. **Default** — declared via `.default(value)` on the builder.
62
+
63
+ If a parameter is `.required()` and none of the above provide a value, `get()` throws.
64
+
65
+ ## Parameter types
66
+
67
+ - **`.string()` / `.number()` / `.boolean()`** — simple primitives. Coerced from the source value (env vars and CLI strings → typed values).
68
+ - **`.directory()`** — string-valued; `resolveDirectory()` performs path resolution (relative paths joined under `CONFIGURATION_BASE_DIRECTORY`), existence/access checks, and optional `createIfMissing()`/`requireWritable()`.
69
+ - **`.yaml()`** — string-valued (the file path); `resolveYaml()` reads the file, parses YAML, and validates against the optional Zod schema. With a schema, the return type is narrowed via `z.infer<typeof schema>`.
70
+
71
+ ## Logger
72
+
73
+ A small `ConfigurationLogger` interface (debug/info/warn/error). Default implementation is `BufferingLogger` that holds entries in memory; pass any `ConfigurationLogger` (e.g. `new ConsoleLogger()` or a NestJS-bridged logger) to `setLogger()` and the buffer flushes to it. This makes pre-bootstrap logs available once the real logger comes up.
74
+
75
+ ## See also
76
+
77
+ - `@zamatica/bootstrap` — adds commander integration, app metadata, and `--generate-dot-env` on top of this library.
@@ -0,0 +1,41 @@
1
+ import { type ConfigurationLogger } from './logger.js';
2
+ import type { ConfigurationParameterDefinition, ConfigurationValuePrimitive } from './parameter-types.js';
3
+ export declare class ConfigurationService {
4
+ private static appEnvVarPrefix;
5
+ private static cliArgs;
6
+ private static buffer;
7
+ private static loggerImpl;
8
+ private static readonly registry;
9
+ private static readonly registryByTarget;
10
+ private static readonly resolvedDirectories;
11
+ private static readonly resolvedYaml;
12
+ private static initialized;
13
+ private constructor();
14
+ static setAppEnvVarPrefix(prefix: string): void;
15
+ static getAppEnvVarPrefix(): string;
16
+ static setCliArgs(args: Record<string, ConfigurationValuePrimitive | undefined>): void;
17
+ static setLogger(logger: ConfigurationLogger): void;
18
+ static useConsoleLogger(): void;
19
+ static register(parameters: ConfigurationParameterDefinition[], target: string): void;
20
+ static initialize(): void;
21
+ static isInitialized(): boolean;
22
+ static get<T extends ConfigurationValuePrimitive>(configurationKey: string): T;
23
+ static resolveDirectory(configurationKey: string): string;
24
+ static resolveYaml<TResult = unknown>(configurationKey: string): TResult;
25
+ static getRegistry(): readonly ConfigurationParameterDefinition[];
26
+ static getRegistryByTarget(): ReadonlyMap<string, readonly ConfigurationParameterDefinition[]>;
27
+ static environmentVariableName(configurationKey: string): string;
28
+ /**
29
+ * Test-only reset. Wipes all module-singleton state (registry, resolution
30
+ * caches, logger, app prefix). Not re-exported from `src/index.ts` — call sites
31
+ * must reach in via `ConfigurationService.__resetForTesting()` directly,
32
+ * which is intentional friction signaling "this is for vitest, not runtime."
33
+ *
34
+ * Production code must never call this; the underscore prefix and explicit
35
+ * `forTesting` suffix exist to make accidental use obvious in code review.
36
+ */
37
+ static __resetForTesting(): void;
38
+ private static coerce;
39
+ private static ensureDirectoryAccess;
40
+ }
41
+ //# sourceMappingURL=configuration-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configuration-service.d.ts","sourceRoot":"","sources":["../src/configuration-service.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,mBAAmB,EAGzB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,gCAAgC,EAChC,2BAA2B,EAE5B,MAAM,sBAAsB,CAAC;AAI9B,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,MAAM,CAAC,eAAe,CAAM;IACpC,OAAO,CAAC,MAAM,CAAC,OAAO,CAA+D;IACrF,OAAO,CAAC,MAAM,CAAC,MAAM,CAAyB;IAC9C,OAAO,CAAC,MAAM,CAAC,UAAU,CAAoD;IAE7E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAuD;IACvF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAyD;IAEjG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAA6B;IACxE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAA8B;IAElE,OAAO,CAAC,MAAM,CAAC,WAAW,CAAS;IAEnC,OAAO;IAIP,MAAM,CAAC,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/C,MAAM,CAAC,kBAAkB,IAAI,MAAM;IAInC,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,2BAA2B,GAAG,SAAS,CAAC,GAAG,IAAI;IAItF,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI;IAOnD,MAAM,CAAC,gBAAgB,IAAI,IAAI;IAI/B,MAAM,CAAC,QAAQ,CACb,UAAU,EAAE,gCAAgC,EAAE,EAC9C,MAAM,EAAE,MAAM,GACb,IAAI;IAqBP,MAAM,CAAC,UAAU,IAAI,IAAI;IAmBzB,MAAM,CAAC,aAAa,IAAI,OAAO;IAI/B,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,2BAA2B,EAAE,gBAAgB,EAAE,MAAM,GAAG,CAAC;IA0C9E,MAAM,CAAC,gBAAgB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM;IAiCzD,MAAM,CAAC,WAAW,CAAC,OAAO,GAAG,OAAO,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO;IA0DxE,MAAM,CAAC,WAAW,IAAI,SAAS,gCAAgC,EAAE;IAIjE,MAAM,CAAC,mBAAmB,IAAI,WAAW,CAAC,MAAM,EAAE,SAAS,gCAAgC,EAAE,CAAC;IAS9F,MAAM,CAAC,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM;IAMhE;;;;;;;;OAQG;IACH,MAAM,CAAC,iBAAiB,IAAI,IAAI;IAYhC,OAAO,CAAC,MAAM,CAAC,MAAM;IA2BrB,OAAO,CAAC,MAAM,CAAC,qBAAqB;CAyBrC"}
@@ -0,0 +1,254 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { camelizeEnvVarName } from '@zamatica/util';
4
+ import { parse as parseYAML } from 'yaml';
5
+ import { BufferingLogger, ConsoleLogger, flushBuffer, } from './logger.js';
6
+ const CONFIG_BASE_DIR_KEY = 'CONFIGURATION_BASE_DIRECTORY';
7
+ export class ConfigurationService {
8
+ static appEnvVarPrefix = '';
9
+ static cliArgs = {};
10
+ static buffer = new BufferingLogger();
11
+ static loggerImpl = ConfigurationService.buffer;
12
+ static registry = new Map();
13
+ static registryByTarget = new Map();
14
+ static resolvedDirectories = new Map();
15
+ static resolvedYaml = new Map();
16
+ static initialized = false;
17
+ constructor() {
18
+ /* static-only */
19
+ }
20
+ static setAppEnvVarPrefix(prefix) {
21
+ ConfigurationService.appEnvVarPrefix = prefix.toUpperCase();
22
+ }
23
+ static getAppEnvVarPrefix() {
24
+ return ConfigurationService.appEnvVarPrefix;
25
+ }
26
+ static setCliArgs(args) {
27
+ ConfigurationService.cliArgs = { ...args };
28
+ }
29
+ static setLogger(logger) {
30
+ if (ConfigurationService.loggerImpl === ConfigurationService.buffer) {
31
+ flushBuffer(ConfigurationService.buffer, logger);
32
+ }
33
+ ConfigurationService.loggerImpl = logger;
34
+ }
35
+ static useConsoleLogger() {
36
+ ConfigurationService.setLogger(new ConsoleLogger());
37
+ }
38
+ static register(parameters, target) {
39
+ for (const parameter of parameters) {
40
+ const existing = ConfigurationService.registry.get(parameter.configurationKey);
41
+ if (existing) {
42
+ throw new Error(`Configuration key '${parameter.configurationKey}' is already registered.`);
43
+ }
44
+ ConfigurationService.registry.set(parameter.configurationKey, parameter);
45
+ const list = ConfigurationService.registryByTarget.get(target) ?? [];
46
+ list.push(parameter);
47
+ ConfigurationService.registryByTarget.set(target, list);
48
+ ConfigurationService.loggerImpl.debug(`[configuration] registered ${parameter.kind} key '${parameter.configurationKey}' for target '${target}' (default=${String(parameter.default)})`);
49
+ }
50
+ }
51
+ static initialize() {
52
+ const errors = [];
53
+ for (const parameter of ConfigurationService.registry.values()) {
54
+ if (parameter.required && parameter.default === undefined) {
55
+ try {
56
+ ConfigurationService.get(parameter.configurationKey);
57
+ }
58
+ catch (error) {
59
+ errors.push(error instanceof Error ? error.message : String(error));
60
+ }
61
+ }
62
+ }
63
+ if (errors.length > 0) {
64
+ throw new Error(`Required configuration not satisfied:\n - ${errors.join('\n - ')}`);
65
+ }
66
+ ConfigurationService.initialized = true;
67
+ }
68
+ static isInitialized() {
69
+ return ConfigurationService.initialized;
70
+ }
71
+ static get(configurationKey) {
72
+ const parameter = ConfigurationService.registry.get(configurationKey);
73
+ if (!parameter) {
74
+ throw new Error(`Configuration key '${configurationKey}' is not registered.`);
75
+ }
76
+ const argumentKey = camelizeEnvVarName(configurationKey);
77
+ const envVarName = ConfigurationService.environmentVariableName(configurationKey);
78
+ let raw;
79
+ let source;
80
+ if (Object.prototype.hasOwnProperty.call(ConfigurationService.cliArgs, argumentKey) &&
81
+ ConfigurationService.cliArgs[argumentKey] !== undefined) {
82
+ raw = ConfigurationService.cliArgs[argumentKey];
83
+ source = 'cli';
84
+ }
85
+ else if (process.env[envVarName] !== undefined && process.env[envVarName] !== '') {
86
+ raw = process.env[envVarName];
87
+ source = 'env';
88
+ }
89
+ else if (parameter.default !== undefined) {
90
+ raw = parameter.default;
91
+ source = 'default';
92
+ }
93
+ if (raw === undefined) {
94
+ if (parameter.required) {
95
+ throw new Error(`No value found for required configuration key '${configurationKey}' in CLI argument '${argumentKey}', environment variable '${envVarName}', or default.`);
96
+ }
97
+ return undefined;
98
+ }
99
+ ConfigurationService.loggerImpl.debug(`[configuration] '${configurationKey}' resolved from ${source}: ${String(raw)}`);
100
+ return ConfigurationService.coerce(raw, parameter);
101
+ }
102
+ static resolveDirectory(configurationKey) {
103
+ const cached = ConfigurationService.resolvedDirectories.get(configurationKey);
104
+ if (cached !== undefined)
105
+ return cached;
106
+ const parameter = ConfigurationService.registry.get(configurationKey);
107
+ if (!parameter) {
108
+ throw new Error(`Configuration key '${configurationKey}' is not registered.`);
109
+ }
110
+ if (parameter.kind !== 'directory') {
111
+ throw new Error(`Configuration key '${configurationKey}' is not a directory parameter.`);
112
+ }
113
+ const configured = ConfigurationService.get(configurationKey);
114
+ if (configured === undefined || configured === '') {
115
+ if (parameter.required) {
116
+ throw new Error(`Required directory configuration '${configurationKey}' is empty.`);
117
+ }
118
+ return '';
119
+ }
120
+ let resolved = configured;
121
+ if (!path.isAbsolute(resolved) && configurationKey !== CONFIG_BASE_DIR_KEY) {
122
+ const base = ConfigurationService.resolveDirectory(CONFIG_BASE_DIR_KEY);
123
+ resolved = path.join(base, resolved);
124
+ }
125
+ else {
126
+ resolved = path.resolve(resolved);
127
+ }
128
+ ConfigurationService.ensureDirectoryAccess(resolved, parameter);
129
+ ConfigurationService.resolvedDirectories.set(configurationKey, resolved);
130
+ return resolved;
131
+ }
132
+ static resolveYaml(configurationKey) {
133
+ const cached = ConfigurationService.resolvedYaml.get(configurationKey);
134
+ if (cached !== undefined)
135
+ return cached;
136
+ const parameter = ConfigurationService.registry.get(configurationKey);
137
+ if (!parameter) {
138
+ throw new Error(`Configuration key '${configurationKey}' is not registered.`);
139
+ }
140
+ if (parameter.kind !== 'yaml') {
141
+ throw new Error(`Configuration key '${configurationKey}' is not a yaml parameter.`);
142
+ }
143
+ const configured = ConfigurationService.get(configurationKey);
144
+ if (configured === undefined || configured === '') {
145
+ throw new Error(`YAML configuration '${configurationKey}' has no value.`);
146
+ }
147
+ let resolved = configured;
148
+ if (!path.isAbsolute(resolved)) {
149
+ const base = ConfigurationService.resolveDirectory(CONFIG_BASE_DIR_KEY);
150
+ resolved = path.join(base, resolved);
151
+ }
152
+ else {
153
+ resolved = path.resolve(resolved);
154
+ }
155
+ if (!fs.existsSync(resolved)) {
156
+ throw new Error(`YAML file for '${configurationKey}' not found: ${resolved}`);
157
+ }
158
+ const stats = fs.statSync(resolved);
159
+ if (!stats.isFile()) {
160
+ throw new Error(`YAML path for '${configurationKey}' is not a file: ${resolved}`);
161
+ }
162
+ const raw = fs.readFileSync(resolved, 'utf8');
163
+ const parsed = parseYAML(raw);
164
+ if (parameter.schema !== undefined) {
165
+ const result = parameter.schema.safeParse(parsed);
166
+ if (!result.success) {
167
+ throw new Error(`YAML validation failed for '${configurationKey}' (${resolved}): ${JSON.stringify(result.error.issues, null, 2)}`);
168
+ }
169
+ ConfigurationService.resolvedYaml.set(configurationKey, result.data);
170
+ return result.data;
171
+ }
172
+ ConfigurationService.loggerImpl.warn(`[configuration] no schema provided for YAML key '${configurationKey}'; returning unvalidated parsed data.`);
173
+ ConfigurationService.resolvedYaml.set(configurationKey, parsed);
174
+ return parsed;
175
+ }
176
+ static getRegistry() {
177
+ return Array.from(ConfigurationService.registry.values());
178
+ }
179
+ static getRegistryByTarget() {
180
+ return new Map(Array.from(ConfigurationService.registryByTarget.entries()).map(([target, params]) => [
181
+ target,
182
+ params.slice(),
183
+ ]));
184
+ }
185
+ static environmentVariableName(configurationKey) {
186
+ return ConfigurationService.appEnvVarPrefix
187
+ ? `${ConfigurationService.appEnvVarPrefix}_${configurationKey}`
188
+ : configurationKey;
189
+ }
190
+ /**
191
+ * Test-only reset. Wipes all module-singleton state (registry, resolution
192
+ * caches, logger, app prefix). Not re-exported from `src/index.ts` — call sites
193
+ * must reach in via `ConfigurationService.__resetForTesting()` directly,
194
+ * which is intentional friction signaling "this is for vitest, not runtime."
195
+ *
196
+ * Production code must never call this; the underscore prefix and explicit
197
+ * `forTesting` suffix exist to make accidental use obvious in code review.
198
+ */
199
+ static __resetForTesting() {
200
+ ConfigurationService.appEnvVarPrefix = '';
201
+ ConfigurationService.cliArgs = {};
202
+ ConfigurationService.buffer = new BufferingLogger();
203
+ ConfigurationService.loggerImpl = ConfigurationService.buffer;
204
+ ConfigurationService.registry.clear();
205
+ ConfigurationService.registryByTarget.clear();
206
+ ConfigurationService.resolvedDirectories.clear();
207
+ ConfigurationService.resolvedYaml.clear();
208
+ ConfigurationService.initialized = false;
209
+ }
210
+ static coerce(value, parameter) {
211
+ switch (parameter.constructor) {
212
+ case Number: {
213
+ if (typeof value === 'number')
214
+ return value;
215
+ if (typeof value === 'boolean')
216
+ return value ? 1 : 0;
217
+ const parsed = Number.parseFloat(value);
218
+ if (Number.isNaN(parsed)) {
219
+ throw new Error(`Cannot coerce value '${value}' to number for '${parameter.configurationKey}'.`);
220
+ }
221
+ return parsed;
222
+ }
223
+ case Boolean: {
224
+ if (typeof value === 'boolean')
225
+ return value;
226
+ if (typeof value === 'number')
227
+ return value !== 0;
228
+ return value.toLowerCase() === 'true' || value === '1';
229
+ }
230
+ default:
231
+ // Default covers String and any future constructor. Stringify whatever value we got.
232
+ return String(value);
233
+ }
234
+ }
235
+ static ensureDirectoryAccess(resolved, parameter) {
236
+ const exists = fs.existsSync(resolved);
237
+ if (!exists) {
238
+ if (parameter.createIfMissing) {
239
+ fs.mkdirSync(resolved, { recursive: true });
240
+ }
241
+ else {
242
+ throw new Error(`Directory '${resolved}' for configuration '${parameter.configurationKey}' does not exist and createIfMissing is false.`);
243
+ }
244
+ }
245
+ const stats = fs.statSync(resolved);
246
+ if (!stats.isDirectory()) {
247
+ throw new Error(`Path '${resolved}' for configuration '${parameter.configurationKey}' is not a directory.`);
248
+ }
249
+ const mode = parameter.requireWritable
250
+ ? fs.constants.R_OK | fs.constants.W_OK
251
+ : fs.constants.R_OK;
252
+ fs.accessSync(resolved, mode);
253
+ }
254
+ }
@@ -0,0 +1,27 @@
1
+ import type { ConfigurationParameterDefinition } from './parameter-types.js';
2
+ type AnyClass = Function;
3
+ /**
4
+ * Class decorator that registers a set of configuration parameters and groups
5
+ * them under a target name (used by `print-config` / `getRegistryByTarget()`
6
+ * for display, and by error messages).
7
+ *
8
+ * Pass the target name as the first argument:
9
+ *
10
+ * @Configuration('HttpListenerConfig', ...HTTP_LISTENER_PARAMETERS)
11
+ * class HttpListenerConfig { ... }
12
+ *
13
+ * Why explicit instead of `target.name`? The TypeScript `experimentalDecorators`
14
+ * emit produces `let Foo = class Foo { ... }` which, after a JS bundler hoists
15
+ * the `let` to the module scope, gets the inner class expression renamed to
16
+ * `Foo2` to avoid the outer/inner binding collision. A decorator reading
17
+ * `target.name` would then register against `Foo2`, surfacing nonsense
18
+ * suffixes in `print-config` output. Passing the name explicitly sidesteps
19
+ * the whole problem and survives any future minifier/bundler renames.
20
+ *
21
+ * If the first argument is omitted (legacy callers), the decorator falls back
22
+ * to `target.name` — which works fine when the class is bundled from source
23
+ * but is fragile when bundled from prebuilt `dist/` JS.
24
+ */
25
+ export declare function Configuration(nameOrFirstParameter: string | ConfigurationParameterDefinition, ...rest: ConfigurationParameterDefinition[]): (target: AnyClass) => void;
26
+ export {};
27
+ //# sourceMappingURL=decorators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,sBAAsB,CAAC;AAG7E,KAAK,QAAQ,GAAG,QAAQ,CAAC;AAEzB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAC3B,oBAAoB,EAAE,MAAM,GAAG,gCAAgC,EAC/D,GAAG,IAAI,EAAE,gCAAgC,EAAE,GAC1C,CAAC,MAAM,EAAE,QAAQ,KAAK,IAAI,CAU5B"}
@@ -0,0 +1,32 @@
1
+ import { ConfigurationService } from './configuration-service.js';
2
+ /**
3
+ * Class decorator that registers a set of configuration parameters and groups
4
+ * them under a target name (used by `print-config` / `getRegistryByTarget()`
5
+ * for display, and by error messages).
6
+ *
7
+ * Pass the target name as the first argument:
8
+ *
9
+ * @Configuration('HttpListenerConfig', ...HTTP_LISTENER_PARAMETERS)
10
+ * class HttpListenerConfig { ... }
11
+ *
12
+ * Why explicit instead of `target.name`? The TypeScript `experimentalDecorators`
13
+ * emit produces `let Foo = class Foo { ... }` which, after a JS bundler hoists
14
+ * the `let` to the module scope, gets the inner class expression renamed to
15
+ * `Foo2` to avoid the outer/inner binding collision. A decorator reading
16
+ * `target.name` would then register against `Foo2`, surfacing nonsense
17
+ * suffixes in `print-config` output. Passing the name explicitly sidesteps
18
+ * the whole problem and survives any future minifier/bundler renames.
19
+ *
20
+ * If the first argument is omitted (legacy callers), the decorator falls back
21
+ * to `target.name` — which works fine when the class is bundled from source
22
+ * but is fragile when bundled from prebuilt `dist/` JS.
23
+ */
24
+ export function Configuration(nameOrFirstParameter, ...rest) {
25
+ const explicitName = typeof nameOrFirstParameter === 'string' ? nameOrFirstParameter : undefined;
26
+ const parameters = explicitName === undefined
27
+ ? [nameOrFirstParameter, ...rest]
28
+ : rest;
29
+ return (target) => {
30
+ ConfigurationService.register(parameters, explicitName ?? target.name);
31
+ };
32
+ }
@@ -0,0 +1,6 @@
1
+ export { Configuration } from './decorators.js';
2
+ export { ConfigurationService } from './configuration-service.js';
3
+ export { ConfigurationParameter, type AnyConfigurationParameterBuilder, type AnyConfigurationParameterDefinition, } from './parameter-builder.js';
4
+ export { type ConfigurationParameterDefinition, type ConfigurationParameterDefinitionBase, type ConfigurationValuePrimitive, type ConfigurationValueConstructor, type SimpleConfigurationParameterDefinition, type DirectoryConfigurationParameterDefinition, type YamlConfigurationParameterDefinition, } from './parameter-types.js';
5
+ export { type ConfigurationLogger, type LogLevel, type BufferedLogEntry, ConsoleLogger, BufferingLogger, flushBuffer, } from './logger.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EACL,sBAAsB,EACtB,KAAK,gCAAgC,EACrC,KAAK,mCAAmC,GACzC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,gCAAgC,EACrC,KAAK,oCAAoC,EACzC,KAAK,2BAA2B,EAChC,KAAK,6BAA6B,EAClC,KAAK,sCAAsC,EAC3C,KAAK,yCAAyC,EAC9C,KAAK,oCAAoC,GAC1C,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,aAAa,EACb,eAAe,EACf,WAAW,GACZ,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { Configuration } from './decorators.js';
2
+ export { ConfigurationService } from './configuration-service.js';
3
+ export { ConfigurationParameter, } from './parameter-builder.js';
4
+ export { ConsoleLogger, BufferingLogger, flushBuffer, } from './logger.js';
@@ -0,0 +1,2 @@
1
+ export declare function camelizeEnvVarName(envVarName: string): string;
2
+ //# sourceMappingURL=camelize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camelize.d.ts","sourceRoot":"","sources":["../../src/internal/camelize.ts"],"names":[],"mappings":"AAKA,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAM7D"}
@@ -0,0 +1,12 @@
1
+ function capitalizeFirstLetter(word) {
2
+ if (word.length === 0)
3
+ return '';
4
+ return word.charAt(0).toUpperCase() + word.slice(1);
5
+ }
6
+ export function camelizeEnvVarName(envVarName) {
7
+ return envVarName
8
+ .toLowerCase()
9
+ .split('_')
10
+ .map((segment, index) => (index === 0 ? segment : capitalizeFirstLetter(segment)))
11
+ .join('');
12
+ }
@@ -0,0 +1,31 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ export interface ConfigurationLogger {
3
+ debug(message: string, ...args: unknown[]): void;
4
+ info(message: string, ...args: unknown[]): void;
5
+ warn(message: string, ...args: unknown[]): void;
6
+ error(message: string, ...args: unknown[]): void;
7
+ }
8
+ export interface BufferedLogEntry {
9
+ level: LogLevel;
10
+ message: string;
11
+ args: unknown[];
12
+ timestamp: Date;
13
+ }
14
+ export declare class ConsoleLogger implements ConfigurationLogger {
15
+ debug(message: string, ...args: unknown[]): void;
16
+ info(message: string, ...args: unknown[]): void;
17
+ warn(message: string, ...args: unknown[]): void;
18
+ error(message: string, ...args: unknown[]): void;
19
+ }
20
+ export declare class BufferingLogger implements ConfigurationLogger {
21
+ private readonly entries;
22
+ debug(message: string, ...args: unknown[]): void;
23
+ info(message: string, ...args: unknown[]): void;
24
+ warn(message: string, ...args: unknown[]): void;
25
+ error(message: string, ...args: unknown[]): void;
26
+ private append;
27
+ drain(): readonly BufferedLogEntry[];
28
+ size(): number;
29
+ }
30
+ export declare function flushBuffer(buffer: BufferingLogger, target: ConfigurationLogger): void;
31
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACjD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,QAAQ,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,qBAAa,aAAc,YAAW,mBAAmB;IACvD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAGhD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAG/C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAG/C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;CAGjD;AAED,qBAAa,eAAgB,YAAW,mBAAmB;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAGhD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAG/C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAG/C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAIhD,OAAO,CAAC,MAAM;IAId,KAAK,IAAI,SAAS,gBAAgB,EAAE;IAMpC,IAAI,IAAI,MAAM;CAGf;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAItF"}
package/dist/logger.js ADDED
@@ -0,0 +1,45 @@
1
+ export class ConsoleLogger {
2
+ debug(message, ...args) {
3
+ console.debug(message, ...args);
4
+ }
5
+ info(message, ...args) {
6
+ console.info(message, ...args);
7
+ }
8
+ warn(message, ...args) {
9
+ console.warn(message, ...args);
10
+ }
11
+ error(message, ...args) {
12
+ console.error(message, ...args);
13
+ }
14
+ }
15
+ export class BufferingLogger {
16
+ entries = [];
17
+ debug(message, ...args) {
18
+ this.append('debug', message, args);
19
+ }
20
+ info(message, ...args) {
21
+ this.append('info', message, args);
22
+ }
23
+ warn(message, ...args) {
24
+ this.append('warn', message, args);
25
+ }
26
+ error(message, ...args) {
27
+ this.append('error', message, args);
28
+ }
29
+ append(level, message, args) {
30
+ this.entries.push({ level, message, args, timestamp: new Date() });
31
+ }
32
+ drain() {
33
+ const drained = this.entries.slice();
34
+ this.entries.length = 0;
35
+ return drained;
36
+ }
37
+ size() {
38
+ return this.entries.length;
39
+ }
40
+ }
41
+ export function flushBuffer(buffer, target) {
42
+ for (const entry of buffer.drain()) {
43
+ target[entry.level](entry.message, ...entry.args);
44
+ }
45
+ }
@@ -0,0 +1,59 @@
1
+ import type { z } from 'zod';
2
+ import type { ConfigurationParameterDefinition, ConfigurationValueConstructor, ConfigurationValuePrimitive, DirectoryConfigurationParameterDefinition, SimpleConfigurationParameterDefinition, YamlConfigurationParameterDefinition } from './parameter-types.js';
3
+ interface BaseState {
4
+ key: string;
5
+ description: string;
6
+ required: boolean;
7
+ default: ConfigurationValuePrimitive | undefined;
8
+ }
9
+ /**
10
+ * Common methods shared by every parameter builder. Subclasses add type-specific
11
+ * methods (e.g. `requireWritable()` on directory builders) and implement `build()`.
12
+ *
13
+ * The builder mutates `this.state` and returns `this` for chaining; subclasses
14
+ * preserve that pattern so chains compose cleanly.
15
+ */
16
+ declare abstract class ParameterBuilderBase<TSelf extends ParameterBuilderBase<TSelf, TDef>, TDef> {
17
+ protected state: BaseState;
18
+ protected constructor(state: BaseState);
19
+ description(text: string): TSelf;
20
+ default(value: ConfigurationValuePrimitive): TSelf;
21
+ required(): TSelf;
22
+ optional(): TSelf;
23
+ abstract build(): TDef;
24
+ }
25
+ declare class SimpleParameterBuilder extends ParameterBuilderBase<SimpleParameterBuilder, SimpleConfigurationParameterDefinition> {
26
+ private readonly constructorFn;
27
+ constructor(state: BaseState, constructorFn: ConfigurationValueConstructor);
28
+ build(): SimpleConfigurationParameterDefinition;
29
+ }
30
+ declare class DirectoryParameterBuilder extends ParameterBuilderBase<DirectoryParameterBuilder, DirectoryConfigurationParameterDefinition> {
31
+ private requireWritableFlag;
32
+ private createIfMissingFlag;
33
+ constructor(state: BaseState);
34
+ requireWritable(): this;
35
+ createIfMissing(): this;
36
+ build(): DirectoryConfigurationParameterDefinition;
37
+ }
38
+ declare class YamlParameterBuilder<TSchema extends z.ZodTypeAny = z.ZodTypeAny> extends ParameterBuilderBase<YamlParameterBuilder<TSchema>, YamlConfigurationParameterDefinition<TSchema>> {
39
+ private schemaValue;
40
+ constructor(state: BaseState);
41
+ schema<TNewSchema extends z.ZodTypeAny>(schema: TNewSchema): YamlParameterBuilder<TNewSchema>;
42
+ build(): YamlConfigurationParameterDefinition<TSchema>;
43
+ }
44
+ declare class ParameterTypeSelector {
45
+ private readonly state;
46
+ constructor(state: BaseState);
47
+ string(): SimpleParameterBuilder;
48
+ number(): SimpleParameterBuilder;
49
+ boolean(): SimpleParameterBuilder;
50
+ directory(): DirectoryParameterBuilder;
51
+ yaml(): YamlParameterBuilder;
52
+ }
53
+ export declare const ConfigurationParameter: {
54
+ named(key: string): ParameterTypeSelector;
55
+ };
56
+ export type AnyConfigurationParameterBuilder = SimpleParameterBuilder | DirectoryParameterBuilder | YamlParameterBuilder;
57
+ export type AnyConfigurationParameterDefinition = ConfigurationParameterDefinition;
58
+ export {};
59
+ //# sourceMappingURL=parameter-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parameter-builder.d.ts","sourceRoot":"","sources":["../src/parameter-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EACV,gCAAgC,EAChC,6BAA6B,EAC7B,2BAA2B,EAC3B,yCAAyC,EACzC,sCAAsC,EACtC,oCAAoC,EACrC,MAAM,sBAAsB,CAAC;AAY9B,UAAU,SAAS;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,2BAA2B,GAAG,SAAS,CAAC;CAClD;AAED;;;;;;GAMG;AACH,uBAAe,oBAAoB,CAAC,KAAK,SAAS,oBAAoB,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI;IACvF,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC;IAE3B,SAAS,aAAa,KAAK,EAAE,SAAS;IAItC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK;IAKhC,OAAO,CAAC,KAAK,EAAE,2BAA2B,GAAG,KAAK;IAKlD,QAAQ,IAAI,KAAK;IAKjB,QAAQ,IAAI,KAAK;IAKjB,QAAQ,CAAC,KAAK,IAAI,IAAI;CACvB;AAED,cAAM,sBAAuB,SAAQ,oBAAoB,CACvD,sBAAsB,EACtB,sCAAsC,CACvC;IAC+B,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAhD,KAAK,EAAE,SAAS,EAAmB,aAAa,EAAE,6BAA6B;IAI3F,KAAK,IAAI,sCAAsC;CAUhD;AAED,cAAM,yBAA0B,SAAQ,oBAAoB,CAC1D,yBAAyB,EACzB,yCAAyC,CAC1C;IACC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,mBAAmB,CAAS;gBAExB,KAAK,EAAE,SAAS;IAI5B,eAAe,IAAI,IAAI;IAKvB,eAAe,IAAI,IAAI;IAKvB,KAAK,IAAI,yCAAyC;CAYnD;AAED,cAAM,oBAAoB,CAAC,OAAO,SAAS,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAE,SAAQ,oBAAoB,CAClG,oBAAoB,CAAC,OAAO,CAAC,EAC7B,oCAAoC,CAAC,OAAO,CAAC,CAC9C;IACC,OAAO,CAAC,WAAW,CAAsB;gBAE7B,KAAK,EAAE,SAAS;IAI5B,MAAM,CAAC,UAAU,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,GAAG,oBAAoB,CAAC,UAAU,CAAC;IAM7F,KAAK,IAAI,oCAAoC,CAAC,OAAO,CAAC;CAWvD;AAED,cAAM,qBAAqB;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,SAAS;IAE7C,MAAM,IAAI,sBAAsB;IAIhC,MAAM,IAAI,sBAAsB;IAIhC,OAAO,IAAI,sBAAsB;IAIjC,SAAS,IAAI,yBAAyB;IAItC,IAAI,IAAI,oBAAoB;CAG7B;AAED,eAAO,MAAM,sBAAsB;eACtB,MAAM,GAAG,qBAAqB;CAS1C,CAAC;AAEF,MAAM,MAAM,gCAAgC,GACxC,sBAAsB,GACtB,yBAAyB,GACzB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,mCAAmC,GAAG,gCAAgC,CAAC"}