everything-dev 1.12.3 → 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 +24 -101
  3. package/dist/app.cjs.map +1 -1
  4. package/dist/app.mjs +25 -102
  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 +223 -81
  27. package/dist/config.cjs.map +1 -1
  28. package/dist/config.d.cts +21 -5
  29. package/dist/config.d.cts.map +1 -1
  30. package/dist/config.d.mts +21 -5
  31. package/dist/config.d.mts.map +1 -1
  32. package/dist/config.mjs +217 -83
  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 +17 -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 +23 -118
  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 +356 -132
  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,17 +138,93 @@ 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,
128
222
  baseDir: string,
223
+ forceSource?: "local" | "remote",
129
224
  ): RuntimeTarget {
225
+ if (forceSource === "remote") {
226
+ return resolveRuntimeTarget(production, baseDir, "remote");
227
+ }
130
228
  const devTarget = resolveRuntimeTarget(development, baseDir);
131
229
  if (devTarget.source === "local" && (!devTarget.localPath || !existsSync(devTarget.localPath))) {
132
230
  return resolveRuntimeTarget(production, baseDir, "remote");
@@ -134,36 +232,72 @@ function resolveDevelopmentTarget(
134
232
  return devTarget;
135
233
  }
136
234
 
137
- function buildRuntimeConfig(
235
+ export interface BuildRuntimeConfigOptions {
236
+ plugins?: Record<string, RuntimePluginConfig>;
237
+ hostSource?: "local" | "remote";
238
+ uiSource?: "local" | "remote";
239
+ apiSource?: "local" | "remote";
240
+ authSource?: "local" | "remote";
241
+ proxy?: string;
242
+ }
243
+
244
+ export function buildRuntimeConfig(
138
245
  config: BosConfig,
139
246
  baseDir: string,
140
- env: "development" | "production",
141
- options?: { plugins?: Record<string, RuntimePluginConfig> },
247
+ env: BosEnv,
248
+ options?: BuildRuntimeConfigOptions,
142
249
  ): RuntimeConfig {
143
250
  const uiConfig = config.app.ui;
144
251
  const apiConfig = config.app.api;
145
252
  const authConfig = config.app.auth;
146
253
  const uiRuntime =
147
254
  env === "development"
148
- ? resolveDevelopmentTarget(uiConfig.development, uiConfig.production, baseDir)
255
+ ? resolveDevelopmentTarget(
256
+ uiConfig.development,
257
+ uiConfig.production,
258
+ baseDir,
259
+ options?.uiSource,
260
+ )
149
261
  : resolveRuntimeTarget(uiConfig.production, baseDir, "remote");
150
262
  const apiRuntime =
151
263
  env === "development"
152
- ? resolveDevelopmentTarget(apiConfig.development, apiConfig.production, baseDir)
264
+ ? resolveDevelopmentTarget(
265
+ apiConfig.development,
266
+ apiConfig.production,
267
+ baseDir,
268
+ options?.apiSource,
269
+ )
153
270
  : resolveRuntimeTarget(apiConfig.production, baseDir, "remote");
154
271
  const authRuntime = authConfig
155
272
  ? env === "development"
156
- ? resolveDevelopmentTarget(authConfig.development, authConfig.production, baseDir)
273
+ ? resolveDevelopmentTarget(
274
+ authConfig.development,
275
+ authConfig.production,
276
+ baseDir,
277
+ options?.authSource,
278
+ )
157
279
  : resolveRuntimeTarget(authConfig.production, baseDir, "remote")
158
280
  : undefined;
159
281
 
160
282
  const hostConfig = config.app.host;
161
283
  const hostRuntime =
162
284
  env === "development"
163
- ? resolveDevelopmentTarget(hostConfig.development, hostConfig.production, baseDir)
285
+ ? resolveDevelopmentTarget(
286
+ hostConfig.development,
287
+ hostConfig.production,
288
+ baseDir,
289
+ options?.hostSource,
290
+ )
164
291
  : resolveRuntimeTarget(hostConfig.production, baseDir, "remote");
165
292
 
166
- const hostListeningUrl = resolveDevelopmentHostUrl(hostConfig.development);
293
+ const hostListeningUrl =
294
+ env === "development"
295
+ ? resolveDevelopmentHostUrl(hostConfig.development)
296
+ : `http://localhost:${hostRuntime.port ?? DEFAULT_HOST_PORT}`;
297
+
298
+ const hostIsRemote = hostRuntime.source === "remote";
299
+ const uiIsRemote = uiRuntime.source === "remote";
300
+ const apiIsRemote = apiRuntime.source === "remote";
167
301
 
168
302
  return {
169
303
  env,
@@ -178,9 +312,9 @@ function buildRuntimeConfig(
178
312
  localPath: hostRuntime.localPath,
179
313
  port: hostRuntime.port ?? DEFAULT_HOST_PORT,
180
314
  secrets: hostConfig.secrets,
181
- integrity: env === "production" ? hostConfig.integrity : undefined,
315
+ integrity: hostIsRemote ? hostConfig.integrity : undefined,
182
316
  source: hostRuntime.source,
183
- remoteUrl: hostRuntime.source === "remote" ? hostRuntime.url : undefined,
317
+ remoteUrl: hostIsRemote ? hostRuntime.url : undefined,
184
318
  },
185
319
  shared: config.shared,
186
320
  ui: {
@@ -189,9 +323,9 @@ function buildRuntimeConfig(
189
323
  entry: uiRuntime.url ? `${uiRuntime.url}/mf-manifest.json` : "/mf-manifest.json",
190
324
  localPath: uiRuntime.localPath,
191
325
  port: uiRuntime.port,
192
- ssrUrl: uiConfig.ssr,
193
- ssrIntegrity: env === "production" ? uiConfig.ssrIntegrity : undefined,
194
- integrity: env === "production" ? uiConfig.integrity : undefined,
326
+ ssrUrl: uiIsRemote ? uiConfig.ssr : undefined,
327
+ ssrIntegrity: uiIsRemote ? uiConfig.ssrIntegrity : undefined,
328
+ integrity: uiIsRemote ? uiConfig.integrity : undefined,
195
329
  source: uiRuntime.source,
196
330
  },
197
331
  api: {
@@ -201,25 +335,31 @@ function buildRuntimeConfig(
201
335
  localPath: apiRuntime.localPath,
202
336
  port: apiRuntime.port,
203
337
  source: apiRuntime.source,
204
- proxy: apiConfig.proxy,
338
+ proxy: options?.proxy ?? apiConfig.proxy,
205
339
  variables: apiConfig.variables,
206
340
  secrets: apiConfig.secrets,
207
- integrity: env === "production" ? apiConfig.integrity : undefined,
341
+ integrity: apiIsRemote ? apiConfig.integrity : undefined,
208
342
  },
209
- auth: authConfig
210
- ? {
211
- name: resolvePluginRuntimeName(undefined, authRuntime!.localPath, authConfig.name),
212
- url: authRuntime!.url,
213
- entry: authRuntime!.url ? `${authRuntime!.url}/mf-manifest.json` : "/mf-manifest.json",
214
- localPath: authRuntime!.localPath,
215
- port: authRuntime!.port,
216
- source: authRuntime!.source,
217
- proxy: authConfig.proxy,
218
- variables: authConfig.variables,
219
- secrets: authConfig.secrets,
220
- integrity: env === "production" ? authConfig.integrity : undefined,
221
- }
222
- : 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
+ })(),
223
363
  plugins:
224
364
  options?.plugins && Object.keys(options.plugins).length > 0 ? options.plugins : undefined,
225
365
  };
@@ -239,74 +379,123 @@ async function resolveConfigWithExtends(
239
379
  baseDir: string,
240
380
  visited: Set<string>,
241
381
  chain: string[],
382
+ env: BosEnv = "development",
242
383
  ): Promise<BosConfigInput> {
243
384
  if (visited.has(configPath)) {
244
385
  throw new Error(`Circular extends detected: ${[...visited, configPath].join(" -> ")}`);
245
386
  }
246
387
 
247
388
  const config = await loadConfigFile(configPath, baseDir);
248
- if (configPath.startsWith("bos://")) {
249
- chain.push(configPath);
250
- }
389
+ chain.push(configPath);
251
390
 
252
391
  if (!config.extends) {
253
392
  return config;
254
393
  }
255
394
 
395
+ const extendsRef = resolveExtendsRef(config.extends as string | ExtendsConfig, env);
396
+ if (!extendsRef) {
397
+ return config;
398
+ }
399
+
256
400
  const nextVisited = new Set(visited);
257
401
  nextVisited.add(configPath);
258
- const parentPath = config.extends;
259
- const parentBaseDir = parentPath.startsWith("bos://")
402
+ const parentBaseDir = extendsRef.startsWith("bos://")
260
403
  ? baseDir
261
- : isAbsolute(parentPath)
262
- ? dirname(parentPath)
404
+ : isAbsolute(extendsRef)
405
+ ? dirname(extendsRef)
263
406
  : baseDir;
264
- const parent = await resolveConfigWithExtends(parentPath, parentBaseDir, nextVisited, chain);
407
+ const parent = await resolveConfigWithExtends(extendsRef, parentBaseDir, nextVisited, chain, env);
265
408
 
266
- return mergeConfigs(parent, config);
409
+ return mergeBosConfigWithExtends(parent, config);
267
410
  }
268
411
 
269
- function mergeConfigs(parent: BosConfigInput, child: BosConfigInput): BosConfigInput {
270
- const result = mergeValues(parent, child) as BosConfigInput;
271
- if (child.plugins !== undefined) {
272
- 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 };
273
418
  }
274
- return result;
419
+ return raw;
275
420
  }
276
421
 
277
422
  async function resolveRuntimePlugins(
278
- plugins: Record<string, BosConfigInput>,
423
+ plugins: Record<string, PluginOverrideValue>,
279
424
  baseDir: string,
280
- env: "development" | "production",
281
- prefix: string[] = [],
425
+ env: BosEnv,
282
426
  ): Promise<Record<string, RuntimePluginConfig>> {
283
427
  const out: Record<string, RuntimePluginConfig> = {};
284
428
 
285
- for (const [pluginId, pluginInput] of Object.entries(plugins)) {
286
- const runtimeKey = [...prefix, pluginId].join("/");
287
- const { config: resolvedConfig, baseDir: pluginBaseDir } = await resolveBosConfigInput(
288
- pluginInput,
289
- baseDir,
290
- new Set(),
291
- [],
292
- );
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
+ }
293
447
 
294
- const pluginRuntime = buildRuntimePluginConfig(
295
- runtimeKey,
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
+ }
461
+
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,
296
485
  resolvedConfig,
297
486
  pluginBaseDir,
298
487
  env,
299
- pluginInput,
488
+ normalized,
300
489
  );
301
490
  if (
302
- pluginInput.name &&
303
- typeof pluginInput.name === "string" &&
491
+ normalized.name &&
492
+ typeof normalized.name === "string" &&
304
493
  !pluginRuntime.name.includes("/")
305
494
  ) {
306
- pluginRuntime.name = pluginInput.name;
495
+ pluginRuntime.name = normalized.name;
307
496
  }
308
497
 
309
- const integrity = pluginInput.integrity;
498
+ const integrity = normalized.integrity;
310
499
  if (env === "production" && integrity) {
311
500
  pluginRuntime.integrity = integrity;
312
501
  }
@@ -316,7 +505,7 @@ async function resolveRuntimePlugins(
316
505
  pluginRuntime.url &&
317
506
  !pluginRuntime.localPath &&
318
507
  typeof resolvedConfig.app?.api?.name !== "string" &&
319
- !pluginInput.name
508
+ !normalized.name
320
509
  ) {
321
510
  pluginRuntime.name = await resolveRemotePluginRuntimeName(
322
511
  pluginRuntime.url,
@@ -324,15 +513,7 @@ async function resolveRuntimePlugins(
324
513
  );
325
514
  }
326
515
 
327
- out[runtimeKey] = pluginRuntime;
328
-
329
- if (resolvedConfig.plugins && Object.keys(resolvedConfig.plugins).length > 0) {
330
- const nested = await resolveRuntimePlugins(resolvedConfig.plugins, pluginBaseDir, env, [
331
- ...prefix,
332
- pluginId,
333
- ]);
334
- Object.assign(out, nested);
335
- }
516
+ out[pluginId] = pluginRuntime;
336
517
  }
337
518
 
338
519
  return out;
@@ -340,7 +521,13 @@ async function resolveRuntimePlugins(
340
521
 
341
522
  async function resolveRemotePluginRuntimeName(baseUrl: string, fallback: string): Promise<string> {
342
523
  try {
343
- 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
+
344
531
  if (!response.ok) {
345
532
  return fallback;
346
533
  }
@@ -357,13 +544,38 @@ async function resolveRemotePluginRuntimeName(baseUrl: string, fallback: string)
357
544
  }
358
545
  }
359
546
 
360
- 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(
361
573
  pluginId: string,
362
574
  config: BosConfigInput,
363
575
  baseDir: string,
364
- env: "development" | "production",
365
- source: BosConfigInput,
366
- ): RuntimePluginConfig {
576
+ env: BosEnv,
577
+ source: BosPluginRef,
578
+ ): Promise<RuntimePluginConfig> {
367
579
  const apiConfig = config.app?.api ?? {};
368
580
  const apiDevelopment =
369
581
  typeof apiConfig.development === "string" ? apiConfig.development : undefined;
@@ -372,7 +584,18 @@ function buildRuntimePluginConfig(
372
584
  const sourceProduction = typeof source.production === "string" ? source.production : undefined;
373
585
  const proxy = typeof apiConfig.proxy === "string" ? apiConfig.proxy : undefined;
374
586
  const development = apiDevelopment ?? sourceDevelopment;
375
- 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
+
376
599
  const runtimeTarget =
377
600
  env === "development"
378
601
  ? resolveDevelopmentTarget(development, production, baseDir)
@@ -383,6 +606,25 @@ function buildRuntimePluginConfig(
383
606
  pluginId,
384
607
  );
385
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
+
386
628
  return {
387
629
  name: apiName,
388
630
  url: runtimeTarget.url,
@@ -395,6 +637,24 @@ function buildRuntimePluginConfig(
395
637
  proxy: proxy ?? (typeof source.proxy === "string" ? source.proxy : undefined),
396
638
  variables: normalizeStringRecord(apiConfig.variables ?? source.variables),
397
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,
398
658
  };
399
659
  }
400
660
 
@@ -422,45 +682,6 @@ export function resolvePluginRuntimeName(
422
682
  return fallback;
423
683
  }
424
684
 
425
- async function resolveBosConfigInput(
426
- input: BosConfigInput,
427
- baseDir: string,
428
- visited: Set<string>,
429
- chain: string[],
430
- ): Promise<{ config: BosConfigInput; baseDir: string }> {
431
- if (input.extends) {
432
- const parentBaseDir = input.extends.startsWith("bos://")
433
- ? baseDir
434
- : isAbsolute(input.extends)
435
- ? dirname(input.extends)
436
- : baseDir;
437
- const config = await resolveConfigWithExtends(input.extends, parentBaseDir, visited, chain);
438
- return { config: mergeConfigs(config, input), baseDir: parentBaseDir };
439
- }
440
-
441
- return { config: input, baseDir };
442
- }
443
-
444
- function mergeValues(parent: unknown, child: unknown): unknown {
445
- if (Array.isArray(parent) && Array.isArray(child)) {
446
- return child;
447
- }
448
-
449
- if (isPlainObject(parent) && isPlainObject(child)) {
450
- const merged: Record<string, unknown> = { ...parent };
451
- for (const [key, value] of Object.entries(child)) {
452
- merged[key] = key in merged ? mergeValues(merged[key], value) : value;
453
- }
454
- return merged;
455
- }
456
-
457
- return child ?? parent;
458
- }
459
-
460
- function isPlainObject(value: unknown): value is Record<string, unknown> {
461
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
462
- }
463
-
464
685
  function normalizeStringRecord(value: unknown): Record<string, string> | undefined {
465
686
  if (!isPlainObject(value)) return undefined;
466
687
  const out: Record<string, string> = {};
@@ -488,7 +709,7 @@ function resolveRuntimeTarget(
488
709
  }
489
710
 
490
711
  if (value.startsWith(LOCAL_PREFIX)) {
491
- const localTarget = value.slice(LOCAL_PREFIX.length).trim();
712
+ const localTarget = value?.slice(LOCAL_PREFIX.length).trim();
492
713
  if (!localTarget) {
493
714
  throw new Error(`Invalid local development target: ${value}`);
494
715
  }
@@ -512,7 +733,9 @@ function resolveRuntimeTarget(
512
733
  };
513
734
  }
514
735
 
515
- export function isLocalDevelopmentTarget(value: string | undefined): boolean {
736
+ export function isLocalDevelopmentTarget(
737
+ value: string | undefined,
738
+ ): value is `${typeof LOCAL_PREFIX}${string}` {
516
739
  return typeof value === "string" && value.startsWith(LOCAL_PREFIX);
517
740
  }
518
741
 
@@ -524,7 +747,7 @@ export function resolveLocalDevelopmentPath(
524
747
  return null;
525
748
  }
526
749
 
527
- const localTarget = value!.slice(LOCAL_PREFIX.length).trim();
750
+ const localTarget = value.slice(LOCAL_PREFIX.length).trim();
528
751
  return localTarget ? resolve(baseDir, localTarget) : null;
529
752
  }
530
753
 
@@ -549,5 +772,6 @@ export function parsePort(url: string): number {
549
772
  }
550
773
  }
551
774
 
775
+ export { BOS_CONFIG_ORDER, rebuildOrderedConfig } from "./merge";
552
776
  export type { BosConfig, RuntimeConfig } from "./types";
553
777
  export { BosConfigSchema } from "./types";