everything-dev 0.2.1 → 0.3.1

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,285 +1,573 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, statSync } from "node:fs";
2
2
  import { access, readFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { Effect } from "every-plugin/effect";
5
+ import { Graph } from "near-social-js";
4
6
  import type {
5
- AppConfig,
6
- BosConfig,
7
- GatewayConfig,
8
- HostConfig,
9
- PortConfig,
10
- RemoteConfig,
11
- RuntimeConfig,
12
- 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,
13
21
  } from "./types";
14
22
 
15
- 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
+ };
16
32
 
33
+ // Constants
17
34
  export const DEFAULT_DEV_CONFIG: AppConfig = {
18
- host: "local",
19
- ui: "local",
20
- api: "local",
35
+ host: "local",
36
+ ui: "local",
37
+ api: "local",
21
38
  };
22
39
 
40
+ // Global state (for caching)
41
+ const configCache = new Map<string, { config: BosConfig; timestamp: number }>();
23
42
  let cachedConfig: BosConfig | null = null;
24
- let configDir: string | null = null;
43
+ let projectRoot: string | null = null;
25
44
  let configLoaded = false;
26
45
 
27
- export function findConfigPath(startDir: string): string | null {
28
- let dir = startDir;
29
- while (dir !== "/") {
30
- const configPath = join(dir, "bos.config.json");
31
- if (existsSync(configPath) && statSync(configPath).size > 0) {
32
- return configPath;
33
- }
34
- dir = dirname(dir);
35
- }
36
- return null;
37
- }
38
-
39
- function findConfigPathSync(startDir: string): string | null {
40
- let dir = startDir;
41
- while (dir !== "/") {
42
- const configPath = join(dir, "bos.config.json");
43
- if (existsSync(configPath) && statSync(configPath).size > 0) {
44
- return configPath;
45
- }
46
- dir = dirname(dir);
47
- }
48
- return null;
49
- }
50
-
51
- export function loadConfig(cwd?: string): BosConfig | null {
52
- if (configLoaded) return cachedConfig;
53
-
54
- const startDir = cwd ?? process.cwd();
55
- const configPath = findConfigPathSync(startDir);
56
-
57
- if (!configPath) {
58
- configLoaded = true;
59
- configDir = startDir;
60
- return null;
61
- }
62
-
63
- configDir = dirname(configPath);
64
- const content = require(configPath);
65
- cachedConfig = content as BosConfig;
66
- configLoaded = true;
67
- 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;
68
58
  }
69
59
 
70
- export function setConfig(config: BosConfig, dir?: string): void {
71
- cachedConfig = config;
72
- configDir = dir ?? process.cwd();
73
- 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;
74
73
  }
75
74
 
76
- export function getConfigDir(): string {
77
- if (!configLoaded) {
78
- loadConfig();
79
- }
80
- return configDir!;
75
+ /**
76
+ * Get the cached config. Returns null if not loaded yet.
77
+ */
78
+ export function getConfig(): BosConfig | null {
79
+ return cachedConfig;
81
80
  }
82
81
 
83
- export function getRemotes(): string[] {
84
- const config = loadConfig();
85
- if (!config) return [];
86
- 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;
87
91
  }
88
92
 
89
- export function getPackages(): string[] {
90
- const config = loadConfig();
91
- if (!config) return [];
92
- 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
+ };
93
112
  }
94
113
 
95
- export function getRemote(name: string): RemoteConfig | undefined {
96
- const config = loadConfig();
97
- if (!config) return undefined;
98
- const remote = config.app[name];
99
- if (remote && "name" in remote) {
100
- return remote as RemoteConfig;
101
- }
102
- 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;
103
123
  }
104
124
 
105
- export function getHost(): HostConfig {
106
- const config = loadConfig();
107
- if (!config) {
108
- throw new Error("No bos.config.json found");
109
- }
110
- 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
+ }
111
239
  }
112
240
 
113
- export function getUrl(
114
- packageName: string,
115
- env: "development" | "production" = "development"
116
- ): string | undefined {
117
- const config = loadConfig();
118
- if (!config) return undefined;
119
- const pkg = config.app[packageName];
120
- if (!pkg) return undefined;
121
- 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 };
122
294
  }
