everything-dev 1.12.4 → 1.13.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.
Files changed (124) hide show
  1. package/cli.js +1 -1
  2. package/dist/app.cjs +17 -5
  3. package/dist/app.cjs.map +1 -1
  4. package/dist/app.mjs +17 -5
  5. package/dist/app.mjs.map +1 -1
  6. package/dist/cli/init.cjs +143 -66
  7. package/dist/cli/init.cjs.map +1 -1
  8. package/dist/cli/init.d.cts +1 -1
  9. package/dist/cli/init.d.cts.map +1 -1
  10. package/dist/cli/init.d.mts +1 -1
  11. package/dist/cli/init.d.mts.map +1 -1
  12. package/dist/cli/init.mjs +144 -67
  13. package/dist/cli/init.mjs.map +1 -1
  14. package/dist/cli/prompts.cjs +3 -3
  15. package/dist/cli/prompts.cjs.map +1 -1
  16. package/dist/cli/prompts.mjs +3 -3
  17. package/dist/cli/prompts.mjs.map +1 -1
  18. package/dist/cli/sync.cjs +15 -56
  19. package/dist/cli/sync.cjs.map +1 -1
  20. package/dist/cli/sync.mjs +15 -56
  21. package/dist/cli/sync.mjs.map +1 -1
  22. package/dist/cli/upgrade.cjs +3 -1
  23. package/dist/cli/upgrade.cjs.map +1 -1
  24. package/dist/cli/upgrade.mjs +3 -1
  25. package/dist/cli/upgrade.mjs.map +1 -1
  26. package/dist/config.cjs +206 -69
  27. package/dist/config.cjs.map +1 -1
  28. package/dist/config.d.cts +13 -6
  29. package/dist/config.d.cts.map +1 -1
  30. package/dist/config.d.mts +13 -6
  31. package/dist/config.d.mts.map +1 -1
  32. package/dist/config.mjs +201 -71
  33. package/dist/config.mjs.map +1 -1
  34. package/dist/contract.d.cts +104 -8
  35. package/dist/contract.d.cts.map +1 -1
  36. package/dist/contract.d.mts +104 -8
  37. package/dist/contract.d.mts.map +1 -1
  38. package/dist/host.cjs +34 -1
  39. package/dist/host.cjs.map +1 -1
  40. package/dist/host.d.cts.map +1 -1
  41. package/dist/host.d.mts.map +1 -1
  42. package/dist/host.mjs +34 -1
  43. package/dist/host.mjs.map +1 -1
  44. package/dist/index.cjs +16 -0
  45. package/dist/index.d.cts +5 -3
  46. package/dist/index.d.mts +5 -3
  47. package/dist/index.mjs +5 -3
  48. package/dist/merge.cjs +113 -0
  49. package/dist/merge.cjs.map +1 -0
  50. package/dist/merge.d.cts +7 -0
  51. package/dist/merge.d.cts.map +1 -0
  52. package/dist/merge.d.mts +7 -0
  53. package/dist/merge.d.mts.map +1 -0
  54. package/dist/merge.mjs +107 -0
  55. package/dist/merge.mjs.map +1 -0
  56. package/dist/plugin.cjs +117 -105
  57. package/dist/plugin.cjs.map +1 -1
  58. package/dist/plugin.d.cts +114 -8
  59. package/dist/plugin.d.cts.map +1 -1
  60. package/dist/plugin.d.mts +114 -8
  61. package/dist/plugin.d.mts.map +1 -1
  62. package/dist/plugin.mjs +117 -105
  63. package/dist/plugin.mjs.map +1 -1
  64. package/dist/service-descriptor.cjs +21 -0
  65. package/dist/service-descriptor.cjs.map +1 -1
  66. package/dist/service-descriptor.d.cts +23 -1
  67. package/dist/service-descriptor.d.cts.map +1 -1
  68. package/dist/service-descriptor.d.mts +23 -1
  69. package/dist/service-descriptor.d.mts.map +1 -1
  70. package/dist/service-descriptor.mjs +21 -0
  71. package/dist/service-descriptor.mjs.map +1 -1
  72. package/dist/shared.cjs +24 -2
  73. package/dist/shared.cjs.map +1 -1
  74. package/dist/shared.d.cts +3 -0
  75. package/dist/shared.d.cts.map +1 -1
  76. package/dist/shared.d.mts +3 -0
  77. package/dist/shared.d.mts.map +1 -1
  78. package/dist/shared.mjs +25 -3
  79. package/dist/shared.mjs.map +1 -1
  80. package/dist/sidebar.cjs +124 -0
  81. package/dist/sidebar.cjs.map +1 -0
  82. package/dist/sidebar.d.cts +8 -0
  83. package/dist/sidebar.d.cts.map +1 -0
  84. package/dist/sidebar.d.mts +8 -0
  85. package/dist/sidebar.d.mts.map +1 -0
  86. package/dist/sidebar.mjs +122 -0
  87. package/dist/sidebar.mjs.map +1 -0
  88. package/dist/types.cjs +104 -10
  89. package/dist/types.cjs.map +1 -1
  90. package/dist/types.d.cts +256 -29
  91. package/dist/types.d.cts.map +1 -1
  92. package/dist/types.d.mts +256 -29
  93. package/dist/types.d.mts.map +1 -1
  94. package/dist/types.mjs +100 -11
  95. package/dist/types.mjs.map +1 -1
  96. package/dist/utils/path-match.cjs +18 -0
  97. package/dist/utils/path-match.cjs.map +1 -0
  98. package/dist/utils/path-match.mjs +17 -0
  99. package/dist/utils/path-match.mjs.map +1 -0
  100. package/dist/utils/save-config.cjs +19 -0
  101. package/dist/utils/save-config.cjs.map +1 -0
  102. package/dist/utils/save-config.mjs +18 -0
  103. package/dist/utils/save-config.mjs.map +1 -0
  104. package/package.json +3 -2
  105. package/skills/dev-workflow/SKILL.md +8 -0
  106. package/skills/extends-config/SKILL.md +132 -0
  107. package/skills/init-upgrade/SKILL.md +128 -0
  108. package/skills/publish-sync/SKILL.md +30 -0
  109. package/src/app.ts +15 -5
  110. package/src/cli/init.ts +199 -100
  111. package/src/cli/prompts.ts +2 -2
  112. package/src/cli/sync.ts +27 -96
  113. package/src/cli/upgrade.ts +2 -0
  114. package/src/config.ts +306 -119
  115. package/src/host.ts +45 -0
  116. package/src/index.ts +1 -0
  117. package/src/merge.ts +198 -0
  118. package/src/plugin.ts +340 -318
  119. package/src/service-descriptor.ts +23 -0
  120. package/src/shared.ts +48 -5
  121. package/src/sidebar.ts +162 -0
  122. package/src/types.ts +134 -28
  123. package/src/utils/path-match.ts +16 -0
  124. package/src/utils/save-config.ts +20 -0
