@holo-js/config 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,219 @@
1
+ import { NormalizedHoloQueueConfig, HoloQueueConfig } from '@holo-js/queue';
2
+ export { HoloQueueConfig, NormalizedHoloQueueConfig } from '@holo-js/queue';
3
+
4
+ interface HoloProjectPaths {
5
+ models: string;
6
+ migrations: string;
7
+ generatedSchema: string;
8
+ seeders: string;
9
+ observers: string;
10
+ factories: string;
11
+ commands: string;
12
+ jobs: string;
13
+ }
14
+ interface HoloProjectConnectionConfig {
15
+ driver?: SupportedDatabaseDriver;
16
+ url?: string;
17
+ host?: string;
18
+ port?: number | string;
19
+ username?: string;
20
+ password?: string;
21
+ database?: string;
22
+ filename?: string;
23
+ schema?: string;
24
+ ssl?: boolean | Record<string, unknown>;
25
+ logging?: boolean;
26
+ }
27
+ interface HoloProjectDatabaseConfig {
28
+ defaultConnection?: string;
29
+ connections?: Record<string, HoloProjectConnectionConfig | string>;
30
+ }
31
+ interface HoloProjectConfig {
32
+ paths?: Partial<HoloProjectPaths>;
33
+ database?: HoloProjectDatabaseConfig;
34
+ models?: readonly string[];
35
+ migrations?: readonly string[];
36
+ seeders?: readonly string[];
37
+ }
38
+ type SupportedDatabaseDriver = 'sqlite' | 'postgres' | 'mysql';
39
+ type HoloAppEnv = 'development' | 'production' | 'test';
40
+ interface HoloAppConfig extends HoloProjectConfig {
41
+ name?: string;
42
+ key?: string;
43
+ url?: string;
44
+ debug?: boolean;
45
+ env?: HoloAppEnv;
46
+ }
47
+ type HoloDatabaseConnectionConfig = HoloProjectConnectionConfig;
48
+ type HoloDatabaseConfig = HoloProjectDatabaseConfig;
49
+ type StorageDriver = 'local' | 'public' | 's3';
50
+ type StorageVisibility = 'private' | 'public';
51
+ interface BaseDiskConfig {
52
+ driver: StorageDriver;
53
+ visibility?: StorageVisibility;
54
+ root?: string;
55
+ url?: string;
56
+ [key: string]: unknown;
57
+ }
58
+ interface LocalDiskConfig extends BaseDiskConfig {
59
+ driver: 'local' | 'public';
60
+ }
61
+ interface S3DiskConfig extends BaseDiskConfig {
62
+ driver: 's3';
63
+ bucket?: string;
64
+ region?: string;
65
+ endpoint?: string;
66
+ accessKeyId?: string;
67
+ secretAccessKey?: string;
68
+ sessionToken?: string;
69
+ forcePathStyleEndpoint?: boolean;
70
+ }
71
+ type DiskConfig = LocalDiskConfig | S3DiskConfig;
72
+ interface HoloStorageConfig {
73
+ defaultDisk?: string;
74
+ routePrefix?: string;
75
+ disks?: Record<string, DiskConfig>;
76
+ }
77
+ interface HoloMediaConfig {
78
+ [key: string]: unknown;
79
+ }
80
+
81
+ interface NormalizedHoloAppConfig {
82
+ readonly name: string;
83
+ readonly key: string;
84
+ readonly url: string;
85
+ readonly debug: boolean;
86
+ readonly env: HoloAppEnv;
87
+ readonly paths: Readonly<HoloProjectPaths>;
88
+ readonly models: readonly string[];
89
+ readonly migrations: readonly string[];
90
+ readonly seeders: readonly string[];
91
+ }
92
+ interface NormalizedHoloDatabaseConfig {
93
+ readonly defaultConnection?: string;
94
+ readonly connections: Readonly<Record<string, HoloDatabaseConnectionConfig | string>>;
95
+ }
96
+ interface NormalizedHoloStorageConfig {
97
+ readonly defaultDisk?: string;
98
+ readonly routePrefix: string;
99
+ readonly disks: Readonly<Record<string, DiskConfig>>;
100
+ }
101
+ interface HoloConfigRegistry {
102
+ app: NormalizedHoloAppConfig;
103
+ database: NormalizedHoloDatabaseConfig;
104
+ storage: NormalizedHoloStorageConfig;
105
+ queue: NormalizedHoloQueueConfig;
106
+ media: HoloMediaConfig;
107
+ }
108
+ type HoloConfigMap = object;
109
+ interface LoadedEnvironment {
110
+ readonly name: HoloAppEnv;
111
+ readonly values: Readonly<Record<string, string>>;
112
+ readonly loadedFiles: readonly string[];
113
+ readonly warnings: readonly string[];
114
+ }
115
+ interface LoadedHoloConfig<TCustom extends HoloConfigMap = HoloConfigMap> {
116
+ readonly app: NormalizedHoloAppConfig;
117
+ readonly database: NormalizedHoloDatabaseConfig;
118
+ readonly storage: NormalizedHoloStorageConfig;
119
+ readonly queue: NormalizedHoloQueueConfig;
120
+ readonly media: HoloMediaConfig;
121
+ readonly custom: Readonly<TCustom>;
122
+ readonly all: Readonly<HoloConfigRegistry & TCustom>;
123
+ readonly environment: LoadedEnvironment;
124
+ readonly loadedFiles: readonly string[];
125
+ readonly warnings: readonly string[];
126
+ }
127
+ type ConfigFileName = keyof HoloConfigRegistry | (string & {});
128
+ type Primitive = string | number | boolean | bigint | symbol | null | undefined;
129
+ type NonTraversable = Primitive | readonly unknown[] | ((...args: never[]) => unknown);
130
+ type KnownPathKey<T> = Extract<{
131
+ [K in keyof T]: K extends string ? string extends K ? never : K : never;
132
+ }[keyof T], string>;
133
+ type DotPath<T> = T extends NonTraversable ? never : {
134
+ [K in KnownPathKey<T>]: T[K] extends NonTraversable ? K : T[K] extends object ? K | `${K}.${DotPath<T[K]>}` : K;
135
+ }[KnownPathKey<T>];
136
+ type ValueAtPath<T, TPath extends string> = TPath extends `${infer Head}.${infer Tail}` ? Head extends keyof T ? ValueAtPath<T[Head], Tail> : never : TPath extends keyof T ? T[TPath] : never;
137
+ type DefineConfigValue<TConfig extends object> = Readonly<TConfig>;
138
+
139
+ declare const DEFAULT_APP_NAME = "Holo";
140
+ declare const holoAppDefaults: Readonly<NormalizedHoloAppConfig>;
141
+ declare const holoDatabaseDefaults: Readonly<NormalizedHoloDatabaseConfig>;
142
+ declare const holoStorageDefaults: Readonly<NormalizedHoloStorageConfig>;
143
+ declare const holoQueueDefaultsNormalized: Readonly<NormalizedHoloQueueConfig>;
144
+ declare function normalizeAppEnv(value: string | undefined, fallback?: HoloAppEnv): HoloAppEnv;
145
+ declare function normalizeAppConfig(config?: HoloAppConfig): NormalizedHoloAppConfig;
146
+ declare function normalizeDatabaseConfig(config?: HoloDatabaseConfig): NormalizedHoloDatabaseConfig;
147
+ declare function normalizeStorageConfig(config?: HoloStorageConfig): NormalizedHoloStorageConfig;
148
+ declare function normalizeQueueConfigForHolo(config?: HoloQueueConfig): NormalizedHoloQueueConfig;
149
+
150
+ type RuntimeConfigMap = HoloConfigRegistry & HoloConfigMap;
151
+ type UseConfigAccessor<TConfig extends RuntimeConfigMap> = {
152
+ <TKey extends Extract<keyof TConfig, string>>(key: TKey): TConfig[TKey];
153
+ <TPath extends DotPath<TConfig>>(path: TPath): ValueAtPath<TConfig, TPath>;
154
+ (path: string): unknown;
155
+ };
156
+ type ConfigAccessor<TConfig extends RuntimeConfigMap> = {
157
+ <TPath extends DotPath<TConfig>>(path: TPath): ValueAtPath<TConfig, TPath>;
158
+ (path: string): unknown;
159
+ };
160
+ declare function createConfigAccessors<TConfig extends RuntimeConfigMap>(configMap: TConfig): {
161
+ useConfig: UseConfigAccessor<TConfig>;
162
+ config: ConfigAccessor<TConfig>;
163
+ };
164
+ declare function configureConfigRuntime<TConfig extends RuntimeConfigMap>(configMap: TConfig): void;
165
+ declare function resetConfigRuntime(): void;
166
+ declare function useConfig<TKey extends Extract<keyof RuntimeConfigMap, string>>(key: TKey): RuntimeConfigMap[TKey];
167
+ declare function useConfig<TPath extends DotPath<RuntimeConfigMap>>(path: TPath): ValueAtPath<RuntimeConfigMap, TPath>;
168
+ declare function useConfig(path: string): unknown;
169
+ declare function config<TPath extends DotPath<RuntimeConfigMap>>(path: TPath): ValueAtPath<RuntimeConfigMap, TPath>;
170
+ declare function config(path: string): unknown;
171
+
172
+ type EnvScalar = string | number | boolean;
173
+ type LoadEnvironmentOptions = {
174
+ cwd: string;
175
+ envName?: string;
176
+ processEnv?: NodeJS.ProcessEnv;
177
+ };
178
+ type EnvRuntimeMode = 'resolve' | 'capture';
179
+ type EnvPlaceholder = {
180
+ readonly __holoEnv: true;
181
+ readonly key: string;
182
+ readonly fallback?: EnvScalar;
183
+ };
184
+ declare function resolveAppEnvironment(processEnv?: NodeJS.ProcessEnv): HoloAppEnv;
185
+ declare function resolveEnvironmentFileOrder(envName: HoloAppEnv): string[];
186
+ declare function loadEnvironment(options: LoadEnvironmentOptions): Promise<LoadedEnvironment>;
187
+ declare function configureEnvRuntime(values?: Readonly<Record<string, string>>, options?: {
188
+ mode?: EnvRuntimeMode;
189
+ }): void;
190
+ declare function env(key: string): string;
191
+ declare function env<TValue extends EnvScalar>(key: string, fallback: TValue): TValue;
192
+ declare function isEnvPlaceholder(value: unknown): value is EnvPlaceholder;
193
+ declare function resolveEnvPlaceholders<TValue>(value: TValue, envValues: Readonly<Record<string, string>>): TValue;
194
+
195
+ declare function resolveConfigExport<TConfig extends object>(moduleValue: unknown): TConfig;
196
+ declare function getConfigExtensionPriority(fileName: string): number;
197
+ declare function resolveConfigCachePath(projectRoot: string): string;
198
+ declare function writeConfigCache(projectRoot: string, options?: {
199
+ envName?: string;
200
+ processEnv?: NodeJS.ProcessEnv;
201
+ }): Promise<string>;
202
+ declare function clearConfigCache(projectRoot: string): Promise<boolean>;
203
+ declare function loadConfigDirectory<TCustom extends HoloConfigMap = HoloConfigMap>(projectRoot: string, options?: {
204
+ envName?: string;
205
+ processEnv?: NodeJS.ProcessEnv;
206
+ preferCache?: boolean;
207
+ }): Promise<LoadedHoloConfig<TCustom>>;
208
+ declare function defineConfig<TConfig extends object>(config: TConfig): DefineConfigValue<TConfig>;
209
+ declare function defineAppConfig<TConfig extends HoloAppConfig>(config: TConfig): DefineConfigValue<TConfig>;
210
+ declare function defineDatabaseConfig<TConfig extends HoloDatabaseConfig>(config: TConfig): DefineConfigValue<TConfig>;
211
+ declare function defineStorageConfig<TConfig extends HoloStorageConfig>(config: TConfig): DefineConfigValue<TConfig>;
212
+ declare function defineQueueConfig<TConfig extends HoloQueueConfig>(config: TConfig): DefineConfigValue<TConfig>;
213
+ declare function defineMediaConfig<TConfig extends HoloMediaConfig>(config: TConfig): DefineConfigValue<TConfig>;
214
+ declare const loaderInternals: {
215
+ getConfigExtensionPriority: typeof getConfigExtensionPriority;
216
+ resolveConfigExport: typeof resolveConfigExport;
217
+ };
218
+
219
+ export { type ConfigFileName, DEFAULT_APP_NAME, type DefineConfigValue, type DiskConfig, type DotPath, type HoloAppConfig, type HoloAppEnv, type HoloConfigMap, type HoloConfigRegistry, type HoloDatabaseConfig, type HoloDatabaseConnectionConfig, type HoloMediaConfig, type HoloStorageConfig, type LoadedEnvironment, type LoadedHoloConfig, type NormalizedHoloAppConfig, type NormalizedHoloDatabaseConfig, type NormalizedHoloStorageConfig, type SupportedDatabaseDriver, type ValueAtPath, clearConfigCache, config, configureConfigRuntime, configureEnvRuntime, createConfigAccessors, defineAppConfig, defineConfig, defineDatabaseConfig, defineMediaConfig, defineQueueConfig, defineStorageConfig, env, holoAppDefaults, holoDatabaseDefaults, holoQueueDefaultsNormalized, holoStorageDefaults, isEnvPlaceholder, loadConfigDirectory, loadEnvironment, loaderInternals, normalizeAppConfig, normalizeAppEnv, normalizeDatabaseConfig, normalizeQueueConfigForHolo, normalizeStorageConfig, resetConfigRuntime, resolveAppEnvironment, resolveConfigCachePath, resolveEnvPlaceholders, resolveEnvironmentFileOrder, useConfig, writeConfigCache };
package/dist/index.mjs ADDED
@@ -0,0 +1,638 @@
1
+ // src/defaults.ts
2
+ import {
3
+ DEFAULT_HOLO_PROJECT_PATHS,
4
+ normalizeHoloProjectConfig
5
+ } from "@holo-js/db";
6
+ import {
7
+ normalizeQueueConfig,
8
+ holoQueueDefaults
9
+ } from "@holo-js/queue";
10
+ var DEFAULT_APP_NAME = "Holo";
11
+ var holoAppDefaults = Object.freeze({
12
+ name: DEFAULT_APP_NAME,
13
+ key: "",
14
+ url: "http://localhost:3000",
15
+ debug: true,
16
+ env: "development",
17
+ paths: Object.freeze({ ...DEFAULT_HOLO_PROJECT_PATHS }),
18
+ models: Object.freeze([]),
19
+ migrations: Object.freeze([]),
20
+ seeders: Object.freeze([])
21
+ });
22
+ var holoDatabaseDefaults = Object.freeze({
23
+ defaultConnection: "default",
24
+ connections: Object.freeze({
25
+ default: Object.freeze({
26
+ driver: "sqlite",
27
+ url: "./data/database.sqlite",
28
+ schema: "public",
29
+ logging: false
30
+ })
31
+ })
32
+ });
33
+ var holoStorageDefaults = Object.freeze({
34
+ defaultDisk: "local",
35
+ routePrefix: "/storage",
36
+ disks: Object.freeze({
37
+ local: Object.freeze({
38
+ driver: "local",
39
+ root: "./storage/app"
40
+ }),
41
+ public: Object.freeze({
42
+ driver: "public",
43
+ root: "./storage/app/public",
44
+ visibility: "public"
45
+ })
46
+ })
47
+ });
48
+ var holoQueueDefaultsNormalized = holoQueueDefaults;
49
+ function normalizeAppEnv(value, fallback = "development") {
50
+ if (!value) {
51
+ return fallback;
52
+ }
53
+ if (value === "development" || value === "production" || value === "test") {
54
+ return value;
55
+ }
56
+ return fallback;
57
+ }
58
+ function normalizeAppConfig(config2 = {}) {
59
+ const project = normalizeHoloProjectConfig(config2);
60
+ const rawDebug = config2.debug;
61
+ const debug = typeof rawDebug === "string" ? !["false", "0", "off", "no"].includes(rawDebug.trim().toLowerCase()) : config2.debug;
62
+ return Object.freeze({
63
+ name: config2.name ?? holoAppDefaults.name,
64
+ key: config2.key ?? holoAppDefaults.key,
65
+ url: config2.url ?? holoAppDefaults.url,
66
+ debug: debug ?? holoAppDefaults.debug,
67
+ env: normalizeAppEnv(config2.env, holoAppDefaults.env),
68
+ paths: project.paths,
69
+ models: project.models,
70
+ migrations: project.migrations,
71
+ seeders: project.seeders
72
+ });
73
+ }
74
+ function normalizeDatabaseConfig(config2 = {}) {
75
+ const configuredConnections = config2.connections;
76
+ const connections = configuredConnections && Object.keys(configuredConnections).length > 0 ? Object.freeze({ ...configuredConnections }) : holoDatabaseDefaults.connections;
77
+ const defaultConnection = config2.defaultConnection ?? Object.keys(connections)[0];
78
+ return Object.freeze({
79
+ /* v8 ignore next */
80
+ defaultConnection: defaultConnection ?? "default",
81
+ connections
82
+ });
83
+ }
84
+ function normalizeStorageConfig(config2 = {}) {
85
+ return Object.freeze({
86
+ defaultDisk: config2.defaultDisk ?? holoStorageDefaults.defaultDisk,
87
+ routePrefix: config2.routePrefix ?? holoStorageDefaults.routePrefix,
88
+ disks: Object.freeze({
89
+ ...holoStorageDefaults.disks,
90
+ ...config2.disks ?? {}
91
+ })
92
+ });
93
+ }
94
+ function normalizeQueueConfigForHolo(config2 = {}) {
95
+ return normalizeQueueConfig(config2);
96
+ }
97
+
98
+ // src/access.ts
99
+ function isObject(value) {
100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
101
+ }
102
+ function getValueAtPath(source, path) {
103
+ let current = source;
104
+ for (const segment of path.split(".")) {
105
+ if (!isObject(current) || !(segment in current)) {
106
+ return void 0;
107
+ }
108
+ current = current[segment];
109
+ }
110
+ return current;
111
+ }
112
+ function createConfigAccessors(configMap) {
113
+ const useConfig2 = ((path) => {
114
+ return getValueAtPath(configMap, path);
115
+ });
116
+ const config2 = ((path) => {
117
+ return getValueAtPath(configMap, path);
118
+ });
119
+ return {
120
+ useConfig: useConfig2,
121
+ config: config2
122
+ };
123
+ }
124
+ function getRuntimeConfigState() {
125
+ const runtime = globalThis;
126
+ runtime.__holoConfigRuntime__ ??= {};
127
+ return runtime.__holoConfigRuntime__;
128
+ }
129
+ function configureConfigRuntime(configMap) {
130
+ getRuntimeConfigState().config = configMap;
131
+ }
132
+ function resetConfigRuntime() {
133
+ getRuntimeConfigState().config = void 0;
134
+ }
135
+ function requireConfigRuntime() {
136
+ const runtimeConfigMap = getRuntimeConfigState().config;
137
+ if (!runtimeConfigMap) {
138
+ throw new Error("Holo config runtime is not configured.");
139
+ }
140
+ return runtimeConfigMap;
141
+ }
142
+ function useConfig(path) {
143
+ return createConfigAccessors(requireConfigRuntime()).useConfig(path);
144
+ }
145
+ function config(path) {
146
+ return createConfigAccessors(requireConfigRuntime()).config(path);
147
+ }
148
+
149
+ // src/env.ts
150
+ import { basename, join, resolve } from "path";
151
+ import { readFile } from "fs/promises";
152
+ function parseEnvFile(contents) {
153
+ const values = {};
154
+ for (const line of contents.split(/\r?\n/)) {
155
+ const trimmed = line.trim();
156
+ if (!trimmed || trimmed.startsWith("#")) {
157
+ continue;
158
+ }
159
+ const exportPrefix = trimmed.startsWith("export ") ? trimmed.slice(7) : trimmed;
160
+ const separator = exportPrefix.indexOf("=");
161
+ if (separator <= 0) {
162
+ continue;
163
+ }
164
+ const key = exportPrefix.slice(0, separator).trim();
165
+ let value = exportPrefix.slice(separator + 1).trim();
166
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
167
+ value = value.slice(1, -1);
168
+ }
169
+ value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ");
170
+ values[key] = value;
171
+ }
172
+ return values;
173
+ }
174
+ async function readEnvFile(filePath) {
175
+ try {
176
+ const contents = await readFile(filePath, "utf8");
177
+ return parseEnvFile(contents);
178
+ } catch {
179
+ return void 0;
180
+ }
181
+ }
182
+ function resolveAppEnvironment(processEnv = process.env) {
183
+ const value = processEnv.HOLO_ENV ?? processEnv.APP_ENV ?? processEnv.NODE_ENV;
184
+ if (value === "production" || value === "development" || value === "test") {
185
+ return value;
186
+ }
187
+ return "development";
188
+ }
189
+ function resolveEnvironmentFileOrder(envName) {
190
+ if (envName === "production") {
191
+ return [".env", ".env.production", ".env.prod", ".env.local"];
192
+ }
193
+ if (envName === "test") {
194
+ return [".env", ".env.test"];
195
+ }
196
+ return [".env", ".env.development", ".env.local"];
197
+ }
198
+ async function loadEnvironment(options) {
199
+ const cwd = resolve(options.cwd);
200
+ const envName = options.envName ? resolveAppEnvironment({ ...options.processEnv, HOLO_ENV: options.envName }) : resolveAppEnvironment(options.processEnv);
201
+ const loadedFiles = [];
202
+ const warnings = [];
203
+ const values = {};
204
+ const productionFileNames = /* @__PURE__ */ new Set();
205
+ for (const relativeName of resolveEnvironmentFileOrder(envName)) {
206
+ const filePath = join(cwd, relativeName);
207
+ const fileValues = await readEnvFile(filePath);
208
+ if (!fileValues) {
209
+ continue;
210
+ }
211
+ loadedFiles.push(filePath);
212
+ if (envName === "production" && (relativeName === ".env.production" || relativeName === ".env.prod")) {
213
+ productionFileNames.add(relativeName);
214
+ }
215
+ if (envName === "production" && relativeName === ".env.prod" && productionFileNames.has(".env.production")) {
216
+ warnings.push(`Ignoring ${basename(filePath)} because .env.production is present.`);
217
+ continue;
218
+ }
219
+ Object.assign(values, fileValues);
220
+ }
221
+ const processEnv = options.processEnv ?? process.env;
222
+ for (const [key, value] of Object.entries(processEnv)) {
223
+ if (typeof value === "string") {
224
+ values[key] = value;
225
+ }
226
+ }
227
+ return {
228
+ name: envName,
229
+ values: Object.freeze({ ...values }),
230
+ loadedFiles: Object.freeze([...loadedFiles]),
231
+ warnings: Object.freeze([...warnings])
232
+ };
233
+ }
234
+ function getEnvRuntimeState() {
235
+ const runtime = globalThis;
236
+ runtime.__holoEnvRuntime__ ??= {
237
+ mode: "resolve"
238
+ };
239
+ return runtime.__holoEnvRuntime__;
240
+ }
241
+ function configureEnvRuntime(values, options = {}) {
242
+ const state = getEnvRuntimeState();
243
+ state.values = values;
244
+ state.mode = options.mode ?? "resolve";
245
+ }
246
+ function coerceEnvScalar(runtimeValue, fallback) {
247
+ if (typeof fallback === "boolean") {
248
+ const normalized = runtimeValue.trim().toLowerCase();
249
+ if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
250
+ return true;
251
+ }
252
+ if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") {
253
+ return false;
254
+ }
255
+ return fallback;
256
+ }
257
+ if (typeof fallback === "number") {
258
+ const parsed = Number(runtimeValue);
259
+ return Number.isFinite(parsed) ? parsed : fallback;
260
+ }
261
+ return runtimeValue;
262
+ }
263
+ function env(key, fallback) {
264
+ const state = getEnvRuntimeState();
265
+ if (process.env.HOLO_CAPTURE_ENV === "1" || state.mode === "capture") {
266
+ return Object.freeze({
267
+ __holoEnv: true,
268
+ key,
269
+ ...typeof fallback !== "undefined" ? { fallback } : {}
270
+ });
271
+ }
272
+ const runtimeValue = state.values?.[key];
273
+ if (typeof runtimeValue === "string") {
274
+ return typeof fallback === "undefined" ? runtimeValue : coerceEnvScalar(runtimeValue, fallback);
275
+ }
276
+ const processValue = process.env[key];
277
+ if (typeof processValue === "string") {
278
+ return typeof fallback === "undefined" ? processValue : coerceEnvScalar(processValue, fallback);
279
+ }
280
+ if (typeof fallback !== "undefined") {
281
+ return fallback;
282
+ }
283
+ return "";
284
+ }
285
+ function isEnvPlaceholder(value) {
286
+ return typeof value === "object" && value !== null && !Array.isArray(value) && value.__holoEnv === true && typeof value.key === "string";
287
+ }
288
+ function resolveEnvPlaceholders(value, envValues) {
289
+ if (isEnvPlaceholder(value)) {
290
+ const runtimeValue = envValues[value.key];
291
+ if (typeof runtimeValue === "string") {
292
+ if (typeof value.fallback === "undefined") {
293
+ return runtimeValue;
294
+ }
295
+ return coerceEnvScalar(runtimeValue, value.fallback);
296
+ }
297
+ if (typeof value.fallback !== "undefined") {
298
+ return value.fallback;
299
+ }
300
+ return "";
301
+ }
302
+ if (Array.isArray(value)) {
303
+ return value.map((entry) => resolveEnvPlaceholders(entry, envValues));
304
+ }
305
+ if (typeof value === "object" && value !== null) {
306
+ return Object.fromEntries(
307
+ Object.entries(value).map(([key, entry]) => [key, resolveEnvPlaceholders(entry, envValues)])
308
+ );
309
+ }
310
+ return value;
311
+ }
312
+
313
+ // src/loader.ts
314
+ import { mkdir, readFile as readFile2, readdir, rm, writeFile } from "fs/promises";
315
+ import { dirname, extname, join as join2, resolve as resolve2 } from "path";
316
+ import { pathToFileURL } from "url";
317
+ var CONFIG_EXTENSION_PRIORITY = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"];
318
+ var SUPPORTED_CONFIG_EXTENSIONS = new Set(CONFIG_EXTENSION_PRIORITY);
319
+ var HOLO_CONFIG_CACHE_VERSION = 1;
320
+ var HOLO_CONFIG_CACHE_PATH = join2(".holo-js", "generated", "config-cache.json");
321
+ var TRANSIENT_CONFIG_IMPORT_MARKER = ".__holo_import_";
322
+ var LEGACY_TRANSIENT_CONFIG_IMPORT_MARKERS = [TRANSIENT_CONFIG_IMPORT_MARKER, ".__native_test__"];
323
+ var USE_TRANSIENT_IMPORTS = process.env.VITEST === "true" || process.env.VITEST === "1";
324
+ function isObject2(value) {
325
+ return !!value && typeof value === "object" && !Array.isArray(value);
326
+ }
327
+ function resolveConfigExport(moduleValue) {
328
+ if (isObject2(moduleValue) && isObject2(moduleValue.default)) {
329
+ return moduleValue.default;
330
+ }
331
+ if (isObject2(moduleValue) && isObject2(moduleValue.config)) {
332
+ return moduleValue.config;
333
+ }
334
+ if (isObject2(moduleValue) && ("default" in moduleValue || "config" in moduleValue)) {
335
+ return {};
336
+ }
337
+ if (isObject2(moduleValue)) {
338
+ return moduleValue;
339
+ }
340
+ return {};
341
+ }
342
+ function getConfigName(fileName) {
343
+ return fileName.slice(0, fileName.length - extname(fileName).length);
344
+ }
345
+ var configImportNonce = 0;
346
+ async function importConfigModule(filePath) {
347
+ configImportNonce += 1;
348
+ if (USE_TRANSIENT_IMPORTS) {
349
+ const extension = extname(filePath);
350
+ const transientPath = `${filePath.slice(0, filePath.length - extension.length)}${TRANSIENT_CONFIG_IMPORT_MARKER}${configImportNonce}${extension}`;
351
+ const source = await readFile2(filePath, "utf8");
352
+ await writeFile(transientPath, source, "utf8");
353
+ try {
354
+ return await import(
355
+ /* webpackIgnore: true */
356
+ pathToFileURL(transientPath).href
357
+ );
358
+ } finally {
359
+ await rm(transientPath, { force: true });
360
+ }
361
+ }
362
+ return import(
363
+ /* webpackIgnore: true */
364
+ `${pathToFileURL(filePath).href}?t=${configImportNonce}`
365
+ );
366
+ }
367
+ async function writeFileIfChanged(filePath, contents) {
368
+ const existing = await readFile2(filePath, "utf8").catch(() => void 0);
369
+ if (existing === contents) {
370
+ return;
371
+ }
372
+ await mkdir(dirname(filePath), { recursive: true });
373
+ await writeFile(filePath, contents, "utf8");
374
+ }
375
+ function getConfigExtensionPriority(fileName) {
376
+ const extension = extname(fileName);
377
+ const index = CONFIG_EXTENSION_PRIORITY.indexOf(extension);
378
+ return index >= 0 ? index : Number.MAX_SAFE_INTEGER;
379
+ }
380
+ async function collectConfigEntries(configDir) {
381
+ const entries = await readdir(configDir, { withFileTypes: true }).catch(() => []);
382
+ const selectedByName = /* @__PURE__ */ new Map();
383
+ for (const entry of entries) {
384
+ if (!entry.isFile()) {
385
+ continue;
386
+ }
387
+ if (LEGACY_TRANSIENT_CONFIG_IMPORT_MARKERS.some((marker) => entry.name.includes(marker))) {
388
+ if (!USE_TRANSIENT_IMPORTS) {
389
+ await rm(join2(configDir, entry.name), { force: true });
390
+ }
391
+ continue;
392
+ }
393
+ const extension = extname(entry.name);
394
+ if (!SUPPORTED_CONFIG_EXTENSIONS.has(extension)) {
395
+ continue;
396
+ }
397
+ const configName = getConfigName(entry.name);
398
+ const filePath = join2(configDir, entry.name);
399
+ const priority = getConfigExtensionPriority(entry.name);
400
+ const current = selectedByName.get(configName);
401
+ if (!current || priority < current.priority) {
402
+ selectedByName.set(configName, { filePath, priority });
403
+ }
404
+ }
405
+ return [...selectedByName.entries()].sort((left, right) => left[0].localeCompare(right[0])).map(([configName, entry]) => ({
406
+ configName,
407
+ filePath: entry.filePath
408
+ }));
409
+ }
410
+ async function collectRawConfig(configDir, environmentValues, options = {}) {
411
+ const rawConfig = {};
412
+ const loadedFiles = [];
413
+ const configEntries = await collectConfigEntries(configDir);
414
+ const previousEnvEntries = /* @__PURE__ */ new Map();
415
+ try {
416
+ configureEnvRuntime(environmentValues, {
417
+ mode: options.captureEnvPlaceholders ? "capture" : "resolve"
418
+ });
419
+ for (const [key, value] of Object.entries(environmentValues)) {
420
+ previousEnvEntries.set(key, process.env[key]);
421
+ process.env[key] = value;
422
+ }
423
+ previousEnvEntries.set("HOLO_CAPTURE_ENV", process.env.HOLO_CAPTURE_ENV);
424
+ if (options.captureEnvPlaceholders) {
425
+ process.env.HOLO_CAPTURE_ENV = "1";
426
+ } else {
427
+ Reflect.deleteProperty(process.env, "HOLO_CAPTURE_ENV");
428
+ }
429
+ for (const entry of configEntries) {
430
+ rawConfig[entry.configName] = resolveConfigExport(await importConfigModule(entry.filePath));
431
+ loadedFiles.push(entry.filePath);
432
+ }
433
+ } finally {
434
+ configureEnvRuntime(void 0);
435
+ for (const [key, value] of previousEnvEntries) {
436
+ if (typeof value === "string") {
437
+ process.env[key] = value;
438
+ continue;
439
+ }
440
+ Reflect.deleteProperty(process.env, key);
441
+ }
442
+ }
443
+ return {
444
+ rawConfig,
445
+ loadedFiles: Object.freeze([...loadedFiles])
446
+ };
447
+ }
448
+ function normalizeLoadedConfig(rawConfig, options) {
449
+ const resolvedRawConfig = resolveEnvPlaceholders(rawConfig, options.environment.values);
450
+ const app = normalizeAppConfig(resolvedRawConfig.app);
451
+ const database = normalizeDatabaseConfig(resolvedRawConfig.database);
452
+ const storage = normalizeStorageConfig(resolvedRawConfig.storage);
453
+ const queue = normalizeQueueConfigForHolo(resolvedRawConfig.queue);
454
+ const media = Object.freeze({ ...resolvedRawConfig.media ?? {} });
455
+ const customEntries = Object.entries(resolvedRawConfig).filter(([key]) => {
456
+ return key !== "app" && key !== "database" && key !== "storage" && key !== "queue" && key !== "media";
457
+ });
458
+ const custom = Object.freeze(Object.fromEntries(customEntries));
459
+ const all = Object.freeze({
460
+ app,
461
+ database,
462
+ storage,
463
+ queue,
464
+ media,
465
+ ...custom
466
+ });
467
+ return {
468
+ app,
469
+ database,
470
+ storage,
471
+ queue,
472
+ media,
473
+ custom,
474
+ all,
475
+ environment: options.environment,
476
+ loadedFiles: Object.freeze([...options.loadedFiles]),
477
+ warnings: options.environment.warnings
478
+ };
479
+ }
480
+ function isSerializableConfigValue(value) {
481
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
482
+ return true;
483
+ }
484
+ if (Array.isArray(value)) {
485
+ return value.every((entry) => isSerializableConfigValue(entry));
486
+ }
487
+ if (isObject2(value)) {
488
+ return Object.values(value).every((entry) => isSerializableConfigValue(entry));
489
+ }
490
+ return false;
491
+ }
492
+ function assertSerializableConfig(rawConfig) {
493
+ if (!isSerializableConfigValue(rawConfig)) {
494
+ throw new TypeError("Holo config cache only supports plain JSON-serializable config values.");
495
+ }
496
+ }
497
+ function isCachePayload(value) {
498
+ return isObject2(value) && value.version === HOLO_CONFIG_CACHE_VERSION && isObject2(value.environment) && typeof value.environment.name === "string" && Array.isArray(value.environment.loadedFiles) && Array.isArray(value.environment.warnings) && Array.isArray(value.configFiles) && isObject2(value.config);
499
+ }
500
+ function resolveConfigCachePath(projectRoot) {
501
+ return join2(resolve2(projectRoot), HOLO_CONFIG_CACHE_PATH);
502
+ }
503
+ async function readConfigCache(projectRoot) {
504
+ const cachePath = resolveConfigCachePath(projectRoot);
505
+ const contents = await readFile2(cachePath, "utf8").catch(() => void 0);
506
+ if (!contents) {
507
+ return void 0;
508
+ }
509
+ try {
510
+ const parsed = JSON.parse(contents);
511
+ return isCachePayload(parsed) ? parsed : void 0;
512
+ } catch {
513
+ return void 0;
514
+ }
515
+ }
516
+ async function writeConfigCache(projectRoot, options = {}) {
517
+ const root = resolve2(projectRoot);
518
+ const configDir = join2(root, "config");
519
+ const environment = await loadEnvironment({
520
+ cwd: root,
521
+ envName: options.envName,
522
+ processEnv: options.processEnv
523
+ });
524
+ const { rawConfig, loadedFiles } = await collectRawConfig(configDir, environment.values, {
525
+ captureEnvPlaceholders: true
526
+ });
527
+ assertSerializableConfig(rawConfig);
528
+ const cachePath = resolveConfigCachePath(root);
529
+ const contents = `${JSON.stringify({
530
+ version: HOLO_CONFIG_CACHE_VERSION,
531
+ environment: {
532
+ name: environment.name,
533
+ loadedFiles: environment.loadedFiles,
534
+ warnings: environment.warnings
535
+ },
536
+ configFiles: loadedFiles,
537
+ config: rawConfig
538
+ }, null, 2)}
539
+ `;
540
+ await writeFileIfChanged(cachePath, contents);
541
+ return cachePath;
542
+ }
543
+ async function clearConfigCache(projectRoot) {
544
+ const cachePath = resolveConfigCachePath(projectRoot);
545
+ const exists = await readFile2(cachePath, "utf8").then(() => true).catch(() => false);
546
+ if (!exists) {
547
+ return false;
548
+ }
549
+ await rm(cachePath, { force: true });
550
+ return true;
551
+ }
552
+ async function loadConfigDirectory(projectRoot, options = {}) {
553
+ const root = resolve2(projectRoot);
554
+ const configDir = join2(root, "config");
555
+ const envName = options.envName ? resolveAppEnvironment({ ...options.processEnv, HOLO_ENV: options.envName }) : resolveAppEnvironment(options.processEnv);
556
+ const preferCache = options.preferCache ?? envName === "production";
557
+ if (preferCache) {
558
+ const cached = await readConfigCache(root);
559
+ if (cached?.environment.name === envName) {
560
+ const environment2 = await loadEnvironment({
561
+ cwd: root,
562
+ envName,
563
+ processEnv: options.processEnv
564
+ });
565
+ return normalizeLoadedConfig(cached.config, {
566
+ environment: environment2,
567
+ loadedFiles: cached.configFiles
568
+ });
569
+ }
570
+ }
571
+ const environment = await loadEnvironment({
572
+ cwd: root,
573
+ envName,
574
+ processEnv: options.processEnv
575
+ });
576
+ const { rawConfig, loadedFiles } = await collectRawConfig(configDir, environment.values);
577
+ return normalizeLoadedConfig(rawConfig, {
578
+ environment,
579
+ loadedFiles
580
+ });
581
+ }
582
+ function defineConfig(config2) {
583
+ return Object.freeze({ ...config2 });
584
+ }
585
+ function defineAppConfig(config2) {
586
+ return defineConfig(config2);
587
+ }
588
+ function defineDatabaseConfig(config2) {
589
+ return defineConfig(config2);
590
+ }
591
+ function defineStorageConfig(config2) {
592
+ return defineConfig(config2);
593
+ }
594
+ function defineQueueConfig(config2) {
595
+ return defineConfig(config2);
596
+ }
597
+ function defineMediaConfig(config2) {
598
+ return defineConfig(config2);
599
+ }
600
+ var loaderInternals = {
601
+ getConfigExtensionPriority,
602
+ resolveConfigExport
603
+ };
604
+ export {
605
+ DEFAULT_APP_NAME,
606
+ clearConfigCache,
607
+ config,
608
+ configureConfigRuntime,
609
+ configureEnvRuntime,
610
+ createConfigAccessors,
611
+ defineAppConfig,
612
+ defineConfig,
613
+ defineDatabaseConfig,
614
+ defineMediaConfig,
615
+ defineQueueConfig,
616
+ defineStorageConfig,
617
+ env,
618
+ holoAppDefaults,
619
+ holoDatabaseDefaults,
620
+ holoQueueDefaultsNormalized,
621
+ holoStorageDefaults,
622
+ isEnvPlaceholder,
623
+ loadConfigDirectory,
624
+ loadEnvironment,
625
+ loaderInternals,
626
+ normalizeAppConfig,
627
+ normalizeAppEnv,
628
+ normalizeDatabaseConfig,
629
+ normalizeQueueConfigForHolo,
630
+ normalizeStorageConfig,
631
+ resetConfigRuntime,
632
+ resolveAppEnvironment,
633
+ resolveConfigCachePath,
634
+ resolveEnvPlaceholders,
635
+ resolveEnvironmentFileOrder,
636
+ useConfig,
637
+ writeConfigCache
638
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@holo-js/config",
3
+ "version": "0.1.0",
4
+ "description": "Holo-JS Framework - typed config loading, env layering, and runtime access",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs"
11
+ }
12
+ },
13
+ "main": "./dist/index.mjs",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "stub": "tsup",
21
+ "typecheck": "tsc -p tsconfig.json --noEmit",
22
+ "test": "vitest --run"
23
+ },
24
+ "dependencies": {
25
+ "@holo-js/db": "workspace:*",
26
+ "@holo-js/queue": "workspace:*"
27
+ },
28
+ "devDependencies": {
29
+ "tsup": "catalog:",
30
+ "typescript": "catalog:",
31
+ "vitest": "catalog:"
32
+ }
33
+ }