everything-dev 0.2.0 → 0.3.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/src/config.ts CHANGED
@@ -1,286 +1,573 @@
1
- import { dirname, join } from "path";
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { access, readFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { Effect } from "every-plugin/effect";
5
+ import { Graph } from "near-social-js";
2
6
  import type {
3
- AppConfig,
4
- BosConfig,
5
- GatewayConfig,
6
- HostConfig,
7
- PortConfig,
8
- RemoteConfig,
9
- RuntimeConfig,
10
- SourceMode,
7
+ AppConfig,
8
+ BosConfig,
9
+ BosConfigInput,
10
+ PortConfig,
11
+ RemoteConfig,
12
+ RuntimeConfig,
13
+ SourceMode,
14
+ } from "./types";
15
+ import {
16
+ BosConfigInputSchema,
17
+ BosConfigSchema,
18
+ ConfigCircularExtendsError,
19
+ ConfigFetchError,
20
+ ConfigResolutionError,
11
21
  } from "./types";
12
22
 
13
- export type { AppConfig, BosConfig, GatewayConfig, HostConfig, PortConfig, RemoteConfig, RuntimeConfig, SourceMode };
23
+ // Re-export types
24
+ export type {
25
+ AppConfig,
26
+ BosConfig,
27
+ PortConfig,
28
+ RemoteConfig,
29
+ RuntimeConfig,
30
+ SourceMode,
31
+ };
14
32
 
33
+ // Constants
15
34
  export const DEFAULT_DEV_CONFIG: AppConfig = {
16
- host: "local",
17
- ui: "local",
18
- api: "local",
35
+ host: "local",
36
+ ui: "local",
37
+ api: "local",
19
38
  };
20
39
 
40
+ // Global state (for caching)
41
+ const configCache = new Map<string, { config: BosConfig; timestamp: number }>();
21
42
  let cachedConfig: BosConfig | null = null;
22
- let configDir: string | null = null;
43
+ let projectRoot: string | null = null;
23
44
  let configLoaded = false;
24
45
 
25
- export function findConfigPath(startDir: string): string | null {
26
- let dir = startDir;
27
- while (dir !== "/") {
28
- const configPath = join(dir, "bos.config.json");
29
- if (Bun.file(configPath).size > 0) {
30
- try {
31
- Bun.file(configPath).text();
32
- return configPath;
33
- } catch {
34
- // File doesn't exist or can't be read
35
- }
36
- }
37
- dir = dirname(dir);
38
- }
39
- return null;
40
- }
41
-
42
- function findConfigPathSync(startDir: string): string | null {
43
- let dir = startDir;
44
- while (dir !== "/") {
45
- const configPath = join(dir, "bos.config.json");
46
- const file = Bun.file(configPath);
47
- if (file.size > 0) {
48
- return configPath;
49
- }
50
- dir = dirname(dir);
51
- }
52
- return null;
53
- }
54
-
55
- export function loadConfig(cwd?: string): BosConfig | null {
56
- if (configLoaded) return cachedConfig;
57
-
58
- const startDir = cwd ?? process.cwd();
59
- const configPath = findConfigPathSync(startDir);
60
-
61
- if (!configPath) {
62
- configLoaded = true;
63
- configDir = startDir;
64
- return null;
65
- }
66
-
67
- configDir = dirname(configPath);
68
- const content = require(configPath);
69
- cachedConfig = content as BosConfig;
70
- configLoaded = true;
71
- return cachedConfig;
46
+ // ============================================================================
47
+ // PUBLIC API (6 methods)
48
+ // ============================================================================
49
+
50
+ /**
51
+ * Clear the config cache. Used by --force flag.
52
+ */
53
+ export function clearConfigCache(): void {
54
+ configCache.clear();
55
+ cachedConfig = null;
56
+ projectRoot = null;
57
+ configLoaded = false;
72
58
  }
73
59
 
74
- export function setConfig(config: BosConfig, dir?: string): void {
75
- cachedConfig = config;
76
- configDir = dir ?? process.cwd();
77
- configLoaded = true;
60
+ /**
61
+ * Find the path to bos.config.json by walking up the directory tree.
62
+ */
63
+ export function findConfigPath(cwd?: string): string | null {
64
+ let dir = cwd ?? process.cwd();
65
+ while (dir !== "/") {
66
+ const configPath = join(dir, "bos.config.json");
67
+ if (existsSync(configPath) && statSync(configPath).size > 0) {
68
+ return configPath;
69
+ }
70
+ dir = dirname(dir);
71
+ }
72
+ return null;
78
73
  }
79
74
 
80
- export function getConfigDir(): string {
81
- if (!configLoaded) {
82
- loadConfig();
83
- }
84
- return configDir!;
75
+ /**
76
+ * Get the cached config. Returns null if not loaded yet.
77
+ */
78
+ export function getConfig(): BosConfig | null {
79
+ return cachedConfig;
85
80
  }
86
81
 
87
- export function getRemotes(): string[] {
88
- const config = loadConfig();
89
- if (!config) return [];
90
- return Object.keys(config.app).filter((k) => k !== "host");
82
+ /**
83
+ * Get the project root directory (where bos.config.json is located).
84
+ * Throws if config hasn't been loaded.
85
+ */
86
+ export function getProjectRoot(): string {
87
+ if (!configLoaded || !projectRoot) {
88
+ throw new Error("Config not loaded. Call loadConfig() first.");
89
+ }
90
+ return projectRoot;
91
91
  }
92
92
 
93
- export function getPackages(): string[] {
94
- const config = loadConfig();
95
- if (!config) return [];
96
- return Object.keys(config.app);
93
+ /**
94
+ * Result of loading config - includes everything needed by callers.
95
+ */
96
+ export interface ConfigResult {
97
+ /** The validated BosConfig */
98
+ config: BosConfig;
99
+ /** Environment-specific runtime config */
100
+ runtime: RuntimeConfig;
101
+ /** Information about where/how the config was loaded */
102
+ source: {
103
+ path: string;
104
+ extended?: string[];
105
+ remote?: boolean;
106
+ };
107
+ /** Package information */
108
+ packages: {
109
+ all: string[];
110
+ resolved: PackageResolution[];
111
+ };
97
112
  }
98
113
 
99
- export function getRemote(name: string): RemoteConfig | undefined {
100
- const config = loadConfig();
101
- if (!config) return undefined;
102
- const remote = config.app[name];
103
- if (remote && "name" in remote) {
104
- return remote as RemoteConfig;
105
- }
106
- return undefined;
114
+ /**
115
+ * Resolution info for a single package.
116
+ */
117
+ export interface PackageResolution {
118
+ name: string;
119
+ mode: SourceMode;
120
+ exists: boolean;
121
+ port: number;
122
+ url: string;
107
123
  }
108
124
 
109
- export function getHost(): HostConfig {
110
- const config = loadConfig();
111
- if (!config) {
112
- throw new Error("No bos.config.json found");
113
- }
114
- return config.app.host;
125
+ /**
126
+ * Primary config loader - handles everything.
127
+ * - Local config loading
128
+ * - Config inheritance (extends)
129
+ * - BOS URL fetching
130
+ * - Caching
131
+ * - Runtime config generation
132
+ */
133
+ export async function loadConfig(options?: {
134
+ cwd?: string;
135
+ path?: string;
136
+ force?: boolean;
137
+ env?: "development" | "production";
138
+ }): Promise<ConfigResult | null> {
139
+ // Return cached result if available and not forcing reload
140
+ if (configLoaded && cachedConfig && !options?.force) {
141
+ const runtime = buildRuntimeConfig(
142
+ cachedConfig,
143
+ options?.env ?? "development",
144
+ );
145
+ return {
146
+ config: cachedConfig,
147
+ runtime,
148
+ source: { path: join(projectRoot!, "bos.config.json") },
149
+ packages: {
150
+ all: Object.keys(cachedConfig.app),
151
+ resolved: [], // Will be populated by resolvePackages
152
+ },
153
+ };
154
+ }
155
+
156
+ // Find or use explicit config path
157
+ const configPath = options?.path ?? findConfigPath(options?.cwd);
158
+ if (!configPath) {
159
+ configLoaded = true;
160
+ projectRoot = options?.cwd ?? process.cwd();
161
+ return null;
162
+ }
163
+
164
+ const baseDir = dirname(configPath);
165
+
166
+ // Check cache for this specific file
167
+ const cached = configCache.get(configPath);
168
+ if (cached && !options?.force) {
169
+ projectRoot = baseDir;
170
+ cachedConfig = cached.config;
171
+ configLoaded = true;
172
+ const runtime = buildRuntimeConfig(
173
+ cached.config,
174
+ options?.env ?? "development",
175
+ );
176
+ return {
177
+ config: cached.config,
178
+ runtime,
179
+ source: { path: configPath },
180
+ packages: {
181
+ all: Object.keys(cached.config.app),
182
+ resolved: [],
183
+ },
184
+ };
185
+ }
186
+
187
+ try {
188
+ // Resolve config with extends
189
+ const extendedChain: string[] = [];
190
+ const rawConfig = await resolveConfigWithExtends(
191
+ configPath,
192
+ baseDir,
193
+ new Set(),
194
+ extendedChain,
195
+ );
196
+
197
+ // Validate with strict schema
198
+ const validated = BosConfigSchema.parse(rawConfig);
199
+
200
+ // Cache result
201
+ configCache.set(configPath, { config: validated, timestamp: Date.now() });
202
+ projectRoot = baseDir;
203
+ cachedConfig = validated;
204
+ configLoaded = true;
205
+
206
+ // Build runtime config
207
+ const runtime = buildRuntimeConfig(
208
+ validated,
209
+ options?.env ?? "development",
210
+ );
211
+
212
+ return {
213
+ config: validated,
214
+ runtime,
215
+ source: {
216
+ path: configPath,
217
+ extended: extendedChain.length > 0 ? extendedChain : undefined,
218
+ remote: extendedChain.some((e) => e.startsWith("bos://")),
219
+ },
220
+ packages: {
221
+ all: Object.keys(validated.app),
222
+ resolved: [],
223
+ },
224
+ };
225
+ } catch (error) {
226
+ if (error instanceof ConfigCircularExtendsError) {
227
+ throw error;
228
+ }
229
+ if (error instanceof ConfigFetchError) {
230
+ throw error;
231
+ }
232
+ if (error instanceof ConfigResolutionError) {
233
+ throw error;
234
+ }
235
+ throw new ConfigResolutionError(
236
+ `Failed to load config from ${configPath}: ${error}`,
237
+ );
238
+ }
115
239
  }
116
240
 
117
- export function getUrl(
118
- packageName: string,
119
- env: "development" | "production" = "development"
120
- ): string | undefined {
121
- const config = loadConfig();
122
- if (!config) return undefined;
123
- const pkg = config.app[packageName];
124
- if (!pkg) return undefined;
125
- return pkg[env];
241
+ /**
242
+ * Resolve packages with modes, existence checks, and auto-remote detection.
243
+ * Merges functionality of resolvePackageModes() and getExistingPackages().
244
+ */
245
+ export async function resolvePackages(
246
+ packages: string[],
247
+ requestedModes: Record<string, SourceMode>,
248
+ ): Promise<{
249
+ resolved: Record<string, PackageResolution>;
250
+ autoRemote: string[];
251
+ }> {
252
+ const dir = getProjectRoot();
253
+ const resolved: Record<string, PackageResolution> = {};
254
+ const autoRemote: string[] = [];
255
+
256
+ // Get config for port calculations
257
+ const config = getConfig();
258
+
259
+ for (const pkg of packages) {
260
+ const exists = await fileExists(`${dir}/${pkg}/package.json`);
261
+ const requestedMode = requestedModes[pkg] ?? "local";
262
+
263
+ // Auto-switch to remote if package doesn't exist locally
264
+ const mode =
265
+ !exists && requestedMode === "local" ? "remote" : requestedMode;
266
+ if (!exists && requestedMode === "local") {
267
+ autoRemote.push(pkg);
268
+ }
269
+
270
+ // Calculate port and URL
271
+ let port = 0;
272
+ let url = "";
273
+
274
+ if (config && pkg in config.app) {
275
+ const pkgConfig = config.app[pkg];
276
+ if (pkg === "host") {
277
+ port = parsePort(pkgConfig.development);
278
+ url = mode === "remote" ? pkgConfig.production : pkgConfig.development;
279
+ } else {
280
+ // For ui/api and other packages, access via index signature
281
+ const devUrl = (pkgConfig as { development?: string }).development;
282
+ const prodUrl = (pkgConfig as { production?: string }).production;
283
+ if (devUrl) {
284
+ port = parsePort(devUrl);
285
+ url = mode === "remote" ? (prodUrl ?? devUrl) : devUrl;
286
+ }
287
+ }
288
+ }
289
+
290
+ resolved[pkg] = { name: pkg, mode, exists, port, url };
291
+ }
292
+
293
+ return { resolved, autoRemote };
126
294
  }
127
295
 
128
- export function getAccount(): string {
129
- const config = loadConfig();
130
- if (!config) {
131
- throw new Error("No bos.config.json found");
132
- }
133
- return config.account;
134
- }
135
-
136
- export function getComponentUrl(
137
- component: "host" | "ui" | "api",
138
- source: SourceMode
139
- ): string {
140
- const config = loadConfig();
141
- if (!config) {
142
- throw new Error("No bos.config.json found");
143
- }
144
-
145
- if (component === "host") {
146
- return source === "remote" ? config.app.host.production : config.app.host.development;
147
- }
148
-
149
- const componentConfig = config.app[component];
150
- if (!componentConfig || !("name" in componentConfig)) {
151
- throw new Error(`Component ${component} not found in bos.config.json`);
152
- }
153
-
154
- return source === "remote" ? componentConfig.production : componentConfig.development;
155
- }
296
+ // ============================================================================
297
+ // INTERNAL HELPERS
298
+ // ============================================================================
156
299
 
300
+ /**
301
+ * Parse port from URL string.
302
+ */
157
303
  export function parsePort(url: string): number {
158
- try {
159
- const parsed = new URL(url);
160
- return parsed.port ? parseInt(parsed.port, 10) : (parsed.protocol === "https:" ? 443 : 80);
161
- } catch {
162
- return 3000;
163
- }
164
- }
165
-
166
- export function getPortsFromConfig(): PortConfig {
167
- const config = loadConfig();
168
- if (!config) {
169
- return { host: 3000, ui: 3002, api: 3014 };
170
- }
171
- return {
172
- host: parsePort(config.app.host.development),
173
- ui: config.app.ui ? parsePort((config.app.ui as RemoteConfig).development) : 3002,
174
- api: config.app.api ? parsePort((config.app.api as RemoteConfig).development) : 3014,
175
- };
304
+ try {
305
+ const parsed = new URL(url);
306
+ return parsed.port
307
+ ? parseInt(parsed.port, 10)
308
+ : parsed.protocol === "https:"
309
+ ? 443
310
+ : 80;
311
+ } catch {
312
+ return 3000;
313
+ }
176
314
  }
177
315
 
178
- export function getConfigPath(): string {
179
- if (!configDir) {
180
- loadConfig();
181
- }
182
- return `${configDir}/bos.config.json`;
316
+ /**
317
+ * Check if a file exists.
318
+ */
319
+ async function fileExists(path: string): Promise<boolean> {
320
+ return access(path)
321
+ .then(() => true)
322
+ .catch(() => false);
183
323
  }
184
324
 
185
- export function getHostRemoteUrl(): string | undefined {
186
- const config = loadConfig();
187
- if (!config) return undefined;
188
- return config.app.host.production || undefined;
325
+ /**
326
+ * Resolve BOS URL to Graph path.
327
+ * bos://{account}/{gateway} → {account}/bos/gateways/{gateway}/bos.config.json
328
+ */
329
+ function resolveBosUrl(bosUrl: string): string {
330
+ const match = bosUrl.match(/^bos:\/\/([^/]+)\/(.+)$/);
331
+ if (!match) {
332
+ throw new ConfigResolutionError(
333
+ `Invalid BOS URL format: ${bosUrl}. Expected: bos://{account}/{gateway}`,
334
+ );
335
+ }
336
+ const [, account, gateway] = match;
337
+ return `${account}/bos/gateways/${gateway}/bos.config.json`;
189
338
  }
190
339
 
191
- export function getGatewayUrl(env: "development" | "production" = "development"): string {
192
- const config = loadConfig();
193
- if (!config) {
194
- throw new Error("No bos.config.json found");
195
- }
196
- return config.gateway[env];
340
+ /**
341
+ * Fetch config from NEAR Social (BOS URL) with 10s timeout.
342
+ */
343
+ async function fetchBosConfig(bosUrl: string): Promise<BosConfigInput> {
344
+ const configPath = resolveBosUrl(bosUrl);
345
+ const graph = new Graph();
346
+
347
+ try {
348
+ const data = await Effect.runPromise(
349
+ Effect.tryPromise({
350
+ try: () =>
351
+ Promise.race([
352
+ graph.get({ keys: [configPath] }),
353
+ new Promise<never>((_, reject) =>
354
+ setTimeout(() => reject(new Error("Timeout after 10s")), 10000),
355
+ ),
356
+ ]),
357
+ catch: (e) => new ConfigFetchError(bosUrl, e),
358
+ }),
359
+ );
360
+
361
+ if (!data) {
362
+ throw new ConfigFetchError(bosUrl, "No data returned");
363
+ }
364
+
365
+ // Navigate nested structure
366
+ const parts = configPath.split("/");
367
+ let current: unknown = data;
368
+ for (const part of parts) {
369
+ if (current && typeof current === "object" && part in current) {
370
+ current = (current as Record<string, unknown>)[part];
371
+ } else {
372
+ throw new ConfigFetchError(bosUrl, `Path not found: ${configPath}`);
373
+ }
374
+ }
375
+
376
+ if (typeof current !== "string") {
377
+ throw new ConfigFetchError(bosUrl, "Config is not a string");
378
+ }
379
+
380
+ return JSON.parse(current) as BosConfigInput;
381
+ } catch (error) {
382
+ if (error instanceof ConfigFetchError) {
383
+ throw error;
384
+ }
385
+ throw new ConfigFetchError(bosUrl, error);
386
+ }
197
387
  }
198
388
 
199
- export async function packageExists(pkg: string): Promise<boolean> {
200
- const dir = getConfigDir();
201
- return Bun.file(`${dir}/${pkg}/package.json`).exists();
389
+ /**
390
+ * Load a single config file (local or BOS URL).
391
+ */
392
+ async function loadConfigFile(
393
+ configPath: string,
394
+ baseDir?: string,
395
+ ): Promise<BosConfigInput> {
396
+ // BOS URL
397
+ if (configPath.startsWith("bos://")) {
398
+ return fetchBosConfig(configPath);
399
+ }
400
+
401
+ // Local file
402
+ const resolvedPath = isAbsolute(configPath)
403
+ ? configPath
404
+ : baseDir
405
+ ? resolve(baseDir, configPath)
406
+ : resolve(process.cwd(), configPath);
407
+
408
+ const text = await readFile(resolvedPath, "utf-8");
409
+ return JSON.parse(text) as BosConfigInput;
202
410
  }
203
411
 
204
- export async function resolvePackageModes(
205
- packages: string[],
206
- input: Record<string, SourceMode | undefined>
207
- ): Promise<{ modes: Record<string, SourceMode>; autoRemote: string[] }> {
208
- const dir = getConfigDir();
209
- const modes: Record<string, SourceMode> = {};
210
- const autoRemote: string[] = [];
211
-
212
- for (const pkg of packages) {
213
- const exists = await Bun.file(`${dir}/${pkg}/package.json`).exists();
214
- const requestedMode = input[pkg] ?? "local";
215
-
216
- if (!exists && requestedMode === "local") {
217
- modes[pkg] = "remote";
218
- autoRemote.push(pkg);
219
- } else {
220
- modes[pkg] = requestedMode;
221
- }
222
- }
223
-
224
- return { modes, autoRemote };
412
+ /**
413
+ * Deep merge configs with inheritance.
414
+ * - Objects: recursive merge
415
+ * - Arrays: child replaces parent
416
+ * - Primitives: child wins
417
+ */
418
+ function mergeConfigs(
419
+ parent: BosConfigInput,
420
+ child: BosConfigInput,
421
+ ): BosConfigInput {
422
+ const merged: BosConfigInput = { ...parent, ...child };
423
+
424
+ // Merge app configs deeply
425
+ if (parent.app || child.app) {
426
+ merged.app = { ...parent.app, ...child.app };
427
+ const parentApps = parent.app || {};
428
+ const childApps = child.app || {};
429
+
430
+ for (const key of new Set([
431
+ ...Object.keys(parentApps),
432
+ ...Object.keys(childApps),
433
+ ])) {
434
+ const parentApp = parentApps[key as keyof typeof parentApps];
435
+ const childApp = childApps[key as keyof typeof childApps];
436
+
437
+ if (parentApp && childApp) {
438
+ (merged.app as Record<string, unknown>)[key] = {
439
+ ...parentApp,
440
+ ...childApp,
441
+ };
442
+ } else if (childApp) {
443
+ (merged.app as Record<string, unknown>)[key] = childApp;
444
+ } else if (parentApp) {
445
+ (merged.app as Record<string, unknown>)[key] = parentApp;
446
+ }
447
+ }
448
+ }
449
+
450
+ // Merge shared deps deeply
451
+ if (parent.shared || child.shared) {
452
+ merged.shared = { ...parent.shared, ...child.shared };
453
+ const parentShared = parent.shared || {};
454
+ const childShared = child.shared || {};
455
+
456
+ // Deep merge each shared category (ui, api, etc.)
457
+ for (const category of new Set([
458
+ ...Object.keys(parentShared),
459
+ ...Object.keys(childShared),
460
+ ])) {
461
+ const parentCategory =
462
+ parentShared[category as keyof typeof parentShared];
463
+ const childCategory = childShared[category as keyof typeof childShared];
464
+
465
+ if (parentCategory && childCategory) {
466
+ (merged.shared as Record<string, unknown>)[category] = {
467
+ ...parentCategory,
468
+ ...childCategory,
469
+ };
470
+ } else if (childCategory) {
471
+ (merged.shared as Record<string, unknown>)[category] = childCategory;
472
+ } else if (parentCategory) {
473
+ (merged.shared as Record<string, unknown>)[category] = parentCategory;
474
+ }
475
+ }
476
+ }
477
+
478
+ return merged;
225
479
  }
226
480
 
227
- export async function getExistingPackages(packages: string[]): Promise<{ existing: string[]; missing: string[] }> {
228
- const dir = getConfigDir();
229
- const existing: string[] = [];
230
- const missing: string[] = [];
231
-
232
- for (const pkg of packages) {
233
- const exists = await Bun.file(`${dir}/${pkg}/package.json`).exists();
234
- if (exists) {
235
- existing.push(pkg);
236
- } else {
237
- missing.push(pkg);
238
- }
239
- }
240
-
241
- return { existing, missing };
481
+ /**
482
+ * Recursively resolve config with extends inheritance.
483
+ */
484
+ async function resolveConfigWithExtends(
485
+ configPath: string,
486
+ baseDir: string,
487
+ visited: Set<string>,
488
+ chain: string[],
489
+ ): Promise<BosConfigInput> {
490
+ // Check for circular dependencies
491
+ if (visited.has(configPath)) {
492
+ throw new ConfigCircularExtendsError([...visited, configPath]);
493
+ }
494
+
495
+ // Load current config
496
+ const config = await loadConfigFile(configPath, baseDir);
497
+
498
+ // Track in chain if it's a BOS URL
499
+ if (configPath.startsWith("bos://")) {
500
+ chain.push(configPath);
501
+ }
502
+
503
+ // If no extends, return as-is
504
+ if (!config.extends) {
505
+ return config;
506
+ }
507
+
508
+ // Mark as visited
509
+ const newVisited = new Set(visited);
510
+ newVisited.add(configPath);
511
+
512
+ // Resolve parent
513
+ const parentPath = config.extends;
514
+ const parentBaseDir = parentPath.startsWith("bos://")
515
+ ? baseDir
516
+ : isAbsolute(parentPath)
517
+ ? dirname(parentPath)
518
+ : baseDir;
519
+
520
+ const parentConfig = await resolveConfigWithExtends(
521
+ parentPath,
522
+ parentBaseDir,
523
+ newVisited,
524
+ chain,
525
+ );
526
+
527
+ // Merge and return
528
+ return mergeConfigs(parentConfig, config);
242
529
  }
243
530
 
244
- export async function loadBosConfig(
245
- env: "development" | "production" = "production"
246
- ): Promise<RuntimeConfig> {
247
- const configPath = process.env.BOS_CONFIG_PATH;
248
-
249
- let bosConfig: BosConfig;
250
- if (configPath) {
251
- const file = Bun.file(configPath);
252
- const text = await file.text();
253
- bosConfig = JSON.parse(text) as BosConfig;
254
- } else {
255
- const config = loadConfig();
256
- if (!config) {
257
- throw new Error("No bos.config.json found");
258
- }
259
- bosConfig = config;
260
- }
261
-
262
- const uiConfig = bosConfig.app.ui as RemoteConfig;
263
- const apiConfig = bosConfig.app.api as RemoteConfig;
264
-
265
- return {
266
- env,
267
- account: bosConfig.account,
268
- title: bosConfig.account,
269
- hostUrl: bosConfig.app.host[env],
270
- shared: bosConfig.shared,
271
- ui: {
272
- name: uiConfig.name,
273
- url: uiConfig[env],
274
- ssrUrl: uiConfig.ssr,
275
- source: "remote",
276
- },
277
- api: {
278
- name: apiConfig.name,
279
- url: apiConfig[env],
280
- source: "remote",
281
- proxy: apiConfig.proxy,
282
- variables: apiConfig.variables,
283
- secrets: apiConfig.secrets,
284
- },
285
- };
531
+ /**
532
+ * Build runtime config from BosConfig.
533
+ */
534
+ function buildRuntimeConfig(
535
+ config: BosConfig,
536
+ env: "development" | "production",
537
+ ): RuntimeConfig {
538
+ const uiConfig = config.app.ui as RemoteConfig | undefined;
539
+ const apiConfig = config.app.api as RemoteConfig | undefined;
540
+
541
+ // Extract properties from api config
542
+ const apiProxy = apiConfig?.proxy;
543
+ const apiVariables = apiConfig?.variables;
544
+ const apiSecrets = apiConfig?.secrets;
545
+
546
+ // Env var overrides
547
+ const uiUrlOverride = process.env.BOS_UI_URL;
548
+ const uiSsrUrlOverride = process.env.BOS_UI_SSR_URL;
549
+
550
+ return {
551
+ env,
552
+ account: config.account,
553
+ title: config.account,
554
+ hostUrl: config.app.host[env],
555
+ shared: config.shared,
556
+ ui: {
557
+ name: uiConfig?.name ?? "ui",
558
+ url: uiUrlOverride ?? uiConfig?.[env] ?? "",
559
+ entry: `${uiUrlOverride ?? uiConfig?.[env] ?? ""}/remoteEntry.js`,
560
+ ssrUrl: uiSsrUrlOverride ?? uiConfig?.ssr,
561
+ source: env === "development" ? "local" : "remote",
562
+ },
563
+ api: {
564
+ name: apiConfig?.name ?? "api",
565
+ url: apiConfig?.[env] ?? "",
566
+ entry: `${apiConfig?.[env] ?? ""}/remoteEntry.js`,
567
+ source: env === "development" ? "local" : "remote",
568
+ proxy: apiProxy,
569
+ variables: apiVariables,
570
+ secrets: apiSecrets,
571
+ },
572
+ };
286
573
  }