@bhooai/nexus-core 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,35 @@
1
+ # @bhooai/nexus-core
2
+
3
+ The framework core: config loading with full precedence, a dependency-injection
4
+ container, typed error classes, and the inbuilt HTTP server.
5
+
6
+ ## Exports
7
+
8
+ - **config** — `loadConfig({ root })` resolves defaults < `nexus.config.ts` <
9
+ `nexus.runtime.json` < `NEXUS_*` env vars into a typed `NexusConfig`.
10
+ - **http** — `Router`, `NexusServer` (custom `node:http` server with a trie router,
11
+ params/wildcards, and a middleware pipeline), `bodyParser`, `RequestContext`.
12
+ - **di** — `Container` (the inbuilt DI used by the plugin host and services).
13
+ - **errors** — `HttpError`, `ValidationError`, `AuthenticationError`,
14
+ `ConflictError`, `NotFoundError`, etc.
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { loadConfig, Router, NexusServer, bodyParser } from '@bhooai/nexus-core';
20
+
21
+ const router = new Router();
22
+ router.get('/health', (ctx) => ctx.json({ status: 'ok' }));
23
+
24
+ const server = new NexusServer({
25
+ router,
26
+ middleware: [bodyParser()],
27
+ });
28
+ await server.listen(4000, '0.0.0.0');
29
+ ```
30
+
31
+ There is **no `server.use()`** — middleware is the `middleware` array passed to the
32
+ `NexusServer` constructor. Route middleware is the 3rd argument to `router.method`,
33
+ as an array, and runs before the handler.
34
+
35
+ See `apps/backend/src/main.ts` for the full bootstrap.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@bhooai/nexus-core",
3
+ "version": "0.1.0",
4
+ "publishConfig": { "access": "public" },
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts"
12
+ },
13
+ "./config": {
14
+ "types": "./src/config/index.ts",
15
+ "import": "./src/config/index.ts"
16
+ },
17
+ "./di": {
18
+ "types": "./src/di/index.ts",
19
+ "import": "./src/di/index.ts"
20
+ },
21
+ "./http": {
22
+ "types": "./src/http/index.ts",
23
+ "import": "./src/http/index.ts"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "test": "vitest run"
29
+ },
30
+ "dependencies": {
31
+ "zod": "^3.23.8"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.5.0",
35
+ "typescript": "^5.6.2",
36
+ "vitest": "^2.1.1"
37
+ }
38
+ }
@@ -0,0 +1,161 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { defaults } from './defaults.js';
5
+ import { deepMerge } from './merge.js';
6
+ import { configFromEnv, loadEnvFile } from './env.js';
7
+ import { nexusConfigSchema } from './schema.js';
8
+ import type { DeepPartial, NexusConfig, UserNexusConfig } from './types.js';
9
+
10
+ export interface LoadOptions {
11
+ /** The imported user config object (from `nexus.config.ts`). */
12
+ userConfig?: UserNexusConfig;
13
+ /** Path to a `nexus.config.ts`/`.js` to import dynamically if `userConfig` not given. */
14
+ userConfigPath?: string;
15
+ /** Path to a shipped default config (the framework's `nexus/nexus.config.ts`) merged right
16
+ * after the code defaults and before the user config. */
17
+ defaultConfigPath?: string;
18
+ /** Path to `nexus.runtime.json` (admin write-back). Defaults to `<root>/nexus.runtime.json`. */
19
+ runtimePath?: string;
20
+ /** Project root used to resolve relative paths. Defaults to cwd. */
21
+ root?: string;
22
+ /** Env override (for tests). */
23
+ env?: NodeJS.ProcessEnv;
24
+ /** CLI flag overrides (highest precedence). */
25
+ cli?: DeepPartial<NexusConfig>;
26
+ }
27
+
28
+ /**
29
+ * Load and merge configuration in precedence order:
30
+ * defaults < userConfig (nexus.config.ts) < runtime.json < env < cli
31
+ * Then validate with the zod schema and return a frozen, typed config.
32
+ */
33
+ export async function loadConfig(opts: LoadOptions = {}): Promise<NexusConfig> {
34
+ const root = resolve(opts.root ?? process.cwd());
35
+ const env = opts.env ?? process.env;
36
+
37
+ // 0. project .env — merged into the env object BEFORE env-var mapping below.
38
+ // Skipped when a caller passes its own env (tests), which must stay pure.
39
+ if (!opts.env) loadEnvFile(root, env);
40
+
41
+ // 1. defaults
42
+ let merged: unknown = defaults;
43
+
44
+ // 1b. shipped default config (the framework's nexus.config.ts) — base layer above code defaults
45
+ if (opts.defaultConfigPath && existsSync(resolve(opts.defaultConfigPath))) {
46
+ const defaultConfig = await importUserConfig(resolve(opts.defaultConfigPath));
47
+ merged = deepMerge(merged, defaultConfig);
48
+ }
49
+
50
+ // 2. user config (nexus.config.ts / nexus.config.js)
51
+ let userConfig = opts.userConfig;
52
+ if (!userConfig && opts.userConfigPath) {
53
+ userConfig = await importUserConfig(resolve(opts.userConfigPath));
54
+ }
55
+ if (userConfig) merged = deepMerge(merged, userConfig);
56
+
57
+ // 3. runtime.json (admin write-back, gitignored)
58
+ const runtimePath = opts.runtimePath ?? resolve(root, 'nexus.runtime.json');
59
+ if (existsSync(runtimePath)) {
60
+ try {
61
+ const runtime = JSON.parse(readFileSync(runtimePath, 'utf8')) as DeepPartial<NexusConfig>;
62
+ merged = deepMerge(merged, runtime);
63
+ } catch {
64
+ // A corrupt runtime file must not crash boot; ignore it.
65
+ }
66
+ }
67
+
68
+ // 4. env
69
+ merged = deepMerge(merged, configFromEnv(env));
70
+
71
+ // 5. cli flags (highest)
72
+ if (opts.cli) merged = deepMerge(merged, opts.cli);
73
+
74
+ // validate
75
+ const parsed = nexusConfigSchema.parse(merged);
76
+ return Object.freeze(parsed) as NexusConfig;
77
+ }
78
+
79
+ async function importUserConfig(absPath: string): Promise<UserNexusConfig> {
80
+ const url = pathToFileURL(absPath).href;
81
+ const mod = (await import(url)) as Record<string, unknown>;
82
+ const cfg = (mod.default ?? mod.config) as UserNexusConfig | undefined;
83
+ if (!cfg) {
84
+ throw new Error(
85
+ `Expected ${absPath} to export a config object as the default export or named "config".`,
86
+ );
87
+ }
88
+ return cfg;
89
+ }
90
+
91
+ /** Resolve a path relative to the project root. */
92
+ export function resolvePath(cfg: NexusConfig, rel: string): string {
93
+ return resolve(rel);
94
+ }
95
+
96
+ const CONFIG_EXTENSIONS = ['ts', 'js', 'mjs', 'cjs'] as const;
97
+
98
+ /**
99
+ * Path to the framework's own shipped `nexus.config.ts` (at the framework workspace root).
100
+ * Resolved relative to THIS file so it works from both `src/` (run via tsx) and `dist/`
101
+ * (compiled). When `@bhooai/nexus-core` is installed inside a user app's `node_modules/`,
102
+ * this resolves to a non-existent path under `node_modules/` — so it is simply skipped there
103
+ * (a user app does not inherit the framework's example config).
104
+ */
105
+ export function frameworkDefaultConfigPath(): string {
106
+ const here = dirname(fileURLToPath(import.meta.url));
107
+ // src/config/ -> src -> <pkg> -> packages -> <framework root> (4 levels up)
108
+ return resolve(here, '..', '..', '..', '..', 'nexus.config.ts');
109
+ }
110
+
111
+ /**
112
+ * First `nexus.config.{ts,js,mjs,cjs}` found walking UP from `root`, skipping `exclude`.
113
+ * Standard find-first discovery; stops at the filesystem root.
114
+ */
115
+ export function discoverUserConfigPath(root: string, exclude?: string): string | undefined {
116
+ let dir = resolve(root);
117
+ const excl = exclude ? resolve(exclude) : undefined;
118
+ for (let i = 0; i < 24; i++) {
119
+ for (const ext of CONFIG_EXTENSIONS) {
120
+ const cand = join(dir, `nexus.config.${ext}`);
121
+ if (existsSync(cand) && (!excl || resolve(cand) !== excl)) return cand;
122
+ }
123
+ const parent = dirname(dir);
124
+ if (parent === dir) break; // filesystem root
125
+ dir = parent;
126
+ }
127
+ return undefined;
128
+ }
129
+
130
+ /**
131
+ * Auto-discover and load config with the framework's two-level model:
132
+ * defaults < framework default (`nexus/nexus.config.ts`) < user config < runtime.json < env < cli
133
+ *
134
+ * The framework default is located via `frameworkDefaultConfigPath()` (the shipped
135
+ * `nexus/nexus.config.ts`). The user config is the nearest `nexus.config.{ts,js,...}` walking
136
+ * up from `root`, SKIPPING that default — so when running the framework's own app (cwd =
137
+ * `nexus/`), the discovery walks past `nexus/nexus.config.ts` and finds the user-editable
138
+ * `nexus.config.js` at the project root, which overrides the default. When running a user app,
139
+ * the framework default path does not exist, so only that app's own config is loaded.
140
+ */
141
+ export async function loadConfigAuto(
142
+ opts: Omit<LoadOptions, 'userConfig' | 'userConfigPath' | 'defaultConfigPath'> = {},
143
+ ): Promise<NexusConfig> {
144
+ const root = resolve(opts.root ?? process.cwd());
145
+ const defaultPath = frameworkDefaultConfigPath();
146
+ const defaultExists = existsSync(defaultPath);
147
+ const userPath = defaultExists
148
+ ? discoverUserConfigPath(root, defaultPath)
149
+ : discoverUserConfigPath(root);
150
+ return loadConfig({
151
+ ...opts,
152
+ root,
153
+ defaultConfigPath: defaultExists ? defaultPath : undefined,
154
+ userConfigPath: userPath,
155
+ });
156
+ }
157
+
158
+ /** Read-only accessor for tests / bootstrap that don't need file loading. */
159
+ export function mergeConfig(...sources: DeepPartial<NexusConfig>[]): NexusConfig {
160
+ return Object.freeze(nexusConfigSchema.parse(deepMerge(defaults, ...sources))) as NexusConfig;
161
+ }
@@ -0,0 +1,155 @@
1
+ import type { NexusConfig } from './types.js';
2
+
3
+ /**
4
+ * Code-level defaults — the lowest-precedence layer of the config stack.
5
+ * Every field is present so a config that supplies nothing still boots.
6
+ */
7
+ export const defaults: NexusConfig = {
8
+ env: 'development',
9
+ server: {
10
+ port: 4000,
11
+ host: '127.0.0.1',
12
+ https: false,
13
+ trustProxy: false,
14
+ bodyLimit: 12 * 1024 * 1024, // 12 MiB, allowing a 10 MiB upload plus multipart overhead
15
+ },
16
+ uploads: {
17
+ dir: 'uploads',
18
+ path: '/uploads',
19
+ maxFileSize: 10 * 1024 * 1024,
20
+ maxFiles: 20,
21
+ allowedTypes: [],
22
+ },
23
+ db: {
24
+ uri: 'mongodb://localhost:27017/nexus',
25
+ maxPoolSize: 10,
26
+ autoIndex: true,
27
+ },
28
+ redis: {
29
+ url: 'redis://localhost:6379',
30
+ keyPrefix: 'nexus:',
31
+ },
32
+ graphql: {
33
+ path: '/graphql',
34
+ federation: 'in-process',
35
+ subscriptions: true,
36
+ introspection: true,
37
+ },
38
+ ws: {
39
+ path: '/ws',
40
+ heartbeatMs: 30_000,
41
+ requireCsrf: true,
42
+ },
43
+ auth: {
44
+ jwt: {
45
+ secret: 'change-me-please',
46
+ accessTtl: 60 * 15, // 15 min
47
+ refreshTtl: 60 * 60 * 24 * 30, // 30 days
48
+ issuer: 'bhooai-nexus',
49
+ audience: 'bhooai-nexus-client',
50
+ },
51
+ cookieName: 'nexus_sid',
52
+ refreshCookieName: 'nexus_rid',
53
+ requireEmailVerification: false,
54
+ google: { clientId: '', clientSecret: '', callbackPath: '/auth/google/callback', scope: 'openid email profile' },
55
+ facebook: { clientId: '', clientSecret: '', callbackPath: '/auth/facebook/callback', scope: 'email' },
56
+ },
57
+ payments: {
58
+ webhookPath: '/payments/webhook/:provider',
59
+ currency: 'INR',
60
+ },
61
+ email: {
62
+ provider: 'log',
63
+ from: 'no-reply@nexus.local',
64
+ },
65
+ certs: {
66
+ dir: 'certs',
67
+ keyType: 'rsa',
68
+ rsaModulus: 2048,
69
+ ecCurve: 'prime256v1',
70
+ validityDays: 365,
71
+ },
72
+ ads: {
73
+ enabled: false,
74
+ developerToken: '',
75
+ clientId: '',
76
+ clientSecret: '',
77
+ refreshToken: '',
78
+ customerId: '',
79
+ },
80
+ webrtc: {
81
+ rtcMinPort: 40000,
82
+ rtcMaxPort: 40100,
83
+ announceIp: '127.0.0.1',
84
+ },
85
+ ai: {
86
+ serverUrl: 'http://localhost:8000',
87
+ timeoutMs: 60_000,
88
+ defaultProvider: 'auto',
89
+ schemaModel: 'gpt-4o-mini',
90
+ providers: [
91
+ { id: 'ollama', label: 'Ollama (local)', baseUrl: 'http://localhost:11434', enabled: true, defaultModel: 'llama3:latest' },
92
+ { id: 'openai', label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', enabled: false, defaultModel: 'gpt-4o-mini' },
93
+ { id: 'anthropic', label: 'Anthropic Claude', baseUrl: 'https://api.anthropic.com/v1', enabled: false, defaultModel: 'claude-sonnet-4-20250514' },
94
+ { id: 'google', label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1', enabled: false, defaultModel: 'gemini-2.0-flash' },
95
+ { id: 'groq', label: 'Groq', baseUrl: 'https://api.groq.com/openai/v1', enabled: false, defaultModel: 'llama-3.3-70b-versatile' },
96
+ { id: 'mistral', label: 'Mistral AI', baseUrl: 'https://api.mistral.ai/v1', enabled: false, defaultModel: 'mistral-large-latest' },
97
+ { id: 'cohere', label: 'Cohere', baseUrl: 'https://api.cohere.ai/v1', enabled: false, defaultModel: 'command-r-plus' },
98
+ { id: 'together', label: 'Together AI', baseUrl: 'https://api.together.xyz/v1', enabled: false, defaultModel: 'meta-llama/Llama-3-70b-chat-hf' },
99
+ { id: 'fireworks', label: 'Fireworks AI', baseUrl: 'https://api.fireworks.ai/inference/v1', enabled: false, defaultModel: 'accounts/fireworks/models/llama-v3-70b-instruct' },
100
+ { id: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', enabled: false, defaultModel: 'deepseek-chat' },
101
+ { id: 'perplexity', label: 'Perplexity', baseUrl: 'https://api.perplexity.ai', enabled: false, defaultModel: 'llama-3.1-sonar-large-128k-online' },
102
+ { id: 'xai', label: 'xAI (Grok)', baseUrl: 'https://api.x.ai/v1', enabled: false, defaultModel: 'grok-2-latest' },
103
+ { id: 'replicate', label: 'Replicate', baseUrl: 'https://api.replicate.com/v1', enabled: false, defaultModel: 'meta/llama-3-70b-instruct' },
104
+ { id: 'huggingface', label: 'Hugging Face', baseUrl: 'https://api-inference.huggingface.co/models', enabled: false, defaultModel: 'meta-llama/Llama-3-70b-chat-hf' },
105
+ { id: 'nvidia', label: 'NVIDIA NIM', baseUrl: 'https://integrate.api.nvidia.com/v1', enabled: false, defaultModel: 'meta/llama-3.1-70b-instruct' },
106
+ { id: 'openrouter', label: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1', enabled: false, defaultModel: 'openai/gpt-4o-mini' },
107
+ { id: 'lmstudio', label: 'LM Studio (local)', baseUrl: 'http://localhost:1234/v1', enabled: false, defaultModel: 'local-model' },
108
+ { id: 'alephalpha', label: 'Aleph Alpha', baseUrl: 'https://api.aleph-alpha.com/v1', enabled: false, defaultModel: 'luminous-supreme-control' },
109
+ { id: 'stability', label: 'Stability AI', baseUrl: 'https://api.stability.ai/v1', enabled: false, defaultModel: 'stable-diffusion-xl' },
110
+ { id: 'azure', label: 'Azure OpenAI', baseUrl: 'https://your-resource.openai.azure.com', enabled: false, defaultModel: 'gpt-4o-mini' },
111
+ ],
112
+ },
113
+ logging: {
114
+ level: 'info',
115
+ format: 'pretty',
116
+ console: true,
117
+ dir: 'logs',
118
+ maxFileSize: 10 * 1024 * 1024, // 10 MiB
119
+ maxFiles: 7,
120
+ },
121
+ plugins: {
122
+ dir: 'plugins',
123
+ entries: [],
124
+ },
125
+ frontend: {
126
+ port: 3000,
127
+ host: 'localhost',
128
+ enabled: true,
129
+ },
130
+ admin: {
131
+ port: 3001,
132
+ host: 'localhost',
133
+ enabled: true,
134
+ },
135
+ cluster: {
136
+ enabled: false,
137
+ failOpenSingleNode: true,
138
+ lbHost: '127.0.0.1',
139
+ lbPort: 8080,
140
+ nodeAgentHost: '127.0.0.1',
141
+ nodeAgentPort: 7575,
142
+ registryFile: 'cluster.runtime.json',
143
+ token: '',
144
+ autoscale: {
145
+ enabled: true,
146
+ mode: 'auto',
147
+ minNodes: 1,
148
+ maxNodes: 4,
149
+ cooldownMs: 60_000,
150
+ cpuHigh: 80,
151
+ rpsPerNodeHigh: 15,
152
+ rpsPerNodeLow: 5,
153
+ },
154
+ },
155
+ };
@@ -0,0 +1,113 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import type { DeepPartial, NexusConfig } from './types.js';
4
+
5
+ /**
6
+ * Load `<root>/.env` into `env` WITHOUT overriding variables that are already
7
+ * set (a real environment value always wins). Dependency-free: `KEY=VALUE`
8
+ * (optional `export ` prefix), `#` comments, blank lines, optional quotes.
9
+ *
10
+ * The CLI used to be the only `.env` loader, so a backend started directly
11
+ * (tsx, plain node) never saw `.env` secrets. Every config consumer now loads
12
+ * the project `.env` itself; double-loading is harmless (idempotent).
13
+ */
14
+ export function loadEnvFile(root: string, env: NodeJS.ProcessEnv): void {
15
+ const file = resolve(root, '.env');
16
+ if (!existsSync(file)) return;
17
+ let content: string;
18
+ try {
19
+ content = readFileSync(file, 'utf8');
20
+ } catch {
21
+ return;
22
+ }
23
+ for (const rawLine of content.split(/\r?\n/)) {
24
+ const line = rawLine.trim();
25
+ if (!line || line.startsWith('#')) continue;
26
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
27
+ if (!m) continue;
28
+ const key = m[1]!;
29
+ if (env[key] !== undefined) continue; // real env always wins
30
+ let value = m[2]!.trim();
31
+ if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
32
+ value = value.slice(1, -1);
33
+ }
34
+ env[key] = value;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Build a config-override object from `NEXUS_*` environment variables.
40
+ * Nested keys use `_` as a separator, e.g. `NEXUS_SERVER_PORT` -> { server: { port } }.
41
+ *
42
+ * Type coercion: integers for numeric fields, booleans for true/false,
43
+ * comma lists for arrays where applicable (heuristic by key).
44
+ */
45
+ function coerce(value: string): string | number | boolean {
46
+ const v = value.trim();
47
+ if (/^-?\d+$/.test(v)) return Number.parseInt(v, 10);
48
+ if (v.toLowerCase() === 'true') return true;
49
+ if (v.toLowerCase() === 'false') return false;
50
+ return v;
51
+ }
52
+
53
+ /**
54
+ * Two `_`-separated env segments that form ONE camelCase config field.
55
+ * Generic splitting would turn `KEY_ID` into `key.id` (a nested object) —
56
+ * these known compounds collapse into a single tree key:
57
+ * NEXUS_PAYMENTS_RAZORPAY_KEY_ID -> payments.razorpay.keyId
58
+ * NEXUS_AUTH_GOOGLE_CLIENT_SECRET -> auth.google.clientSecret
59
+ */
60
+ const PAIR_FIELDS: Record<string, string> = {
61
+ 'key_id': 'keyId',
62
+ 'key_secret': 'keySecret',
63
+ 'client_id': 'clientId',
64
+ 'client_secret': 'clientSecret',
65
+ 'merchant_key': 'merchantKey',
66
+ 'merchant_email': 'merchantEmail',
67
+ 'program_id': 'programId',
68
+ 'api_key': 'apiKey',
69
+ 'webhook_secret': 'webhookSecret',
70
+ 'customer_id': 'customerId',
71
+ 'developer_token': 'developerToken',
72
+ 'refresh_token': 'refreshToken',
73
+ 'access_ttl': 'accessTtl',
74
+ 'refresh_ttl': 'refreshTtl',
75
+ 'callback_path': 'callbackPath',
76
+ 'lb_port': 'lbPort',
77
+ 'lb_host': 'lbHost',
78
+ 'node_agent_port': 'nodeAgentPort',
79
+ 'node_agent_host': 'nodeAgentHost',
80
+ 'registry_file': 'registryFile',
81
+ 'min_nodes': 'minNodes',
82
+ 'max_nodes': 'maxNodes',
83
+ 'cooldown_ms': 'cooldownMs',
84
+ 'cpu_high': 'cpuHigh',
85
+ 'rps_per_node_high': 'rpsPerNodeHigh',
86
+ 'rps_per_node_low': 'rpsPerNodeLow',
87
+ };
88
+
89
+ export function configFromEnv(env: NodeJS.ProcessEnv = process.env): DeepPartial<NexusConfig> {
90
+ const out: Record<string, unknown> = {};
91
+ for (const [rawKey, rawValue] of Object.entries(env)) {
92
+ if (!rawKey.startsWith('NEXUS_') || rawValue === undefined) continue;
93
+ const path = rawKey.slice('NEXUS_'.length).toLowerCase().split('_');
94
+ const last = path.length - 1;
95
+ if (last >= 1) {
96
+ const compound = PAIR_FIELDS[`${path[last - 1]}_${path[last]}`];
97
+ if (compound) {
98
+ path.splice(last - 1, 2, compound);
99
+ }
100
+ }
101
+ let node: Record<string, unknown> = out;
102
+ for (let i = 0; i < path.length; i++) {
103
+ const segment = path[i]!;
104
+ if (i === path.length - 1) {
105
+ node[segment] = coerce(rawValue);
106
+ } else {
107
+ node[segment] = (node[segment] as Record<string, unknown>) ?? {};
108
+ node = node[segment] as Record<string, unknown>;
109
+ }
110
+ }
111
+ }
112
+ return out as DeepPartial<NexusConfig>;
113
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './defaults.js';
3
+ export * from './merge.js';
4
+ export * from './env.js';
5
+ export * from './schema.js';
6
+ export * from './ConfigLoader.js';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Deep merge of plain objects. Later sources win. Arrays are replaced, not
3
+ * concatenated (config arrays are declarative). Non-plain objects (e.g. Date,
4
+ * RegExp) are assigned by reference.
5
+ */
6
+ export function isPlainObject(v: unknown): v is Record<string, unknown> {
7
+ if (v === null || typeof v !== 'object') return false;
8
+ const proto = Object.getPrototypeOf(v) as unknown;
9
+ return proto === Object.prototype || proto === null;
10
+ }
11
+
12
+ export function deepMerge<T>(base: T, ...sources: unknown[]): T {
13
+ if (!isPlainObject(base)) return (sources.at(-1) as T) ?? base;
14
+ const out: Record<string, unknown> = { ...(base as Record<string, unknown>) };
15
+ for (const source of sources) {
16
+ if (!isPlainObject(source)) continue;
17
+ for (const [key, value] of Object.entries(source)) {
18
+ if (value === undefined) continue;
19
+ const current = out[key];
20
+ if (isPlainObject(current) && isPlainObject(value)) {
21
+ out[key] = deepMerge(current, value);
22
+ } else {
23
+ out[key] = value;
24
+ }
25
+ }
26
+ }
27
+ return out as T;
28
+ }