everything-dev 1.16.3 → 1.17.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.
Files changed (62) hide show
  1. package/dist/cli/init.cjs +50 -51
  2. package/dist/cli/init.cjs.map +1 -1
  3. package/dist/cli/init.d.cts.map +1 -1
  4. package/dist/cli/init.d.mts.map +1 -1
  5. package/dist/cli/init.mjs +50 -51
  6. package/dist/cli/init.mjs.map +1 -1
  7. package/dist/cli/sync.cjs +3 -5
  8. package/dist/cli/sync.cjs.map +1 -1
  9. package/dist/cli/sync.mjs +3 -5
  10. package/dist/cli/sync.mjs.map +1 -1
  11. package/dist/cli/upgrade.cjs +149 -2
  12. package/dist/cli/upgrade.cjs.map +1 -1
  13. package/dist/cli/upgrade.mjs +149 -2
  14. package/dist/cli/upgrade.mjs.map +1 -1
  15. package/dist/config.cjs +125 -74
  16. package/dist/config.cjs.map +1 -1
  17. package/dist/config.d.cts +9 -2
  18. package/dist/config.d.cts.map +1 -1
  19. package/dist/config.d.mts +9 -2
  20. package/dist/config.d.mts.map +1 -1
  21. package/dist/config.mjs +126 -76
  22. package/dist/config.mjs.map +1 -1
  23. package/dist/contract.d.cts +34 -10
  24. package/dist/contract.d.cts.map +1 -1
  25. package/dist/contract.d.mts +34 -10
  26. package/dist/contract.d.mts.map +1 -1
  27. package/dist/index.cjs +2 -0
  28. package/dist/index.d.cts +3 -3
  29. package/dist/index.d.mts +3 -3
  30. package/dist/index.mjs +3 -3
  31. package/dist/merge.cjs +1 -0
  32. package/dist/merge.mjs +1 -1
  33. package/dist/plugin.cjs +11 -13
  34. package/dist/plugin.cjs.map +1 -1
  35. package/dist/plugin.d.cts +34 -10
  36. package/dist/plugin.d.mts +34 -10
  37. package/dist/plugin.mjs +11 -13
  38. package/dist/plugin.mjs.map +1 -1
  39. package/dist/sidebar.cjs +6 -14
  40. package/dist/sidebar.cjs.map +1 -1
  41. package/dist/sidebar.d.cts +3 -3
  42. package/dist/sidebar.d.cts.map +1 -1
  43. package/dist/sidebar.d.mts +3 -3
  44. package/dist/sidebar.d.mts.map +1 -1
  45. package/dist/sidebar.mjs +6 -14
  46. package/dist/sidebar.mjs.map +1 -1
  47. package/dist/types.cjs +10 -16
  48. package/dist/types.cjs.map +1 -1
  49. package/dist/types.d.cts +54 -10
  50. package/dist/types.d.cts.map +1 -1
  51. package/dist/types.d.mts +54 -10
  52. package/dist/types.d.mts.map +1 -1
  53. package/dist/types.mjs +10 -17
  54. package/dist/types.mjs.map +1 -1
  55. package/package.json +1 -1
  56. package/src/cli/init.ts +95 -63
  57. package/src/cli/sync.ts +5 -8
  58. package/src/cli/upgrade.ts +209 -2
  59. package/src/config.ts +250 -107
  60. package/src/plugin.ts +22 -16
  61. package/src/sidebar.ts +9 -31
  62. package/src/types.ts +10 -15
package/src/config.ts CHANGED
@@ -3,6 +3,7 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
3
3
  import { fetchBosConfigFromFastKv } from "./fastkv";
4
4
  import {
5
5
  type BosEnv,
6
+ bosConfigMerger,
6
7
  isPlainObject,
7
8
  mergeBosConfigWithExtends,
8
9
  type ResolvedConfigMeta,
@@ -18,7 +19,6 @@ import type {
18
19
  PluginEntryValue,
19
20
  RuntimeConfig,
20
21
  RuntimePluginConfig,
21
- SharedDepConfig,
22
22
  } from "./types";
23
23
  import { BosConfigSchema } from "./types";
24
24
 
@@ -74,6 +74,18 @@ export interface ConfigResult {
74
74
  };
75
75
  }
76
76
 