package/src/config.ts CHANGED
@@ -1,12 +1,30 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, resolve } from "node:path";
3
- import { fetchBosConfigFromFastKv } from "./fastkv";
3
+ import { fetchBosConfigFromFastKv, fetchPluginFromRegistry, parsePluginBosUrl } from "./fastkv";
4
+ import {
5
+ type BosEnv,
6
+ isPlainObject,
7
+ mergeBosConfigWithExtends,
8
+ type ResolvedConfigMeta,
9
+ rebuildOrderedConfig,
10
+ resolveExtendsRef,
11
+ } from "./merge";
4
12
  import { getNetworkIdForAccount } from "./network";
5
- import type { BosConfig, BosConfigInput, RuntimeConfig, RuntimePluginConfig } from "./types";
13
+ import type {
14
+ BosConfig,
15
+ BosConfigInput,
16
+ BosPluginRef,
17
+ ExtendsConfig,
18
+ PluginEntryValue,
19
+ RuntimeConfig,
20
+ RuntimePluginConfig,
21
+ SharedDepConfig,
22
+ } from "./types";
6
23
  import { BosConfigSchema } from "./types";
7
24
 
8
25
  const LOCAL_PREFIX = "local:";
9
26
  const DEFAULT_HOST_PORT = 3000;
27
+ const RESOLVED_CONFIG_FILENAME = "bos.resolved-config.json";
10
28
 
