@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.
@@ -0,0 +1,133 @@
1
+ const ENV_VAR_NAME_REGEX = /^[A-Z]([A-Z0-9_]*[A-Z0-9])?$/;
2
+ function assertValidConfigurationKey(key) {
3
+ if (!ENV_VAR_NAME_REGEX.test(key)) {
4
+ throw new Error(`Invalid configuration key '${key}': must match SCREAMING_SNAKE_CASE (env-var convention).`);
5
+ }
6
+ }
7
+ /**
8
+ * Common methods shared by every parameter builder. Subclasses add type-specific
9
+ * methods (e.g. `requireWritable()` on directory builders) and implement `build()`.
10
+ *
11
+ * The builder mutates `this.state` and returns `this` for chaining; subclasses
12
+ * preserve that pattern so chains compose cleanly.
13
+ */
14
+ class ParameterBuilderBase {
15
+ state;
16
+ constructor(state) {
17
+ this.state = state;
18
+ }
19
+ description(text) {
20
+ this.state = { ...this.state, description: text };
21
+ return this;
22
+ }
23
+ default(value) {
24
+ this.state = { ...this.state, default: value };
25
+ return this;
26
+ }
27
+ required() {
28
+ this.state = { ...this.state, required: true };
29
+ return this;
30
+ }
31
+ optional() {
32
+ this.state = { ...this.state, required: false };
33
+ return this;
34
+ }
35
+ }
36
+ class SimpleParameterBuilder extends ParameterBuilderBase {
37
+ constructorFn;
38
+ constructor(state, constructorFn) {
39
+ super(state);
40
+ this.constructorFn = constructorFn;
41
+ }
42
+ build() {
43
+ return {
44
+ kind: 'simple',
45
+ configurationKey: this.state.key,
46
+ description: this.state.description,
47
+ required: this.state.required,
48
+ default: this.state.default,
49
+ constructor: this.constructorFn,
50
+ };
51
+ }
52
+ }
53
+ class DirectoryParameterBuilder extends ParameterBuilderBase {
54
+ requireWritableFlag = false;
55
+ createIfMissingFlag = false;
56
+ constructor(state) {
57
+ super(state);
58
+ }
59
+ requireWritable() {
60
+ this.requireWritableFlag = true;
61
+ return this;
62
+ }
63
+ createIfMissing() {
64
+ this.createIfMissingFlag = true;
65
+ return this;
66
+ }
67
+ build() {
68
+ return {
69
+ kind: 'directory',
70
+ configurationKey: this.state.key,
71
+ description: this.state.description,
72
+ required: this.state.required,
73
+ default: this.state.default,
74
+ constructor: String,
75
+ requireWritable: this.requireWritableFlag,
76
+ createIfMissing: this.createIfMissingFlag,
77
+ };
78
+ }
79
+ }
80
+ class YamlParameterBuilder extends ParameterBuilderBase {
81
+ schemaValue;
82
+ constructor(state) {
83
+ super(state);
84
+ }
85
+ schema(schema) {
86
+ const next = new YamlParameterBuilder(this.state);
87
+ next.schemaValue = schema;
88
+ return next;
89
+ }
90
+ build() {
91
+ return {
92
+ kind: 'yaml',
93
+ configurationKey: this.state.key,
94
+ description: this.state.description,
95
+ required: this.state.required,
96
+ default: this.state.default,
97
+ constructor: String,
98
+ schema: this.schemaValue,
99
+ };
100
+ }
101
+ }
102
+ class ParameterTypeSelector {
103
+ state;
104
+ constructor(state) {
105
+ this.state = state;
106
+ }
107
+ string() {
108
+ return new SimpleParameterBuilder(this.state, String);
109
+ }
110
+ number() {
111
+ return new SimpleParameterBuilder(this.state, Number);
112
+ }
113
+ boolean() {
114
+ return new SimpleParameterBuilder(this.state, Boolean);
115
+ }
116
+ directory() {
117
+ return new DirectoryParameterBuilder(this.state);
118
+ }
119
+ yaml() {
120
+ return new YamlParameterBuilder(this.state);
121
+ }
122
+ }
123
+ export const ConfigurationParameter = {
124
+ named(key) {
125
+ assertValidConfigurationKey(key);
126
+ return new ParameterTypeSelector({
127
+ key,
128
+ description: '',
129
+ required: false,
130
+ default: undefined,
131
+ });
132
+ },
133
+ };
@@ -0,0 +1,26 @@
1
+ import type { z } from 'zod';
2
+ export type ConfigurationValuePrimitive = string | number | boolean;
3
+ export type ConfigurationValueConstructor = StringConstructor | NumberConstructor | BooleanConstructor;
4
+ export interface ConfigurationParameterDefinitionBase {
5
+ configurationKey: string;
6
+ description: string;
7
+ required: boolean;
8
+ default: ConfigurationValuePrimitive | undefined;
9
+ }
10
+ export interface SimpleConfigurationParameterDefinition extends ConfigurationParameterDefinitionBase {
11
+ kind: 'simple';
12
+ constructor: ConfigurationValueConstructor;
13
+ }
14
+ export interface DirectoryConfigurationParameterDefinition extends ConfigurationParameterDefinitionBase {
15
+ kind: 'directory';
16
+ constructor: StringConstructor;
17
+ requireWritable: boolean;
18
+ createIfMissing: boolean;
19
+ }
20
+ export interface YamlConfigurationParameterDefinition<TSchema extends z.ZodTypeAny = z.ZodTypeAny> extends ConfigurationParameterDefinitionBase {
21
+ kind: 'yaml';
22
+ constructor: StringConstructor;
23
+ schema: TSchema | undefined;
24
+ }
25
+ export type ConfigurationParameterDefinition = SimpleConfigurationParameterDefinition | DirectoryConfigurationParameterDefinition | YamlConfigurationParameterDefinition;
26
+ //# sourceMappingURL=parameter-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parameter-types.d.ts","sourceRoot":"","sources":["../src/parameter-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEpE,MAAM,MAAM,6BAA6B,GACrC,iBAAiB,GACjB,iBAAiB,GACjB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,oCAAoC;IACnD,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,2BAA2B,GAAG,SAAS,CAAC;CAClD;AAED,MAAM,WAAW,sCACf,SAAQ,oCAAoC;IAC5C,IAAI,EAAE,QAAQ,CAAC;IACf,WAAW,EAAE,6BAA6B,CAAC;CAC5C;AAED,MAAM,WAAW,yCACf,SAAQ,oCAAoC;IAC5C,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,oCAAoC,CAAC,OAAO,SAAS,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAC/F,SAAQ,oCAAoC;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,iBAAiB,CAAC;IAC/B,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B;AAED,MAAM,MAAM,gCAAgC,GACxC,sCAAsC,GACtC,yCAAyC,GACzC,oCAAoC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@zamatica/configuration",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic runtime configuration registry with builder-pattern parameter definitions, layered resolution (CLI > env > default), directory access checks, and Zod-validated YAML.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ "./package.json": "./package.json",
12
+ ".": {
13
+ "@zamatica/source": "./src/index.ts",
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "src",
22
+ "!**/*.tsbuildinfo",
23
+ "!**/*.spec.*",
24
+ "!**/*.test.*"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/zoobroker/zamatica-libs.git",
29
+ "directory": "libs/configuration"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@zamatica/util": "workspace:^",
36
+ "tslib": "^2.3.0",
37
+ "yaml": "^2.8.3",
38
+ "zod": "^4.3.6"
39
+ }
40
+ }
@@ -0,0 +1,333 @@
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
+
6
+ import {
7
+ BufferingLogger,
8
+ type ConfigurationLogger,
9
+ ConsoleLogger,
10
+ flushBuffer,
11
+ } from './logger.js';
12
+ import type {
13
+ ConfigurationParameterDefinition,
14
+ ConfigurationValuePrimitive,
15
+ DirectoryConfigurationParameterDefinition,
16
+ } from './parameter-types.js';
17
+
18
+ const CONFIG_BASE_DIR_KEY = 'CONFIGURATION_BASE_DIRECTORY';
19
+
20
+ export class ConfigurationService {
21
+ private static appEnvVarPrefix = '';
22
+ private static cliArgs: Record<string, ConfigurationValuePrimitive | undefined> = {};
23
+ private static buffer = new BufferingLogger();
24
+ private static loggerImpl: ConfigurationLogger = ConfigurationService.buffer;
25
+
26
+ private static readonly registry = new Map<string, ConfigurationParameterDefinition>();
27
+ private static readonly registryByTarget = new Map<string, ConfigurationParameterDefinition[]>();
28
+
29
+ private static readonly resolvedDirectories = new Map<string, string>();
30
+ private static readonly resolvedYaml = new Map<string, unknown>();
31
+
32
+ private static initialized = false;
33
+
34
+ private constructor() {
35
+ /* static-only */
36
+ }
37
+
38
+ static setAppEnvVarPrefix(prefix: string): void {
39
+ ConfigurationService.appEnvVarPrefix = prefix.toUpperCase();
40
+ }
41
+
42
+ static getAppEnvVarPrefix(): string {
43
+ return ConfigurationService.appEnvVarPrefix;
44
+ }
45
+
46
+ static setCliArgs(args: Record<string, ConfigurationValuePrimitive | undefined>): void {
47
+ ConfigurationService.cliArgs = { ...args };
48
+ }
49
+
50
+ static setLogger(logger: ConfigurationLogger): void {
51
+ if (ConfigurationService.loggerImpl === ConfigurationService.buffer) {
52
+ flushBuffer(ConfigurationService.buffer, logger);
53
+ }
54
+ ConfigurationService.loggerImpl = logger;
55
+ }
56
+
57
+ static useConsoleLogger(): void {
58
+ ConfigurationService.setLogger(new ConsoleLogger());
59
+ }
60
+
61
+ static register(
62
+ parameters: ConfigurationParameterDefinition[],
63
+ target: string,
64
+ ): void {
65
+ for (const parameter of parameters) {
66
+ const existing = ConfigurationService.registry.get(parameter.configurationKey);
67
+ if (existing) {
68
+ throw new Error(
69
+ `Configuration key '${parameter.configurationKey}' is already registered.`,
70
+ );
71
+ }
72
+ ConfigurationService.registry.set(parameter.configurationKey, parameter);
73
+ const list = ConfigurationService.registryByTarget.get(target) ?? [];
74
+ list.push(parameter);
75
+ ConfigurationService.registryByTarget.set(target, list);
76
+
77
+ ConfigurationService.loggerImpl.debug(
78
+ `[configuration] registered ${parameter.kind} key '${parameter.configurationKey}' for target '${target}' (default=${String(
79
+ parameter.default,
80
+ )})`,
81
+ );
82
+ }
83
+ }
84
+
85
+ static initialize(): void {
86
+ const errors: string[] = [];
87
+ for (const parameter of ConfigurationService.registry.values()) {
88
+ if (parameter.required && parameter.default === undefined) {
89
+ try {
90
+ ConfigurationService.get(parameter.configurationKey);
91
+ } catch (error) {
92
+ errors.push(error instanceof Error ? error.message : String(error));
93
+ }
94
+ }
95
+ }
96
+ if (errors.length > 0) {
97
+ throw new Error(
98
+ `Required configuration not satisfied:\n - ${errors.join('\n - ')}`,
99
+ );
100
+ }
101
+ ConfigurationService.initialized = true;
102
+ }
103
+
104
+ static isInitialized(): boolean {
105
+ return ConfigurationService.initialized;
106
+ }
107
+
108
+ static get<T extends ConfigurationValuePrimitive>(configurationKey: string): T {
109
+ const parameter = ConfigurationService.registry.get(configurationKey);
110
+ if (!parameter) {
111
+ throw new Error(`Configuration key '${configurationKey}' is not registered.`);
112
+ }
113
+
114
+ const argumentKey = camelizeEnvVarName(configurationKey);
115
+ const envVarName = ConfigurationService.environmentVariableName(configurationKey);
116
+
117
+ let raw: ConfigurationValuePrimitive | undefined;
118
+ let source: 'cli' | 'env' | 'default' | undefined;
119
+
120
+ if (
121
+ Object.prototype.hasOwnProperty.call(ConfigurationService.cliArgs, argumentKey) &&
122
+ ConfigurationService.cliArgs[argumentKey] !== undefined
123
+ ) {
124
+ raw = ConfigurationService.cliArgs[argumentKey];
125
+ source = 'cli';
126
+ } else if (process.env[envVarName] !== undefined && process.env[envVarName] !== '') {
127
+ raw = process.env[envVarName];
128
+ source = 'env';
129
+ } else if (parameter.default !== undefined) {
130
+ raw = parameter.default;
131
+ source = 'default';
132
+ }
133
+
134
+ if (raw === undefined) {
135
+ if (parameter.required) {
136
+ throw new Error(
137
+ `No value found for required configuration key '${configurationKey}' in CLI argument '${argumentKey}', environment variable '${envVarName}', or default.`,
138
+ );
139
+ }
140
+ return undefined as unknown as T;
141
+ }
142
+
143
+ ConfigurationService.loggerImpl.debug(
144
+ `[configuration] '${configurationKey}' resolved from ${source}: ${String(raw)}`,
145
+ );
146
+
147
+ return ConfigurationService.coerce(raw, parameter) as T;
148
+ }
149
+
150
+ static resolveDirectory(configurationKey: string): string {
151
+ const cached = ConfigurationService.resolvedDirectories.get(configurationKey);
152
+ if (cached !== undefined) return cached;
153
+
154
+ const parameter = ConfigurationService.registry.get(configurationKey);
155
+ if (!parameter) {
156
+ throw new Error(`Configuration key '${configurationKey}' is not registered.`);
157
+ }
158
+ if (parameter.kind !== 'directory') {
159
+ throw new Error(`Configuration key '${configurationKey}' is not a directory parameter.`);
160
+ }
161
+
162
+ const configured = ConfigurationService.get<string>(configurationKey);
163
+ if (configured === undefined || configured === '') {
164
+ if (parameter.required) {
165
+ throw new Error(`Required directory configuration '${configurationKey}' is empty.`);
166
+ }
167
+ return '';
168
+ }
169
+
170
+ let resolved = configured;
171
+ if (!path.isAbsolute(resolved) && configurationKey !== CONFIG_BASE_DIR_KEY) {
172
+ const base = ConfigurationService.resolveDirectory(CONFIG_BASE_DIR_KEY);
173
+ resolved = path.join(base, resolved);
174
+ } else {
175
+ resolved = path.resolve(resolved);
176
+ }
177
+
178
+ ConfigurationService.ensureDirectoryAccess(resolved, parameter);
179
+ ConfigurationService.resolvedDirectories.set(configurationKey, resolved);
180
+ return resolved;
181
+ }
182
+
183
+ static resolveYaml<TResult = unknown>(configurationKey: string): TResult {
184
+ const cached = ConfigurationService.resolvedYaml.get(configurationKey);
185
+ if (cached !== undefined) return cached as TResult;
186
+
187
+ const parameter = ConfigurationService.registry.get(configurationKey);
188
+ if (!parameter) {
189
+ throw new Error(`Configuration key '${configurationKey}' is not registered.`);
190
+ }
191
+ if (parameter.kind !== 'yaml') {
192
+ throw new Error(`Configuration key '${configurationKey}' is not a yaml parameter.`);
193
+ }
194
+
195
+ const configured = ConfigurationService.get<string>(configurationKey);
196
+ if (configured === undefined || configured === '') {
197
+ throw new Error(`YAML configuration '${configurationKey}' has no value.`);
198
+ }
199
+
200
+ let resolved = configured;
201
+ if (!path.isAbsolute(resolved)) {
202
+ const base = ConfigurationService.resolveDirectory(CONFIG_BASE_DIR_KEY);
203
+ resolved = path.join(base, resolved);
204
+ } else {
205
+ resolved = path.resolve(resolved);
206
+ }
207
+
208
+ if (!fs.existsSync(resolved)) {
209
+ throw new Error(`YAML file for '${configurationKey}' not found: ${resolved}`);
210
+ }
211
+ const stats = fs.statSync(resolved);
212
+ if (!stats.isFile()) {
213
+ throw new Error(`YAML path for '${configurationKey}' is not a file: ${resolved}`);
214
+ }
215
+
216
+ const raw = fs.readFileSync(resolved, 'utf8');
217
+ const parsed = parseYAML(raw) as unknown;
218
+
219
+ if (parameter.schema !== undefined) {
220
+ const result = parameter.schema.safeParse(parsed);
221
+ if (!result.success) {
222
+ throw new Error(
223
+ `YAML validation failed for '${configurationKey}' (${resolved}): ${JSON.stringify(
224
+ result.error.issues,
225
+ null,
226
+ 2,
227
+ )}`,
228
+ );
229
+ }
230
+ ConfigurationService.resolvedYaml.set(configurationKey, result.data);
231
+ return result.data as TResult;
232
+ }
233
+
234
+ ConfigurationService.loggerImpl.warn(
235
+ `[configuration] no schema provided for YAML key '${configurationKey}'; returning unvalidated parsed data.`,
236
+ );
237
+ ConfigurationService.resolvedYaml.set(configurationKey, parsed);
238
+ return parsed as TResult;
239
+ }
240
+
241
+ static getRegistry(): readonly ConfigurationParameterDefinition[] {
242
+ return Array.from(ConfigurationService.registry.values());
243
+ }
244
+
245
+ static getRegistryByTarget(): ReadonlyMap<string, readonly ConfigurationParameterDefinition[]> {
246
+ return new Map(
247
+ Array.from(ConfigurationService.registryByTarget.entries()).map(([target, params]) => [
248
+ target,
249
+ params.slice(),
250
+ ]),
251
+ );
252
+ }
253
+
254
+ static environmentVariableName(configurationKey: string): string {
255
+ return ConfigurationService.appEnvVarPrefix
256
+ ? `${ConfigurationService.appEnvVarPrefix}_${configurationKey}`
257
+ : configurationKey;
258
+ }
259
+
260
+ /**
261
+ * Test-only reset. Wipes all module-singleton state (registry, resolution
262
+ * caches, logger, app prefix). Not re-exported from `src/index.ts` — call sites
263
+ * must reach in via `ConfigurationService.__resetForTesting()` directly,
264
+ * which is intentional friction signaling "this is for vitest, not runtime."
265
+ *
266
+ * Production code must never call this; the underscore prefix and explicit
267
+ * `forTesting` suffix exist to make accidental use obvious in code review.
268
+ */
269
+ static __resetForTesting(): void {
270
+ ConfigurationService.appEnvVarPrefix = '';
271
+ ConfigurationService.cliArgs = {};
272
+ ConfigurationService.buffer = new BufferingLogger();
273
+ ConfigurationService.loggerImpl = ConfigurationService.buffer;
274
+ ConfigurationService.registry.clear();
275
+ ConfigurationService.registryByTarget.clear();
276
+ ConfigurationService.resolvedDirectories.clear();
277
+ ConfigurationService.resolvedYaml.clear();
278
+ ConfigurationService.initialized = false;
279
+ }
280
+
281
+ private static coerce(
282
+ value: ConfigurationValuePrimitive,
283
+ parameter: ConfigurationParameterDefinition,
284
+ ): ConfigurationValuePrimitive {
285
+ switch (parameter.constructor) {
286
+ case Number: {
287
+ if (typeof value === 'number') return value;
288
+ if (typeof value === 'boolean') return value ? 1 : 0;
289
+ const parsed = Number.parseFloat(value);
290
+ if (Number.isNaN(parsed)) {
291
+ throw new Error(
292
+ `Cannot coerce value '${value}' to number for '${parameter.configurationKey}'.`,
293
+ );
294
+ }
295
+ return parsed;
296
+ }
297
+ case Boolean: {
298
+ if (typeof value === 'boolean') return value;
299
+ if (typeof value === 'number') return value !== 0;
300
+ return value.toLowerCase() === 'true' || value === '1';
301
+ }
302
+ default:
303
+ // Default covers String and any future constructor. Stringify whatever value we got.
304
+ return String(value);
305
+ }
306
+ }
307
+
308
+ private static ensureDirectoryAccess(
309
+ resolved: string,
310
+ parameter: DirectoryConfigurationParameterDefinition,
311
+ ): void {
312
+ const exists = fs.existsSync(resolved);
313
+ if (!exists) {
314
+ if (parameter.createIfMissing) {
315
+ fs.mkdirSync(resolved, { recursive: true });
316
+ } else {
317
+ throw new Error(
318
+ `Directory '${resolved}' for configuration '${parameter.configurationKey}' does not exist and createIfMissing is false.`,
319
+ );
320
+ }
321
+ }
322
+ const stats = fs.statSync(resolved);
323
+ if (!stats.isDirectory()) {
324
+ throw new Error(
325
+ `Path '${resolved}' for configuration '${parameter.configurationKey}' is not a directory.`,
326
+ );
327
+ }
328
+ const mode = parameter.requireWritable
329
+ ? fs.constants.R_OK | fs.constants.W_OK
330
+ : fs.constants.R_OK;
331
+ fs.accessSync(resolved, mode);
332
+ }
333
+ }
@@ -0,0 +1,42 @@
1
+ import { ConfigurationService } from './configuration-service.js';
2
+ import type { ConfigurationParameterDefinition } from './parameter-types.js';
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
5
+ type AnyClass = Function;
6
+
7
+ /**
8
+ * Class decorator that registers a set of configuration parameters and groups
9
+ * them under a target name (used by `print-config` / `getRegistryByTarget()`
10
+ * for display, and by error messages).
11
+ *
12
+ * Pass the target name as the first argument:
13
+ *
14
+ * @Configuration('HttpListenerConfig', ...HTTP_LISTENER_PARAMETERS)
15
+ * class HttpListenerConfig { ... }
16
+ *
17
+ * Why explicit instead of `target.name`? The TypeScript `experimentalDecorators`
18
+ * emit produces `let Foo = class Foo { ... }` which, after a JS bundler hoists
19
+ * the `let` to the module scope, gets the inner class expression renamed to
20
+ * `Foo2` to avoid the outer/inner binding collision. A decorator reading
21
+ * `target.name` would then register against `Foo2`, surfacing nonsense
22
+ * suffixes in `print-config` output. Passing the name explicitly sidesteps
23
+ * the whole problem and survives any future minifier/bundler renames.
24
+ *
25
+ * If the first argument is omitted (legacy callers), the decorator falls back
26
+ * to `target.name` — which works fine when the class is bundled from source
27
+ * but is fragile when bundled from prebuilt `dist/` JS.
28
+ */
29
+ export function Configuration(
30
+ nameOrFirstParameter: string | ConfigurationParameterDefinition,
31
+ ...rest: ConfigurationParameterDefinition[]
32
+ ): (target: AnyClass) => void {
33
+ const explicitName =
34
+ typeof nameOrFirstParameter === 'string' ? nameOrFirstParameter : undefined;
35
+ const parameters: ConfigurationParameterDefinition[] =
36
+ explicitName === undefined
37
+ ? [nameOrFirstParameter as ConfigurationParameterDefinition, ...rest]
38
+ : rest;
39
+ return (target: AnyClass) => {
40
+ ConfigurationService.register(parameters, explicitName ?? target.name);
41
+ };
42
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ export { Configuration } from './decorators.js';
2
+ export { ConfigurationService } from './configuration-service.js';
3
+ export {
4
+ ConfigurationParameter,
5
+ type AnyConfigurationParameterBuilder,
6
+ type AnyConfigurationParameterDefinition,
7
+ } from './parameter-builder.js';
8
+ export {
9
+ type ConfigurationParameterDefinition,
10
+ type ConfigurationParameterDefinitionBase,
11
+ type ConfigurationValuePrimitive,
12
+ type ConfigurationValueConstructor,
13
+ type SimpleConfigurationParameterDefinition,
14
+ type DirectoryConfigurationParameterDefinition,
15
+ type YamlConfigurationParameterDefinition,
16
+ } from './parameter-types.js';
17
+ export {
18
+ type ConfigurationLogger,
19
+ type LogLevel,
20
+ type BufferedLogEntry,
21
+ ConsoleLogger,
22
+ BufferingLogger,
23
+ flushBuffer,
24
+ } from './logger.js';
package/src/logger.ts ADDED
@@ -0,0 +1,67 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+
3
+ export interface ConfigurationLogger {
4
+ debug(message: string, ...args: unknown[]): void;
5
+ info(message: string, ...args: unknown[]): void;
6
+ warn(message: string, ...args: unknown[]): void;
7
+ error(message: string, ...args: unknown[]): void;
8
+ }
9
+
10
+ export interface BufferedLogEntry {
11
+ level: LogLevel;
12
+ message: string;
13
+ args: unknown[];
14
+ timestamp: Date;
15
+ }
16
+
17
+ export class ConsoleLogger implements ConfigurationLogger {
18
+ debug(message: string, ...args: unknown[]): void {
19
+ console.debug(message, ...args);
20
+ }
21
+ info(message: string, ...args: unknown[]): void {
22
+ console.info(message, ...args);
23
+ }
24
+ warn(message: string, ...args: unknown[]): void {
25
+ console.warn(message, ...args);
26
+ }
27
+ error(message: string, ...args: unknown[]): void {
28
+ console.error(message, ...args);
29
+ }
30
+ }
31
+
32
+ export class BufferingLogger implements ConfigurationLogger {
33
+ private readonly entries: BufferedLogEntry[] = [];
34
+
35
+ debug(message: string, ...args: unknown[]): void {
36
+ this.append('debug', message, args);
37
+ }
38
+ info(message: string, ...args: unknown[]): void {
39
+ this.append('info', message, args);
40
+ }
41
+ warn(message: string, ...args: unknown[]): void {
42
+ this.append('warn', message, args);
43
+ }
44
+ error(message: string, ...args: unknown[]): void {
45
+ this.append('error', message, args);
46
+ }
47
+
48
+ private append(level: LogLevel, message: string, args: unknown[]): void {
49
+ this.entries.push({ level, message, args, timestamp: new Date() });
50
+ }
51
+
52
+ drain(): readonly BufferedLogEntry[] {
53
+ const drained = this.entries.slice();
54
+ this.entries.length = 0;
55
+ return drained;
56
+ }
57
+
58
+ size(): number {
59
+ return this.entries.length;
60
+ }
61
+ }
62
+
63
+ export function flushBuffer(buffer: BufferingLogger, target: ConfigurationLogger): void {
64
+ for (const entry of buffer.drain()) {
65
+ target[entry.level](entry.message, ...entry.args);
66
+ }
67
+ }