77
+ export interface ResolvedComposableReference {
78
+ entry: BosPluginRef;
79
+ providerBaseDir: string;
80
+ targetPath: string;
81
+ associatedUi?: Record<string, unknown>;
82
+ }
83
+
84
+ interface ParsedExtendsTarget {
85
+ configPath: string;
86
+ targetPath?: string;
87
+ }
88
+
77
89
  export async function loadConfig(options?: {
78
90
  cwd?: string;
79
91
  path?: string;
@@ -98,7 +110,11 @@ export async function loadConfig(options?: {
98
110
  extendedChain,
99
111
  env,
100
112
  );
101
- const config = BosConfigSchema.parse(parsed);
113
+ const config = await resolveRootComposableEntries(
114
+ BosConfigSchema.parse(parsed),
115
+ baseDir,
116
+ runtimeEnv,
117
+ );
102
118
 
103
119
  cachedConfig = config;
104
120
  projectRoot = baseDir;
@@ -144,6 +160,31 @@ export async function buildRuntimePluginsForConfig(
144
160
  return Object.keys(plugins).length > 0 ? plugins : undefined;
145
161
  }
146
162
 
163
+ async function resolveRootComposableEntries(
164
+ config: BosConfig,
165
+ baseDir: string,
166
+ env: BosEnv,
167
+ ): Promise<BosConfig> {
168
+ const resolvedApi = await resolveComposableReference(
169
+ config.app.api as BosPluginRef,
170
+ baseDir,
171
+ env,
172
+ "app.api",
173
+ );
174
+ const resolvedAuth = config.app.auth
175
+ ? await resolveComposableReference(config.app.auth as BosPluginRef, baseDir, env, "app.auth")
176
+ : undefined;
177
+
178
+ return {
179
+ ...config,
180
+ app: {
181
+ ...config.app,
182
+ api: resolvedApi.entry,
183
+ auth: resolvedAuth?.entry,
184
+ },
185
+ };
186
+ }
187
+
147
188
  export function getResolvedConfigPath(configDir: string): string {
148
189
  return join(configDir, ".bos", RESOLVED_CONFIG_FILENAME);
149
190
  }
@@ -216,6 +257,173 @@ export function readBosConfigForBuild(configDir: string): Record<string, unknown
216
257
  return JSON.parse(readFileSync(bosConfigPath, "utf-8")) as Record<string, unknown>;
217
258
  }
218
259
 
260
+ function parseExtendsTarget(ref: string): ParsedExtendsTarget {
261
+ const hashIndex = ref.indexOf("#");
262
+ if (hashIndex === -1) {
263
+ return { configPath: ref };
264
+ }
265
+
266
+ const configPath = ref.slice(0, hashIndex);
267
+ const targetPath = ref.slice(hashIndex + 1);
268
+ return {
269
+ configPath,
270
+ targetPath: targetPath.length > 0 ? targetPath : undefined,
271
+ };
272
+ }
273
+
274
+ function getConfigBaseDir(configPath: string, baseDir: string): string {
275
+ if (configPath.startsWith("bos://")) return baseDir;
276
+ return dirname(isAbsolute(configPath) ? configPath : resolve(baseDir, configPath));
277
+ }
278
+
279
+ function asComposableEntry(value: unknown): BosPluginRef {
280
+ if (typeof value === "string") {
281
+ return { extends: value };
282
+ }
283
+ if (!isPlainObject(value)) {
284
+ throw new Error(`Expected config entry object, received ${typeof value}`);
285
+ }
286
+ return value as BosPluginRef;
287
+ }
288
+
289
+ function getTargetedEntry(config: BosConfigInput, targetPath: string): BosPluginRef {
290
+ if (targetPath === "app.api") {
291
+ return asComposableEntry(config.app?.api);
292
+ }
293
+
294
+ if (targetPath === "app.auth") {
295
+ return asComposableEntry(config.app?.auth);
296
+ }
297
+
298
+ if (targetPath.startsWith("plugins.")) {
299
+ const pluginId = targetPath.slice("plugins.".length);
300
+ if (pluginId.length === 0) {
301
+ throw new Error(`Invalid plugin target path: ${targetPath}`);
302
+ }
303
+ return asComposableEntry(config.plugins?.[pluginId]);
304
+ }
305
+
306
+ throw new Error(`Unsupported extends target path: ${targetPath}`);
307
+ }
308
+
309
+ function getAssociatedUi(
310
+ config: BosConfigInput,
311
+ _targetPath: string,
312
+ ): Record<string, unknown> | undefined {
313
+ return isPlainObject(config.app?.ui) ? (config.app.ui as Record<string, unknown>) : undefined;
314
+ }
315
+
316
+ function mergeComposableEntries(
317
+ parent: Partial<BosPluginRef>,
318
+ child: Partial<BosPluginRef>,
319
+ ): BosPluginRef {
320
+ return bosConfigMerger({ ...child }, parent) as BosPluginRef;
321
+ }
322
+
323
+ function stripUnsafeLocalDevelopment<T extends Record<string, unknown> | undefined>(
324
+ entry: T,
325
+ allowLocalPaths: boolean,
326
+ ): T {
327
+ if (!entry || allowLocalPaths) {
328
+ return entry;
329
+ }
330
+
331
+ if (typeof entry.development === "string" && entry.development.startsWith(LOCAL_PREFIX)) {
332
+ const { development: _ignored, ...rest } = entry;
333
+ return rest as T;
334
+ }
335
+
336
+ return entry;
337
+ }
338
+
339
+ export async function resolveComposableReference(
340
+ source: BosPluginRef,
341
+ baseDir: string,
342
+ env: BosEnv,
343
+ defaultTargetPath: string,
344
+ ): Promise<ResolvedComposableReference> {
345
+ let resolvedEntry: BosPluginRef = {};
346
+ let providerBaseDir = baseDir;
347
+ let targetPath = defaultTargetPath;
348
+ let associatedUi: Record<string, unknown> | undefined;
349
+ let allowLocalPaths = false;
350
+ let extendsError: unknown;
351
+
352
+ const extendsRef = source.extends ? resolveExtendsRef(source.extends, env) : undefined;
353
+ if (extendsRef) {
354
+ const parsed = parseExtendsTarget(extendsRef);
355
+ targetPath = parsed.targetPath ?? defaultTargetPath;
356
+ const extendsBaseDir = getConfigBaseDir(parsed.configPath, baseDir);
357
+ try {
358
+ const extendedConfig = await resolveConfigWithExtends(
359
+ parsed.configPath,
360
+ extendsBaseDir,
361
+ new Set(),
362
+ [],
363
+ env,
364
+ );
365
+ resolvedEntry = mergeComposableEntries(
366
+ resolvedEntry,
367
+ getTargetedEntry(extendedConfig, targetPath),
368
+ );
369
+ providerBaseDir = extendsBaseDir;
370
+ associatedUi = getAssociatedUi(extendedConfig, targetPath);
371
+ } catch (error) {
372
+ extendsError = error;
373
+ }
374
+ }
375
+
376
+ const localDevelopment =
377
+ typeof source.development === "string" && source.development.startsWith(LOCAL_PREFIX)
378
+ ? source.development
379
+ : undefined;
380
+
381
+ if (localDevelopment) {
382
+ const localPath = resolve(baseDir, localDevelopment.slice(LOCAL_PREFIX.length).trim());
383
+ const localConfigPath = join(localPath, "bos.config.json");
384
+ if (existsSync(localConfigPath)) {
385
+ const localConfig = await resolveConfigWithExtends(
386
+ localConfigPath,
387
+ localPath,
388
+ new Set(),
389
+ [],
390
+ env,
391
+ );
392
+ resolvedEntry = mergeComposableEntries(
393
+ resolvedEntry,
394
+ getTargetedEntry(localConfig, targetPath),
395
+ );
396
+ providerBaseDir = localPath;
397
+ associatedUi = getAssociatedUi(localConfig, targetPath);
398
+ allowLocalPaths = true;
399
+ }
400
+ }
401
+
402
+ const sourceOverrides = { ...source };
403
+ if (allowLocalPaths && localDevelopment) {
404
+ delete sourceOverrides.development;
405
+ }
406
+
407
+ resolvedEntry = mergeComposableEntries(resolvedEntry, sourceOverrides);
408
+
409
+ if (
410
+ extendsError &&
411
+ !allowLocalPaths &&
412
+ typeof resolvedEntry.development !== "string" &&
413
+ typeof resolvedEntry.production !== "string" &&
414
+ typeof resolvedEntry.name !== "string"
415
+ ) {
416
+ throw extendsError;
417
+ }
418
+
419
+ return {
420
+ entry: stripUnsafeLocalDevelopment(resolvedEntry, allowLocalPaths || Boolean(localDevelopment)),
421
+ providerBaseDir,
422
+ targetPath,
423
+ associatedUi: stripUnsafeLocalDevelopment(associatedUi, allowLocalPaths),
424
+ };
425
+ }
426
+
219
427
  function resolveDevelopmentTarget(
220
428
  development: string | undefined,
221
429
  production: string | undefined,
@@ -225,6 +433,9 @@ function resolveDevelopmentTarget(
225
433
  if (forceSource === "remote") {
226
434
  return resolveRuntimeTarget(production, baseDir, "remote");
227
435
  }
436
+ if (!development) {
437
+ return resolveRuntimeTarget(production, baseDir, "remote");
438
+ }
228
439
  const devTarget = resolveRuntimeTarget(development, baseDir);
229
440
  if (devTarget.source === "local" && (!devTarget.localPath || !existsSync(devTarget.localPath))) {
230
441
  return resolveRuntimeTarget(production, baseDir, "remote");
@@ -298,6 +509,7 @@ export function buildRuntimeConfig(
298
509
  const hostIsRemote = hostRuntime.source === "remote";
299
510
  const uiIsRemote = uiRuntime.source === "remote";
300
511
  const apiIsRemote = apiRuntime.source === "remote";
512
+ const resolvedApiName = resolvePluginRuntimeName(apiConfig.name, apiRuntime.localPath, "api");
301
513
 
302
514
  return {
303
515
  env,
@@ -331,7 +543,7 @@ export function buildRuntimeConfig(
331
543
  source: uiRuntime.source,
332
544
  },
333
545
  api: {
334
- name: apiConfig.name,
546
+ name: resolvedApiName,
335
547
  url: apiRuntime.url,
336
548
  entry: apiRuntime.url ? `${apiRuntime.url}/mf-manifest.json` : "/mf-manifest.json",
337
549
  localPath: apiRuntime.localPath,
@@ -345,7 +557,7 @@ export function buildRuntimeConfig(
345
557
  auth: (() => {
346
558
  if (!authConfig || !authRuntime) return undefined;
347
559
  return {
348
- name: resolvePluginRuntimeName(undefined, authRuntime.localPath, authConfig.name),
560
+ name: resolvePluginRuntimeName(authConfig.name, authRuntime.localPath, "auth"),
349
561
  url: authRuntime.url,
350
562
  entry: authRuntime.url ? `${authRuntime.url}/mf-manifest.json` : "/mf-manifest.json",
351
563
  localPath: authRuntime.localPath,
@@ -399,14 +611,18 @@ async function resolveConfigWithExtends(
399
611
  return config;
400
612
  }
401
613
 
614
+ const parsedParentRef = parseExtendsTarget(extendsRef);
615
+
402
616
  const nextVisited = new Set(visited);
403
617
  nextVisited.add(configPath);
404
- const parentBaseDir = extendsRef.startsWith("bos://")
405
- ? baseDir
406
- : isAbsolute(extendsRef)
407
- ? dirname(extendsRef)
408
- : baseDir;
409
- const parent = await resolveConfigWithExtends(extendsRef, parentBaseDir, nextVisited, chain, env);
618
+ const parentBaseDir = getConfigBaseDir(parsedParentRef.configPath, baseDir);
619
+ const parent = await resolveConfigWithExtends(
620
+ parsedParentRef.configPath,
621
+ parentBaseDir,
622
+ nextVisited,
623
+ chain,
624
+ env,
625
+ );
410
626
 
411
627
  return mergeBosConfigWithExtends(parent, config);
412
628
  }
@@ -432,82 +648,20 @@ async function resolveRuntimePlugins(
432
648
  const normalized = normalizePluginEntry(rawInput);
433
649
  if (normalized === null || normalized === false) continue;
434
650
 
435
- let resolvedConfig: BosConfigInput = {};
436
- let pluginBaseDir = baseDir;
437
-
438
- if (normalized.extends) {
439
- try {
440
- const extendsUrl = resolveExtendsRef(normalized.extends, env);
441
- if (extendsUrl) {
442
- const remoteConfig = await fetchBosConfigFromFastKv<BosConfigInput>(extendsUrl);
443
- resolvedConfig = remoteConfig;
444
- }
445
- } catch {
446
- resolvedConfig = {};
447
- }
448
- }
449
-
450
- if (normalized.development?.startsWith(LOCAL_PREFIX)) {
451
- const localPath = resolve(baseDir, normalized.development.slice(LOCAL_PREFIX.length).trim());
452
- if (existsSync(localPath)) {
453
- const localConfigPath = join(localPath, "bos.config.json");
454
- if (existsSync(localConfigPath)) {
455
- try {
456
- const localRaw = JSON.parse(readFileSync(localConfigPath, "utf-8")) as BosConfigInput;
457
- resolvedConfig = mergeBosConfigWithExtends(resolvedConfig, localRaw);
458
- pluginBaseDir = localPath;
459
- } catch {}
460
- }
461
- }
462
- }
463
-
464
- if (normalized.app && isPlainObject(normalized.app)) {
465
- const mergedApp: Record<string, unknown> = {
466
- ...((resolvedConfig.app as Record<string, unknown>) ?? {}),
467
- ...(normalized.app as Record<string, unknown>),
468
- };
469
- resolvedConfig = { ...resolvedConfig, app: mergedApp as BosConfigInput["app"] };
470
- }
471
- if (normalized.shared && isPlainObject(normalized.shared)) {
472
- const mergedShared: Record<string, Record<string, SharedDepConfig>> = {
473
- ...(resolvedConfig.shared ?? {}),
474
- ...(normalized.shared as Record<string, Record<string, SharedDepConfig>>),
475
- };
476
- resolvedConfig = { ...resolvedConfig, shared: mergedShared };
477
- }
478
- if (normalized.sidebar) {
479
- resolvedConfig = { ...resolvedConfig, sidebar: normalized.sidebar };
480
- }
481
- if (normalized.routes) {
482
- resolvedConfig = { ...resolvedConfig, routes: normalized.routes };
483
- }
484
-
485
- const pluginRuntime = await buildRuntimePluginConfig(
486
- pluginId,
487
- resolvedConfig,
488
- pluginBaseDir,
489
- env,
651
+ const resolvedReference = await resolveComposableReference(
490
652
  normalized,
653
+ baseDir,
654
+ env,
655
+ `plugins.${pluginId}`,
491
656
  );
492
- if (
493
- normalized.name &&
494
- typeof normalized.name === "string" &&
495
- !pluginRuntime.name.includes("/")
496
- ) {
497
- pluginRuntime.name = normalized.name;
498
- }
499
657
 
500
- const integrity = normalized.integrity;
501
- if (env === "production" && integrity) {
502
- pluginRuntime.integrity = integrity;
503
- }
658
+ const pluginRuntime = buildRuntimePluginConfig(pluginId, env, resolvedReference);
504
659
 
505
660
  if (
506
661
  pluginRuntime.source === "remote" &&
507
662
  pluginRuntime.url &&
508
663
  !pluginRuntime.localPath &&
509
- typeof resolvedConfig.app?.api?.name !== "string" &&
510
- !normalized.name
664
+ typeof resolvedReference.entry.name !== "string"
511
665
  ) {
512
666
  pluginRuntime.name = await resolveRemotePluginRuntimeName(
513
667
  pluginRuntime.url,
@@ -546,22 +700,14 @@ async function resolveRemotePluginRuntimeName(baseUrl: string, fallback: string)
546
700
  }
547
701
  }
548
702
 
549
- async function buildRuntimePluginConfig(
703
+ function buildRuntimePluginConfig(
550
704
  pluginId: string,
551
- config: BosConfigInput,
552
- baseDir: string,
553
705
  env: BosEnv,
554
- source: BosPluginRef,
555
- ): Promise<RuntimePluginConfig> {
556
- const apiConfig = config.app?.api ?? {};
557
- const apiDevelopment =
558
- typeof apiConfig.development === "string" ? apiConfig.development : undefined;
559
- const apiProduction = typeof apiConfig.production === "string" ? apiConfig.production : undefined;
560
- const sourceDevelopment = typeof source.development === "string" ? source.development : undefined;
561
- const sourceProduction = typeof source.production === "string" ? source.production : undefined;
562
- const proxy = typeof apiConfig.proxy === "string" ? apiConfig.proxy : undefined;
563
- const development = apiDevelopment ?? sourceDevelopment;
564
- const production = apiProduction ?? sourceProduction;
706
+ resolved: ResolvedComposableReference,
707
+ ): RuntimePluginConfig {
708
+ const source = resolved.entry;
709
+ const development = typeof source.development === "string" ? source.development : undefined;
710
+ const production = typeof source.production === "string" ? source.production : undefined;
565
711
 
566
712
  if (production?.startsWith("bos://")) {
567
713
  throw new Error(
@@ -571,32 +717,28 @@ async function buildRuntimePluginConfig(
571
717
 
572
718
  const runtimeTarget =
573
719
  env === "development"
574
- ? resolveDevelopmentTarget(development, production, baseDir)
575
- : resolveRuntimeTarget(production, baseDir, "remote");
576
- const apiName = resolvePluginRuntimeName(
577
- typeof apiConfig.name === "string" ? apiConfig.name : undefined,
578
- runtimeTarget.localPath,
579
- pluginId,
580
- );
720
+ ? resolveDevelopmentTarget(development, production, resolved.providerBaseDir)
721
+ : resolveRuntimeTarget(production, resolved.providerBaseDir, "remote");
722
+ const apiName = resolvePluginRuntimeName(source.name, runtimeTarget.localPath, pluginId);
581
723
 
582
- const uiConfig = config.app?.ui;
724
+ const uiConfig = resolved.associatedUi;
583
725
  const uiDevelopment =
584
726
  typeof uiConfig?.development === "string" ? uiConfig.development : undefined;
585
727
  const uiProduction = typeof uiConfig?.production === "string" ? uiConfig.production : undefined;
586
728
  const uiRuntime =
587
729
  uiConfig && (uiDevelopment || uiProduction)
588
730
  ? env === "development"
589
- ? resolveDevelopmentTarget(uiDevelopment, uiProduction, baseDir)
590
- : resolveRuntimeTarget(uiProduction, baseDir, "remote")
731
+ ? resolveDevelopmentTarget(uiDevelopment, uiProduction, resolved.providerBaseDir)
732
+ : resolveRuntimeTarget(uiProduction, resolved.providerBaseDir, "remote")
591
733
  : undefined;
592
734
 
593
- const sidebar = (config.sidebar ?? source.sidebar)?.map((item) => ({
735
+ const sidebar = source.sidebar?.map((item) => ({
594
736
  ...item,
595
737
  to: item.to ?? `/${pluginId}`,
596
738
  roleRequired: item.roleRequired ?? ("member" as const),
597
739
  }));
598
740
 
599
- const routes = config.routes ?? source.routes;
741
+ const routes = source.routes;
600
742
 
601
743
  return {
602
744
  name: apiName,
@@ -607,9 +749,10 @@ async function buildRuntimePluginConfig(
607
749
  source: runtimeTarget.source,
608
750
  localPath: runtimeTarget.localPath,
609
751
  port: runtimeTarget.port,
610
- proxy: proxy ?? (typeof source.proxy === "string" ? source.proxy : undefined),
611
- variables: normalizeStringRecord(apiConfig.variables ?? source.variables),
612
- secrets: normalizeStringArray(apiConfig.secrets ?? source.secrets),
752
+ proxy: typeof source.proxy === "string" ? source.proxy : undefined,
753
+ variables: normalizeStringRecord(source.variables),
754
+ secrets: normalizeStringArray(source.secrets),
755
+ integrity: runtimeTarget.source === "remote" ? source.integrity : undefined,
613
756
  ui: uiRuntime
614
757
  ? {
615
758
  name: typeof uiConfig?.name === "string" ? uiConfig.name : `${apiName}-ui`,
package/src/plugin.ts CHANGED
@@ -236,8 +236,6 @@ async function generateCodeArtifacts(
236
236
  runtimeConfig?: RuntimeConfig;
237
237
  },
238
238
  ): Promise<GeneratedArtifacts | null> {
239
- writePluginSidebarGen(configDir, config);
240
-
241
239
  if (opts?.env) {
242
240
  writeResolvedConfig(configDir, config, opts.env, opts.extendsChain);
243
241
  }
@@ -245,6 +243,8 @@ async function generateCodeArtifacts(
245
243
  const runtimeConfig = opts?.runtimeConfig ?? (await loadConfig({ cwd: configDir }))?.runtime;
246
244
  if (!runtimeConfig) return null;
247
245
 
246
+ writePluginSidebarGen(configDir, runtimeConfig);
247
+
248
248
  const bridge = await syncApiContractBridge({
249
249
  configDir,
250
250
  runtimeConfig,
@@ -653,18 +653,22 @@ export default createPlugin({
653
653
  string,
654
654
  unknown
655
655
  >;
656
- if (!pluginConfig.app) pluginConfig.app = {};
657
- const app = pluginConfig.app as Record<string, unknown>;
658
- if (!app.api) app.api = {};
659
- const api = app.api as Record<string, unknown>;
660
- api.production = publishedUrl;
656
+ if (!pluginConfig.plugins || typeof pluginConfig.plugins !== "object") {
657
+ pluginConfig.plugins = {};
658
+ }
659
+ const plugins = pluginConfig.plugins as Record<string, unknown>;
660
+ if (!plugins[input.key] || typeof plugins[input.key] !== "object") {
661
+ plugins[input.key] = {};
662
+ }
663
+ const entry = plugins[input.key] as Record<string, unknown>;
664
+ entry.production = publishedUrl;
661
665
  if (integrity) {
662
- api.integrity = integrity;
666
+ entry.integrity = integrity;
663
667
  } else {
664
- delete api.integrity;
668
+ delete entry.integrity;
665
669
  }
666
670
  writeFileSync(pluginConfigPath, `${JSON.stringify(pluginConfig, null, 2)}\n`);
667
- console.log(` ✅ Updated ${pluginConfigPath}: app.api.production`);
671
+ console.log(` ✅ Updated ${pluginConfigPath}: plugins.${input.key}.production`);
668
672
  } catch (err) {
669
673
  console.error(
670
674
  ` ❌ Failed to update plugin bos.config.json:`,
@@ -1382,12 +1386,14 @@ export default createPlugin({
1382
1386
  }
1383
1387
 
1384
1388
  const pluginRoutes: Record<string, string[]> = {};
1385
- if (parentConfig.plugins) {
1386
- for (const [key, entry] of Object.entries(parentConfig.plugins)) {
1387
- const entryRef = getPluginRef(entry);
1388
- if (entryRef?.routes && entryRef.routes.length > 0) {
1389
- pluginRoutes[key] = entryRef.routes;
1390
- }
1389
+ const parentRuntimePlugins = await buildRuntimePluginsForConfig(
1390
+ parentConfig as BosConfig,
1391
+ sourceDir,
1392
+ "production",
1393
+ );
1394
+ for (const [key, plugin] of Object.entries(parentRuntimePlugins ?? {})) {
1395
+ if (plugin.routes && plugin.routes.length > 0) {
1396
+ pluginRoutes[key] = plugin.routes;
1391
1397
  }
1392
1398
  }
1393
1399
 
package/src/sidebar.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import type { BosConfig, SidebarItem } from "./types";
3
+ import type { RuntimeConfig, SidebarItem } from "./types";
4
4
 
5
5
  const ICON_IMPORTS: Record<string, string> = {
6
6
  Home: "lucide-react",
@@ -51,11 +51,11 @@ function collectIconImports(items: SidebarItem[]): Map<string, Set<string>> {
51
51
  return moduleMap;
52
52
  }
53
53
 
54
- export function generatePluginSidebarContent(config: BosConfig, configDir?: string): string {
54
+ export function generatePluginSidebarContent(runtimeConfig: RuntimeConfig): string {
55
55
  const coreItems: SidebarItem[] = [{ icon: "Home", label: "home", to: "/", roleRequired: "anon" }];
56
56
 
57
- if (config.app.auth?.sidebar) {
58
- for (const item of config.app.auth.sidebar) {
57
+ if (runtimeConfig.auth?.sidebar) {
58
+ for (const item of runtimeConfig.auth.sidebar) {
59
59
  coreItems.push({
60
60
  ...item,
61
61
  to: item.to ?? "/auth",
@@ -65,31 +65,9 @@ export function generatePluginSidebarContent(config: BosConfig, configDir?: stri
65
65
  }
66
66
 
67
67
  const pluginItems: SidebarItem[] = [];
68
- if (config.plugins) {
69
- for (const [key, entry] of Object.entries(config.plugins)) {
70
- let sidebar: SidebarItem[] | undefined;
71
-
72
- if (typeof entry === "object" && entry.sidebar) {
73
- sidebar = entry.sidebar;
74
- } else if (
75
- typeof entry === "object" &&
76
- entry.development?.startsWith("local:") &&
77
- configDir
78
- ) {
79
- const localPath = join(configDir, entry.development.slice("local:".length).trim());
80
- const pluginConfigPath = join(localPath, "bos.config.json");
81
- if (existsSync(pluginConfigPath)) {
82
- try {
83
- const pluginConfig = JSON.parse(readFileSync(pluginConfigPath, "utf-8")) as {
84
- sidebar?: SidebarItem[];
85
- };
86
- if (pluginConfig.sidebar) {
87
- sidebar = pluginConfig.sidebar;
88
- }
89
- } catch {}
90
- }
91
- }
92
-
68
+ if (runtimeConfig.plugins) {
69
+ for (const [key, entry] of Object.entries(runtimeConfig.plugins)) {
70
+ const sidebar = entry.sidebar;
93
71
  if (!sidebar) continue;
94
72
  for (const item of sidebar) {
95
73
  pluginItems.push({
@@ -135,10 +113,10 @@ ${itemsCode}
135
113
  `;
136
114
  }
137
115
 
138
- export function writePluginSidebarGen(configDir: string, config: BosConfig): string {
116
+ export function writePluginSidebarGen(configDir: string, runtimeConfig: RuntimeConfig): string {
139
117
  const outputPath = join(configDir, "ui/src/lib/plugin-sidebar.gen.ts");
140
118
 
141
- const content = generatePluginSidebarContent(config, configDir);
119
+ const content = generatePluginSidebarContent(runtimeConfig);
142
120
 
143
121
  const outputDir = dirname(outputPath);
144
122
  if (!existsSync(outputDir)) {
package/src/types.ts CHANGED
@@ -46,8 +46,9 @@ export const SidebarItemSchema = z.object({
46
46
  });
47
47
  export type SidebarItem = z.infer<typeof SidebarItemSchema>;
48
48
 
49
- export const ApiPluginConfigSchema = z.object({
50
- name: z.string(),
49
+ export const ComposableAppEntrySchema = z.object({
50
+ extends: ExtendsSchema.optional(),
51
+ name: z.string().optional(),
51
52
  development: z.string().optional(),
52
53
  production: z.string().optional(),
53
54
  integrity: z.string().optional(),
@@ -55,7 +56,11 @@ export const ApiPluginConfigSchema = z.object({
55
56
  variables: z.record(z.string(), z.string()).optional(),
56
57
  secrets: z.array(z.string()).optional(),
57
58
  sidebar: z.array(SidebarItemSchema).optional(),
59
+ routes: z.array(z.string()).optional(),
58
60
  });
61
+ export type ComposableAppEntry = z.infer<typeof ComposableAppEntrySchema>;
62
+
63
+ export const ApiPluginConfigSchema = ComposableAppEntrySchema;
59
64
  export type ApiPluginConfig = z.infer<typeof ApiPluginConfigSchema>;
60
65
 
61
66
  export const PluginUiConfigSchema = z.object({
@@ -66,18 +71,8 @@ export const PluginUiConfigSchema = z.object({
66
71
  });
67
72
  export type PluginUiConfig = z.infer<typeof PluginUiConfigSchema>;
68
73
 
69
- export const BosPluginRefSchema = z.object({
70
- extends: ExtendsSchema.optional(),
71
- development: z.string().optional(),
72
- production: z.string().optional(),
73
- integrity: z.string().optional(),
74
- name: z.string().optional(),
74
+ export const BosPluginRefSchema = ComposableAppEntrySchema.extend({
75
75
  version: z.string().optional(),
76
- proxy: z.string().optional(),
77
- variables: z.record(z.string(), z.string()).optional(),
78
- secrets: z.array(z.string()).optional(),
79
- routes: z.array(z.string()).optional(),
80
- sidebar: z.array(SidebarItemSchema).optional(),
81
76
  app: z.record(z.string(), z.unknown()).optional(),
82
77
  shared: z.record(z.string(), z.record(z.string(), SharedConfigSchema)).optional(),
83
78
  plugins: z.record(z.string(), z.unknown()).optional(),
@@ -222,8 +217,8 @@ export const BosConfigSchema = z.object({
222
217
  app: z.object({
223
218
  host: HostConfigSchema,
224
219
  ui: UiConfigSchema,
225
- api: ApiPluginConfigSchema,
226
- auth: ApiPluginConfigSchema.optional(),
220
+ api: ComposableAppEntrySchema,
221
+ auth: ComposableAppEntrySchema.optional(),
227
222
  }),
228
223
  });
229
224
  export type BosConfig = z.infer<typeof BosConfigSchema>;