11
29
  interface RuntimeTarget {
12
30
  source: "local" | "remote";
@@ -59,7 +77,7 @@ export interface ConfigResult {
59
77
  export async function loadConfig(options?: {
60
78
  cwd?: string;
61
79
  path?: string;
62
- env?: "development" | "production";
80
+ env?: BosEnv;
63
81
  }): Promise<ConfigResult | null> {
64
82
  const configPath = options?.path ?? findConfigPath(options?.cwd);
65
83
  if (!configPath) {
@@ -68,21 +86,25 @@ export async function loadConfig(options?: {
68
86
  }
69
87
 
70
88
  const baseDir = dirname(configPath);
89
+ const env = options?.env ?? "development";
90
+ const runtimeEnv: BosEnv = env === "staging" ? "production" : env;
71
91
 
72
92
  try {
73
93
  const extendedChain: string[] = [];
74
- const parsed = await resolveConfigWithExtends(configPath, baseDir, new Set(), extendedChain);
94
+ const parsed = await resolveConfigWithExtends(
95
+ configPath,
96
+ baseDir,
97
+ new Set(),
98
+ extendedChain,
99
+ env,
100
+ );
75
101
  const config = BosConfigSchema.parse(parsed);
76
102
 
77
103
  cachedConfig = config;
78
104
  projectRoot = baseDir;
79
105
 
80
- const pluginRuntime = await resolveRuntimePlugins(
81
- config.plugins ?? {},
82
- baseDir,
83
- options?.env ?? "development",
84
- );
85
- const runtime = buildRuntimeConfig(config, baseDir, options?.env ?? "development", {
106
+ const pluginRuntime = await resolveRuntimePlugins(config.plugins ?? {}, baseDir, runtimeEnv);
107
+ const runtime = buildRuntimeConfig(config, baseDir, runtimeEnv, {
86
108
  plugins: pluginRuntime,
87
109
  });
88
110
 
@@ -103,7 +125,7 @@ export async function loadConfig(options?: {
103
125
  export async function loadBosConfig(options?: {
104
126
  cwd?: string;
105
127
  path?: string;
106
- env?: "development" | "production";
128
+ env?: BosEnv;
107
129
  }): Promise<RuntimeConfig> {
108
130
  const result = await loadConfig(options);
109
131
  if (!result) {
@@ -116,12 +138,84 @@ export async function loadBosConfig(options?: {
116
138
  export async function buildRuntimePluginsForConfig(
117
139
  config: BosConfig,
118
140
  baseDir: string,
119
- env: "development" | "production",
141
+ env: BosEnv,
120
142
  ): Promise<Record<string, RuntimePluginConfig> | undefined> {
121
143
  const plugins = await resolveRuntimePlugins(config.plugins ?? {}, baseDir, env);
122
144
  return Object.keys(plugins).length > 0 ? plugins : undefined;
123
145
  }
124
146
 
147
+ export function getResolvedConfigPath(configDir: string): string {
148
+ return join(configDir, ".bos", RESOLVED_CONFIG_FILENAME);
149
+ }
150
+
151
+ export function loadResolvedConfig(configDir: string): BosConfig | null {
152
+ const resolvedPath = getResolvedConfigPath(configDir);
153
+ if (!existsSync(resolvedPath)) return null;
154
+ try {
155
+ const raw = JSON.parse(readFileSync(resolvedPath, "utf-8"));
156
+ if (!isPlainObject(raw)) return null;
157
+ const { _resolved, ...configData } = raw;
158
+ return BosConfigSchema.parse(configData);
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ export function writeResolvedConfig(
165
+ configDir: string,
166
+ config: BosConfig,
167
+ env: BosEnv,
168
+ extendsChain?: string[],
169
+ source?: string,
170
+ ): void {
171
+ const resolvedPath = getResolvedConfigPath(configDir);
172
+ const resolvedDir = dirname(resolvedPath);
173
+ if (!existsSync(resolvedDir)) {
174
+ mkdirSync(resolvedDir, { recursive: true });
175
+ }
176
+
177
+ const ordered = rebuildOrderedConfig(config);
178
+ const meta: ResolvedConfigMeta = {
179
+ env,
180
+ resolvedAt: new Date().toISOString(),
181
+ extendsChain: extendsChain ?? [],
182
+ ...(source ? { source } : {}),
183
+ };
184
+ const output = {
185
+ _resolved: meta,
186
+ ...ordered,
187
+ };
188
+
189
+ const content = `${JSON.stringify(output, null, 2)}\n`;
190
+ try {
191
+ if (readFileSync(resolvedPath, "utf-8") === content) return;
192
+ } catch {
193
+ // file doesn't exist yet
194
+ }
195
+ writeFileSync(resolvedPath, content);
196
+ }
197
+
198
+ export function resolveBosConfigPath(configDir: string): string {
199
+ const resolvedPath = getResolvedConfigPath(configDir);
200
+ if (existsSync(resolvedPath)) return resolvedPath;
201
+ return join(configDir, "bos.config.json");
202
+ }
203
+
204
+ export function readBosConfigForBuild(configDir: string): Record<string, unknown> {
205
+ const resolvedPath = getResolvedConfigPath(configDir);
206
+ if (existsSync(resolvedPath)) {
207
+ try {
208
+ const raw = JSON.parse(readFileSync(resolvedPath, "utf-8"));
209
+ if (isPlainObject(raw)) {
210
+ const { _resolved, ...configData } = raw;
211
+ return configData as Record<string, unknown>;
212
+ }
213
+ } catch {}
214
+ }
215
+ const bosConfigPath = join(configDir, "bos.config.json");
216
+ return JSON.parse(readFileSync(bosConfigPath, "utf-8")) as Record<string, unknown>;
217
+ }
218
+
125
219
  function resolveDevelopmentTarget(
126
220
  development: string | undefined,
127
221
  production: string | undefined,
@@ -150,7 +244,7 @@ export interface BuildRuntimeConfigOptions {
150
244
  export function buildRuntimeConfig(
151
245
  config: BosConfig,
152
246
  baseDir: string,
153
- env: "development" | "production",
247
+ env: BosEnv,
154
248
  options?: BuildRuntimeConfigOptions,
155
249
  ): RuntimeConfig {
156
250
  const uiConfig = config.app.ui;
@@ -196,7 +290,10 @@ export function buildRuntimeConfig(
196
290
  )
197
291
  : resolveRuntimeTarget(hostConfig.production, baseDir, "remote");
198
292
 
199
- const hostListeningUrl = resolveDevelopmentHostUrl(hostConfig.development);
293
+ const hostListeningUrl =
294
+ env === "development"
295
+ ? resolveDevelopmentHostUrl(hostConfig.development)
296
+ : `http://localhost:${hostRuntime.port ?? DEFAULT_HOST_PORT}`;
200
297
 
201
298
  const hostIsRemote = hostRuntime.source === "remote";
202
299
  const uiIsRemote = uiRuntime.source === "remote";
@@ -243,20 +340,26 @@ export function buildRuntimeConfig(
243
340
  secrets: apiConfig.secrets,
244
341
  integrity: apiIsRemote ? apiConfig.integrity : undefined,
245
342
  },
246
- auth: authConfig
247
- ? {
248
- name: resolvePluginRuntimeName(undefined, authRuntime!.localPath, authConfig.name),
249
- url: authRuntime!.url,
250
- entry: authRuntime!.url ? `${authRuntime!.url}/mf-manifest.json` : "/mf-manifest.json",
251
- localPath: authRuntime!.localPath,
252
- port: authRuntime!.port,
253
- source: authRuntime!.source,
254
- proxy: authConfig.proxy,
255
- variables: authConfig.variables,
256
- secrets: authConfig.secrets,
257
- integrity: authRuntime!.source === "remote" ? authConfig.integrity : undefined,
258
- }
259
- : undefined,
343
+ auth: (() => {
344
+ if (!authConfig || !authRuntime) return undefined;
345
+ return {
346
+ name: resolvePluginRuntimeName(undefined, authRuntime.localPath, authConfig.name),
347
+ url: authRuntime.url,
348
+ entry: authRuntime.url ? `${authRuntime.url}/mf-manifest.json` : "/mf-manifest.json",
349
+ localPath: authRuntime.localPath,
350
+ port: authRuntime.port,
351
+ source: authRuntime.source,
352
+ proxy: authConfig.proxy,
353
+ variables: authConfig.variables,
354
+ secrets: authConfig.secrets,
355
+ integrity: authRuntime.source === "remote" ? authConfig.integrity : undefined,
356
+ sidebar: authConfig.sidebar?.map((item) => ({
357
+ ...item,
358
+ to: item.to ?? "/auth",
359
+ roleRequired: item.roleRequired ?? ("member" as const),
360
+ })),
361
+ };
362
+ })(),
260
363
  plugins:
261
364
  options?.plugins && Object.keys(options.plugins).length > 0 ? options.plugins : undefined,
262
365
  };
@@ -276,74 +379,123 @@ async function resolveConfigWithExtends(
276
379
  baseDir: string,
277
380
  visited: Set<string>,
278
381
  chain: string[],
382
+ env: BosEnv = "development",
279
383
  ): Promise<BosConfigInput> {
280
384
  if (visited.has(configPath)) {
281
385
  throw new Error(`Circular extends detected: ${[...visited, configPath].join(" -> ")}`);
282
386
  }
283
387
 
284
388
  const config = await loadConfigFile(configPath, baseDir);
285
- if (configPath.startsWith("bos://")) {
286
- chain.push(configPath);
287
- }
389
+ chain.push(configPath);
288
390
 
289
391
  if (!config.extends) {
290
392
  return config;
291
393
  }
292
394
 
395
+ const extendsRef = resolveExtendsRef(config.extends as string | ExtendsConfig, env);
396
+ if (!extendsRef) {
397
+ return config;
398
+ }
399
+
293
400
  const nextVisited = new Set(visited);
294
401
  nextVisited.add(configPath);
295
- const parentPath = config.extends;
296
- const parentBaseDir = parentPath.startsWith("bos://")
402
+ const parentBaseDir = extendsRef.startsWith("bos://")
297
403
  ? baseDir
298
- : isAbsolute(parentPath)
299
- ? dirname(parentPath)
404
+ : isAbsolute(extendsRef)
405
+ ? dirname(extendsRef)
300
406
  : baseDir;
301
- const parent = await resolveConfigWithExtends(parentPath, parentBaseDir, nextVisited, chain);
407
+ const parent = await resolveConfigWithExtends(extendsRef, parentBaseDir, nextVisited, chain, env);
302
408
 
303
- return mergeConfigs(parent, config);
409
+ return mergeBosConfigWithExtends(parent, config);
304
410
  }
305
411
 
306
- function mergeConfigs(parent: BosConfigInput, child: BosConfigInput): BosConfigInput {
307
- const result = mergeValues(parent, child) as BosConfigInput;
308
- if (child.plugins !== undefined) {
309
- result.plugins = child.plugins;
412
+ type PluginOverrideValue = PluginEntryValue | null | false;
413
+
414
+ function normalizePluginEntry(raw: PluginOverrideValue): BosPluginRef | null | false {
415
+ if (raw === null || raw === false) return raw;
416
+ if (typeof raw === "string") {
417
+ return { extends: raw };
310
418
  }
311
- return result;
419
+ return raw;
312
420
  }
313
421
 
314
422
  async function resolveRuntimePlugins(
315
- plugins: Record<string, BosConfigInput>,
423
+ plugins: Record<string, PluginOverrideValue>,
316
424
  baseDir: string,
317
- env: "development" | "production",
318
- prefix: string[] = [],
425
+ env: BosEnv,
319
426
  ): Promise<Record<string, RuntimePluginConfig>> {
320
427
  const out: Record<string, RuntimePluginConfig> = {};
321
428
 
322
- for (const [pluginId, pluginInput] of Object.entries(plugins)) {
323
- const runtimeKey = [...prefix, pluginId].join("/");
324
- const { config: resolvedConfig, baseDir: pluginBaseDir } = await resolveBosConfigInput(
325
- pluginInput,
326
- baseDir,
327
- new Set(),
328
- [],
329
- );
429
+ for (const [pluginId, rawInput] of Object.entries(plugins)) {
430
+ const normalized = normalizePluginEntry(rawInput);
431
+ if (normalized === null || normalized === false) continue;
432
+
433
+ let resolvedConfig: BosConfigInput = {};
434
+ let pluginBaseDir = baseDir;
435
+
436
+ if (normalized.extends) {
437
+ try {
438
+ const extendsUrl = resolveExtendsRef(normalized.extends, env);
439
+ if (extendsUrl) {
440
+ const remoteConfig = await fetchBosConfigFromFastKv<BosConfigInput>(extendsUrl);
441
+ resolvedConfig = remoteConfig;
442
+ }
443
+ } catch {
444
+ resolvedConfig = {};
445
+ }
446
+ }
447
+
448
+ if (normalized.development?.startsWith(LOCAL_PREFIX)) {
449
+ const localPath = resolve(baseDir, normalized.development.slice(LOCAL_PREFIX.length).trim());
450
+ if (existsSync(localPath)) {
451
+ const localConfigPath = join(localPath, "bos.config.json");
452
+ if (existsSync(localConfigPath)) {
453
+ try {
454
+ const localRaw = JSON.parse(readFileSync(localConfigPath, "utf-8")) as BosConfigInput;
455
+ resolvedConfig = mergeBosConfigWithExtends(resolvedConfig, localRaw);
456
+ pluginBaseDir = localPath;
457
+ } catch {}
458
+ }
459
+ }
460
+ }
330
461
 
331
- const pluginRuntime = buildRuntimePluginConfig(
332
- runtimeKey,
462
+ if (normalized.app && isPlainObject(normalized.app)) {
463
+ const mergedApp: Record<string, unknown> = {
464
+ ...((resolvedConfig.app as Record<string, unknown>) ?? {}),
465
+ ...(normalized.app as Record<string, unknown>),
466
+ };
467
+ resolvedConfig = { ...resolvedConfig, app: mergedApp as BosConfigInput["app"] };
468
+ }
469
+ if (normalized.shared && isPlainObject(normalized.shared)) {
470
+ const mergedShared: Record<string, Record<string, SharedDepConfig>> = {
471
+ ...(resolvedConfig.shared ?? {}),
472
+ ...(normalized.shared as Record<string, Record<string, SharedDepConfig>>),
473
+ };
474
+ resolvedConfig = { ...resolvedConfig, shared: mergedShared };
475
+ }
476
+ if (normalized.sidebar) {
477
+ resolvedConfig = { ...resolvedConfig, sidebar: normalized.sidebar };
478
+ }
479
+ if (normalized.routes) {
480
+ resolvedConfig = { ...resolvedConfig, routes: normalized.routes };
481
+ }
482
+
483
+ const pluginRuntime = await buildRuntimePluginConfig(
484
+ pluginId,
333
485
  resolvedConfig,
334
486
  pluginBaseDir,
335
487
  env,
336
- pluginInput,
488
+ normalized,
337
489
  );
338
490
  if (
339
- pluginInput.name &&
340
- typeof pluginInput.name === "string" &&
491
+ normalized.name &&
492
+ typeof normalized.name === "string" &&
341
493
  !pluginRuntime.name.includes("/")
342
494
  ) {
343
- pluginRuntime.name = pluginInput.name;
495
+ pluginRuntime.name = normalized.name;
344
496
  }
345
497
 
346
- const integrity = pluginInput.integrity;
498
+ const integrity = normalized.integrity;
347
499
  if (env === "production" && integrity) {
348
500
  pluginRuntime.integrity = integrity;
349
501
  }
@@ -353,7 +505,7 @@ async function resolveRuntimePlugins(
353
505
  pluginRuntime.url &&
354
506
  !pluginRuntime.localPath &&
355
507
  typeof resolvedConfig.app?.api?.name !== "string" &&
356
- !pluginInput.name
508
+ !normalized.name
357
509
  ) {
358
510
  pluginRuntime.name = await resolveRemotePluginRuntimeName(
359
511
  pluginRuntime.url,
@@ -361,15 +513,7 @@ async function resolveRuntimePlugins(
361
513
  );
362
514
  }
363
515
 
364
- out[runtimeKey] = pluginRuntime;
365
-
366
- if (resolvedConfig.plugins && Object.keys(resolvedConfig.plugins).length > 0) {
367
- const nested = await resolveRuntimePlugins(resolvedConfig.plugins, pluginBaseDir, env, [
368
- ...prefix,
369
- pluginId,
370
- ]);
371
- Object.assign(out, nested);
372
- }
516
+ out[pluginId] = pluginRuntime;
373
517
  }
374
518
 
375
519
  return out;
@@ -377,7 +521,13 @@ async function resolveRuntimePlugins(
377
521
 
378
522
  async function resolveRemotePluginRuntimeName(baseUrl: string, fallback: string): Promise<string> {
379
523
  try {
380
- const response = await fetch(`${baseUrl.replace(/\/$/, "")}/plugin.manifest.json`);
524
+ const controller = new AbortController();
525
+ const timeout = setTimeout(() => controller.abort(), 5000);
526
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/plugin.manifest.json`, {
527
+ signal: controller.signal,
528
+ });
529
+ clearTimeout(timeout);
530
+
381
531
  if (!response.ok) {
382
532
  return fallback;
383
533
  }
@@ -394,13 +544,38 @@ async function resolveRemotePluginRuntimeName(baseUrl: string, fallback: string)
394
544
  }
395
545
  }
396
546
 
397
- function buildRuntimePluginConfig(
547
+ interface ResolvedBosPlugin {
548
+ url: string;
549
+ integrity?: string;
550
+ }
551
+
552
+ async function resolveBosPluginUrl(bosUrl: string): Promise<ResolvedBosPlugin | null> {
553
+ const parsed = parsePluginBosUrl(bosUrl);
554
+ if (!parsed) return null;
555
+
556
+ try {
557
+ const entry = await fetchPluginFromRegistry(parsed.accountId, parsed.pluginName);
558
+ if (!entry) return null;
559
+
560
+ const cdnUrl = entry.metadata.cdnUrl;
561
+ if (!cdnUrl) return null;
562
+
563
+ return {
564
+ url: cdnUrl,
565
+ integrity: entry.metadata.integrity ?? undefined,
566
+ };
567
+ } catch {
568
+ return null;
569
+ }
570
+ }
571
+
572
+ async function buildRuntimePluginConfig(
398
573
  pluginId: string,
399
574
  config: BosConfigInput,
400
575
  baseDir: string,
401
- env: "development" | "production",
402
- source: BosConfigInput,
403
- ): RuntimePluginConfig {
576
+ env: BosEnv,
577
+ source: BosPluginRef,
578
+ ): Promise<RuntimePluginConfig> {
404
579
  const apiConfig = config.app?.api ?? {};
405
580
  const apiDevelopment =
406
581
  typeof apiConfig.development === "string" ? apiConfig.development : undefined;
@@ -409,7 +584,18 @@ function buildRuntimePluginConfig(
409
584
  const sourceProduction = typeof source.production === "string" ? source.production : undefined;
410
585
  const proxy = typeof apiConfig.proxy === "string" ? apiConfig.proxy : undefined;
411
586
  const development = apiDevelopment ?? sourceDevelopment;
412
- const production = apiProduction ?? sourceProduction;
587
+ let production = apiProduction ?? sourceProduction;
588
+
589
+ if (production?.startsWith("bos://")) {
590
+ const resolved = await resolveBosPluginUrl(production);
591
+ if (resolved) {
592
+ production = resolved.url;
593
+ if (resolved.integrity && env === "production") {
594
+ source.integrity = resolved.integrity;
595
+ }
596
+ }
597
+ }
598
+
413
599
  const runtimeTarget =
414
600
  env === "development"
415
601
  ? resolveDevelopmentTarget(development, production, baseDir)
@@ -420,6 +606,25 @@ function buildRuntimePluginConfig(
420
606
  pluginId,
421
607
  );
422
608
 
609
+ const uiConfig = config.app?.ui;
610
+ const uiDevelopment =
611
+ typeof uiConfig?.development === "string" ? uiConfig.development : undefined;
612
+ const uiProduction = typeof uiConfig?.production === "string" ? uiConfig.production : undefined;
613
+ const uiRuntime =
614
+ uiConfig && (uiDevelopment || uiProduction)
615
+ ? env === "development"
616
+ ? resolveDevelopmentTarget(uiDevelopment, uiProduction, baseDir)
617
+ : resolveRuntimeTarget(uiProduction, baseDir, "remote")
618
+ : undefined;
619
+
620
+ const sidebar = (config.sidebar ?? source.sidebar)?.map((item) => ({
621
+ ...item,
622
+ to: item.to ?? `/${pluginId}`,
623
+ roleRequired: item.roleRequired ?? ("member" as const),
624
+ }));
625
+
626
+ const routes = config.routes ?? source.routes;
627
+
423
628
  return {
424
629
  name: apiName,
425
630
  url: runtimeTarget.url,
@@ -432,6 +637,24 @@ function buildRuntimePluginConfig(
432
637
  proxy: proxy ?? (typeof source.proxy === "string" ? source.proxy : undefined),
433
638
  variables: normalizeStringRecord(apiConfig.variables ?? source.variables),
434
639
  secrets: normalizeStringArray(apiConfig.secrets ?? source.secrets),
640
+ ui: uiRuntime
641
+ ? {
642
+ name: typeof uiConfig?.name === "string" ? uiConfig.name : `${apiName}-ui`,
643
+ url: uiRuntime.url,
644
+ entry: uiRuntime.url
645
+ ? `${uiRuntime.url.replace(/\/$/, "")}/mf-manifest.json`
646
+ : "/mf-manifest.json",
647
+ source: uiRuntime.source,
648
+ localPath: uiRuntime.localPath,
649
+ port: uiRuntime.port,
650
+ integrity:
651
+ uiRuntime.source === "remote" && typeof uiConfig?.integrity === "string"
652
+ ? uiConfig.integrity
653
+ : undefined,
654
+ }
655
+ : undefined,
656
+ sidebar,
657
+ routes,
435
658
  };
436
659
  }
437
660
 
@@ -459,45 +682,6 @@ export function resolvePluginRuntimeName(
459
682
  return fallback;
460
683
  }
461
684
 
462
- async function resolveBosConfigInput(
463
- input: BosConfigInput,
464
- baseDir: string,
465
- visited: Set<string>,
466
- chain: string[],
467
- ): Promise<{ config: BosConfigInput; baseDir: string }> {
468
- if (input.extends) {
469
- const parentBaseDir = input.extends.startsWith("bos://")
470
- ? baseDir
471
- : isAbsolute(input.extends)
472
- ? dirname(input.extends)
473
- : baseDir;
474
- const config = await resolveConfigWithExtends(input.extends, parentBaseDir, visited, chain);
475
- return { config: mergeConfigs(config, input), baseDir: parentBaseDir };
476
- }
477
-
478
- return { config: input, baseDir };
479
- }
480
-
481
- function mergeValues(parent: unknown, child: unknown): unknown {
482
- if (Array.isArray(parent) && Array.isArray(child)) {
483
- return child;
484
- }
485
-
486
- if (isPlainObject(parent) && isPlainObject(child)) {
487
- const merged: Record<string, unknown> = { ...parent };
488
- for (const [key, value] of Object.entries(child)) {
489
- merged[key] = key in merged ? mergeValues(merged[key], value) : value;
490
- }
491
- return merged;
492
- }
493
-
494
- return child ?? parent;
495
- }
496
-
497
- function isPlainObject(value: unknown): value is Record<string, unknown> {
498
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
499
- }
500
-
501
685
  function normalizeStringRecord(value: unknown): Record<string, string> | undefined {
502
686
  if (!isPlainObject(value)) return undefined;
503
687
  const out: Record<string, string> = {};
@@ -525,7 +709,7 @@ function resolveRuntimeTarget(
525
709
  }
526
710
 
527
711
  if (value.startsWith(LOCAL_PREFIX)) {
528
- const localTarget = value.slice(LOCAL_PREFIX.length).trim();
712
+ const localTarget = value?.slice(LOCAL_PREFIX.length).trim();
529
713
  if (!localTarget) {
530
714
  throw new Error(`Invalid local development target: ${value}`);
531
715
  }
@@ -549,7 +733,9 @@ function resolveRuntimeTarget(
549
733
  };
550
734
  }
551
735
 
552
- export function isLocalDevelopmentTarget(value: string | undefined): boolean {
736
+ export function isLocalDevelopmentTarget(
737
+ value: string | undefined,
738
+ ): value is `${typeof LOCAL_PREFIX}${string}` {
553
739
  return typeof value === "string" && value.startsWith(LOCAL_PREFIX);
554
740
  }
555
741
 
@@ -561,7 +747,7 @@ export function resolveLocalDevelopmentPath(
561
747
  return null;
562
748
  }
563
749
 
564
- const localTarget = value!.slice(LOCAL_PREFIX.length).trim();
750
+ const localTarget = value.slice(LOCAL_PREFIX.length).trim();
565
751
  return localTarget ? resolve(baseDir, localTarget) : null;
566
752
  }
567
753
 
@@ -586,5 +772,6 @@ export function parsePort(url: string): number {
586
772
  }
587
773
  }
588
774
 
775
+ export { BOS_CONFIG_ORDER, rebuildOrderedConfig } from "./merge";
589
776
  export type { BosConfig, RuntimeConfig } from "./types";
590
777
  export { BosConfigSchema } from "./types";
package/src/host.ts CHANGED
@@ -54,6 +54,15 @@ function buildClientRuntimeConfig(runtimeConfig: RuntimeConfig): ClientRuntimeCo
54
54
  integrity: runtimeConfig.api.integrity,
55
55
  }
56
56
  : undefined,
57
+ auth: runtimeConfig.auth
58
+ ? {
59
+ name: runtimeConfig.auth.name,
60
+ url: runtimeConfig.auth.url,
61
+ entry: runtimeConfig.auth.entry,
62
+ integrity: runtimeConfig.auth.integrity,
63
+ sidebar: runtimeConfig.auth.sidebar,
64
+ }
65
+ : undefined,
57
66
  plugins: runtimeConfig.plugins
58
67
  ? Object.fromEntries(
59
68
  Object.entries(runtimeConfig.plugins).map(([key, plugin]) => [
@@ -63,6 +72,18 @@ function buildClientRuntimeConfig(runtimeConfig: RuntimeConfig): ClientRuntimeCo
63
72
  url: plugin.url,
64
73
  entry: plugin.entry,
65
74
  integrity: plugin.integrity,
75
+ ...(plugin.ui
76
+ ? {
77
+ ui: {
78
+ name: plugin.ui.name,
79
+ url: plugin.ui.url,
80
+ entry: plugin.ui.entry,
81
+ source: plugin.ui.source,
82
+ integrity: plugin.ui.integrity,
83
+ },
84
+ }
85
+ : {}),
86
+ ...(plugin.sidebar ? { sidebar: plugin.sidebar } : {}),
66
87
  },
67
88
  ]),
68
89
  )
@@ -79,6 +100,15 @@ function renderLoadingShell(runtimeConfig: ClientRuntimeConfig, error?: string |
79
100
  const uiIntegrity = runtimeConfig.ui?.integrity;
80
101
  const sriAttr = uiIntegrity ? ` integrity="${uiIntegrity}" crossorigin="anonymous"` : "";
81
102
 
103
+ const pluginUiScripts = Object.values(runtimeConfig.plugins ?? {})
104
+ .filter((plugin) => plugin.ui?.url && plugin.ui.source === "remote")
105
+ .map((plugin) => {
106
+ const uiIntegrity = plugin.ui!.integrity;
107
+ const sri = uiIntegrity ? ` integrity="${uiIntegrity}" crossorigin="anonymous"` : "";
108
+ return `<script src="${plugin.ui!.url}/remoteEntry.js"${sri}></script>`;
109
+ })
110
+ .join("\n");
111
+
82
112
  return `
83
113
  <!DOCTYPE html>
84
114
  <html lang="en">
@@ -92,6 +122,7 @@ function renderLoadingShell(runtimeConfig: ClientRuntimeConfig, error?: string |
92
122
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
93
123
  </style>
94
124
  ${runtimeConfig.assetsUrl ? `<script src="${runtimeConfig.assetsUrl}/remoteEntry.js"${sriAttr}></script>` : ""}
125
+ ${pluginUiScripts}
95
126
  <script>${bootstrap}</script>
96
127
  </head>
97
128
  <body>
@@ -454,6 +485,20 @@ async function runHostServer(opts: {
454
485
  }
455
486
  }
456
487
 
488
+ for (const [pluginKey, pluginConfig] of Object.entries(runtimeConfig.plugins ?? {})) {
489
+ if (!pluginConfig.ui?.url) continue;
490
+ const pluginUiUrl = pluginConfig.ui.url;
491
+ const proxyPrefix = `/__mf/plugin-ui/${pluginKey}`;
492
+ app.all(`${proxyPrefix}/*`, async (c) => {
493
+ const targetUrl = `${pluginUiUrl}${c.req.path.replace(proxyPrefix, "")}`;
494
+ const response = await fetch(targetUrl, {
495
+ method: c.req.method,
496
+ headers: c.req.header(),
497
+ });
498
+ return response;
499
+ });
500
+ }
501
+
457
502
  app.get("*", async (c) => {
458
503
  const routerModule = await ensureRouterModuleLoaded();
459
504