123
295
 
124
- export function getAccount(): string {
125
- const config = loadConfig();
126
- if (!config) {
127
- throw new Error("No bos.config.json found");
128
- }
129
- return config.account;
130
- }
131
-
132
- export function getComponentUrl(
133
- component: "host" | "ui" | "api",
134
- source: SourceMode
135
- ): string {
136
- const config = loadConfig();
137
- if (!config) {
138
- throw new Error("No bos.config.json found");
139
- }
140
-
141
- if (component === "host") {
142
- return source === "remote" ? config.app.host.production : config.app.host.development;
143
- }
144
-
145
- const componentConfig = config.app[component];
146
- if (!componentConfig || !("name" in componentConfig)) {
147
- throw new Error(`Component ${component} not found in bos.config.json`);
148
- }
149
-
150
- return source === "remote" ? componentConfig.production : componentConfig.development;
151
- }
296
+ // ============================================================================
297
+ // INTERNAL HELPERS
298
+ // ============================================================================
152
299
 
300
+ /**
301
+ * Parse port from URL string.
302
+ */
153
303
  export function parsePort(url: string): number {
154
- try {
155
- const parsed = new URL(url);
156
- return parsed.port ? parseInt(parsed.port, 10) : (parsed.protocol === "https:" ? 443 : 80);
157
- } catch {
158
- return 3000;
159
- }
160
- }
161
-
162
- export function getPortsFromConfig(): PortConfig {
163
- const config = loadConfig();
164
- if (!config) {
165
- return { host: 3000, ui: 3002, api: 3014 };
166
- }
167
- return {
168
- host: parsePort(config.app.host.development),
169
- ui: config.app.ui ? parsePort((config.app.ui as RemoteConfig).development) : 3002,
170
- api: config.app.api ? parsePort((config.app.api as RemoteConfig).development) : 3014,
171
- };
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
+ }
172
314
  }
173
315
 
174
- export function getConfigPath(): string {
175
- if (!configDir) {
176
- loadConfig();
177
- }
178
- return `${configDir}/bos.config.json`;
179
- }
180
-
181
- export function getHostRemoteUrl(): string | undefined {
182
- const config = loadConfig();
183
- if (!config) return undefined;
184
- return config.app.host.production || undefined;
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);
185
323
  }
186
324
 
187
- export function getGatewayUrl(env: "development" | "production" = "development"): string {
188
- const config = loadConfig();
189
- if (!config) {
190
- throw new Error("No bos.config.json found");
191
- }
192
- return config.gateway[env];
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`;
193
338
  }
194
339
 
195
- async function fileExists(path: string): Promise<boolean> {
196
- return access(path).then(() => true).catch(() => false);
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 fileExists(`${dir}/${pkg}/package.json`);
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 fileExists(`${dir}/${pkg}/package.json`);
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 fileExists(`${dir}/${pkg}/package.json`);
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 text = await readFile(configPath, "utf-8");
252
- bosConfig = JSON.parse(text) as BosConfig;
253
- } else {
254
- const config = loadConfig();
255
- if (!config) {
256
- throw new Error("No bos.config.json found");
257
- }
258
- bosConfig = config;
259
- }
260
-
261
- const uiConfig = bosConfig.app.ui as RemoteConfig;
262
- const apiConfig = bosConfig.app.api as RemoteConfig;
263
-
264
- return {
265
- env,
266
- account: bosConfig.account,
267
- title: bosConfig.account,
268
- hostUrl: bosConfig.app.host[env],
269
- shared: bosConfig.shared,
270
- ui: {
271
- name: uiConfig.name,
272
- url: uiConfig[env],
273
- ssrUrl: uiConfig.ssr,
274
- source: "remote",
275
- },
276
- api: {
277
- name: apiConfig.name,
278
- url: apiConfig[env],
279
- source: "remote",
280
- proxy: apiConfig.proxy,
281
- variables: apiConfig.variables,
282
- secrets: apiConfig.secrets,
283
- },
284
- };
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
+ };
285
573
  }