Skip to content

feat: add composeStacks() for declarative multi-stack composition#721

Merged
hotlong merged 3 commits into
mainfrom
copilot/support-compose-stacks-feature
Feb 18, 2026
Merged

feat: add composeStacks() for declarative multi-stack composition#721
hotlong merged 3 commits into
mainfrom
copilot/support-compose-stacks-feature

Conversation

Copilot AI commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

Combining multiple stacks (CRM + Todo + BI) requires manually spread-merging every array field (objects, apps, dashboards, reports, pages, data, etc.), which is verbose, error-prone, and scales poorly toward marketplace scenarios.

New API: composeStacks(stacks, options?)

  • Array fields (apps, views, dashboards, reports, pages, data, plugins, etc.) — concatenated in order
  • Objects — configurable conflict resolution via objectConflict:
    • 'error' (default) — throws on duplicate names
    • 'override' — last-wins replacement
    • 'merge' — shallow-merge fields from later stacks
  • Manifest'first' / 'last' (default) / numeric index selection
  • i18n — last-wins for single-value config
  • namespace option reserved as Phase 2 extension point for marketplace isolation

Schemas

  • ConflictStrategySchemaz.enum(['error', 'override', 'merge'])
  • ComposeStacksOptionsSchema — typed options with defaults

Usage

import { composeStacks, defineStack } from '@objectstack/spec';

const crm = defineStack({ objects: [...], apps: [...], dashboards: [...] });
const todo = defineStack({ objects: [...], apps: [...], data: [...] });

// Replaces all manual ...spread merging
const combined = composeStacks([crm, todo], {
  objectConflict: 'merge',
  manifest: 'first',
});

37 new tests covering all strategies, edge cases, and defineStack round-trip validation.

Original prompt

This section details on the original issue you should resolve

<issue_title>[Feature] 支持多应用 stack 合并原语(composeStacks):解决多配置手工合并痛点</issue_title>
<issue_description>## 背景

当前 objectui(和 ObjectStack 在多个项目场景)的应用汇总配置存在严重的手工合并痛点:

  • 当引入多个应用(如 CRM、Todo、Kitchen Sink),必须分别读出其配置,再用 ...spread 手工合并多个字段:objects、apps、dashboards、reports、pages、manifest.data 等。
  • 每加一个配置项都需要代码层反复 spread 合并,每个地方都重复一遍,容易遗漏且难维护。
  • 如果多个对象同名(如 CRM 和 Kitchen Sink 都有 account),不得不额外实现 object 去重/优先级策略,或硬合成一个大 AppPlugin。

根因分析

  1. defineStack 只支持单一 stack,不支持声明式组合多个 stack,没有 composeStacks/mergeStacks 原语。
  2. Runtime 的 Schema Registry 不支持多 owner 的同名 object,导致无法直接 plugin 多个 stack,只能先合并。
  3. views 配置在 stack 顶层,而 runtime 协议期望 views 挂在 object 下,需要自己写 bridge 代码做合并。

希望的能力

1. 声明式 stack 组合

  • 引入一个原生的 composeStacks(...stacks, options) 或类似 API,支持将多个 defineStack 的输出合并为一个大 stack。
  • options 支持 object 派生策略、manifest 合并、字段 override、同名 object merge/优先级等。

2. 多 AppPlugin/Plugin 机制

  • 允许 runtime 多 AppPlugin 共存,即便出现同名 object(通过 namespace/owner/优先级等机制)
  • 支持 plugin 隔离,不同 App 的 metadata/seed 数据不会冲突

3. 自动 bridge views → objects

  • spec 层自动将 views[].listViews 合并到 objects 的 listViews 字段,无需手工 bridge

4. 合并策略内置

  • 内置 object 去重/优先级策略,支持 seed 数据优先、plugin metadata优先等
  • 可选 deep-merge/override/error 三种同名对象处理

���状代码痛点举例

  1. apps/console/objectstack.shared.ts
objects: mergeViewsIntoObjects(
  mergeObjects([
    ...(crmConfig.objects || []),
    ...(todoConfig.objects || []),
    ...(kitchenSinkConfig.objects || []),
  ], hotcrmObjects),
  allConfigs,
),
apps: [
  ...crmApps,
  ...(todoConfig.apps || []),
  ...(kitchenSinkConfig.apps || []),
  ...hotcrmApps,
],
// dashboards, reports, pages, manifest.data等全部 spread 合并
  1. objectstack.config.ts
apps: [
  ...(crm.apps || []),
  ...(todo.apps || []),
  ...(kitchenSink.apps || []),
],
objects: baseObjects,
// pages, dashboards, reports, manifest.data 等 spread 合并
  1. 组合多个 plugin 时因同名 object 只能选单一AppPlugin,错失插件隔离能力

建议

  • 在 @objectstack/spec 层尽早增加 compose/merge 支持,不再依赖手工 spread
  • 支持 runtime 多 AppPlugin 注册,解决 schema registry owner 冲突问题
  • 自动 views → objects bridge,提升协议层一致性

如能推进此 feature,将极大简化多应用架构内容、提升维护效率,减少手工合并和错配风险。</issue_description>

Comments on the Issue (you are @copilot in this section)

@hotlong ## 补充:应用市场(Marketplace)场景的延伸需求

上面提出的 composeStacks 解决的是编译期静态合并的最小可行问题。但如果 roadmap 里有应用市场(Marketplace),即支持从不同厂商动态安装应用,问题会更加严峻——本质是同一类架构缺陷在更复杂场景下的放大。


当前场景 vs 应用市场场景对比

维度 当前(monorepo 内部合并) 未来(应用市场安装)
应用来源 自己团队写的几个 example 不同厂商、不同版本、动态安装
合并时机 编译期,开发者手写 spread 运行时,用户点击"安装"后动态加载
冲突可控性 开发者知道有哪些冲突,可以手工处理 完全不可控,任何厂商都可能定义 accountcontact 等通用对象名
数量 3-5 个配置 几十甚至上百个第三方应用
卸载需求 不需要 必须支持,卸载后要干净移除

应用市场会额外暴露的 5 个问题

1. 运行时动态组合——不能再靠编译期 spread

应用市场意味着用户安装/卸载应用是在运行时发生的:

用户安装 "财务报表" 应用 → 运行时动态注入 objects/reports/dashboards → 必须热合并到当前 stack
用户卸载 "财务报表" 应用 → 运行时动态移除 → 必须干净回退

需要 spec/runtime 层有 installApp() / uninstallApp()registerStack() / unregisterStack() 这样的运行时协议,而不仅仅是编译期的 composeStacks()

2. 对象命名冲突会爆炸——需要 Namespace/Scope 隔离

现在只是 CRM 和 Kitchen Sink 都有 account 就需要手工去重。应用市场场景下:

  • 厂商 A 的 CRM 定义了 accountcontactopportunity
  • 厂商 B 的 ERP 也定义了 accountorderinvoice
  • 厂商 C 的 HR 系统也定义了 employee(可能和某个 contact 有 overlap)

没有 namespace/scope/owner 隔离机制,多厂商根本无法共存。需要类似 @vendor/crm:account 的命名空间支持,或 Schema Registry 支持多 owner 的同名 object。

3. 依赖声明与版本兼容

应用市场的应用之间可能有依赖关系:

"销售报表" 应用依赖 "CRM 基础" 应用的 opportunity 对象
"财务对账" 应用依赖 "ERP 核心" 应用的 account 对象

需要 spec 层支持:

  • 依赖声明:应用 A 声明依赖应用 B 的某个 object
  • 版本约束requires: @vendor/crm-base >= 2.0.0
  • 安装校验:安装时校验依赖是否满足,卸载时校验是否有其他应用依赖

4. 权限边界与数据 Ownership

不同厂商的应用共享同一个 account 对象时:

  • 厂商 A 能否读取厂商 B 写入的 fields?
  • 一个应用卸载后,它新增的 fields/数据怎么处理?
  • 应用的 seed data 需要 ownership 标记,卸载时可回收

5. Schema 演化与迁移

应用市场应用会升级版本:

  • v1 的 report 用了 columns,v2 改成 fields —— 如何自动迁移?
  • 某厂商的应用更新了 object schema,用户已有数据如何兼容?
  • 需要 schema migration protocol

对本 Feature 设计的建议

本 issue 的 composeStacks 是解决当前痛点的 Phase 1,但设计时建议同步考虑可扩展性

Phase 1(本 issue) Phase 2+(应用市场)
composeStacks() 编译期静态合并 运行时 registerStack() / unregisterStack()
同名 object merge/override/error 策略 Namespace/Scope 完整隔离 + 多 owner 支持
views → objects 自动 bridge 跨应用依赖声明和版本约束
权限边界、数据 ownership、卸载回收
Schema 版本迁移协议

建议 composeStacks 的 API 设计预留扩展点(如 options 中的 namespace、ownership、conflict strategy),使其能够平滑演进到运行时动态组合场景,避免后续 breaking change。</comment_new>


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel

vercel Bot commented Feb 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Feb 18, 2026 8:33am
spec Ready Ready Preview, Comment Feb 18, 2026 8:33am

Request Review

Implements the core composeStacks() function that eliminates manual
spread-merging when combining multiple stack definitions (CRM + Todo + BI).

- ConflictStrategySchema: 'error' | 'override' | 'merge'
- ComposeStacksOptionsSchema: objectConflict, manifest strategy, namespace
- Array fields (apps, views, dashboards, etc.) are concatenated
- Objects support configurable conflict resolution
- Manifest selection via 'first', 'last', or index
- Exported from package root for convenience
- 37 new tests covering all strategies and edge cases

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add support for multi-application stack merging feat: add composeStacks() for declarative multi-stack composition Feb 18, 2026
Copilot AI requested a review from hotlong February 18, 2026 08:27
@hotlong
hotlong marked this pull request as ready for review February 18, 2026 08:55
Copilot AI review requested due to automatic review settings February 18, 2026 08:55
@hotlong
hotlong merged commit f9c3406 into main Feb 18, 2026
5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new composeStacks() helper to @objectstack/spec to declaratively combine multiple defineStack() outputs into a single ObjectStackDefinition, reducing manual spread-merging across stack fields.

Changes:

  • Introduces composeStacks(stacks, options?) with conflict strategy + manifest selection options and supporting schemas/types.
  • Exports the new API/schemas/types from the package root.
  • Adds a dedicated Vitest suite for composeStacks and updates the roadmap checklist.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
packages/spec/src/stack.zod.ts Adds composeStacks() implementation, conflict/option schemas, and merge helpers.
packages/spec/src/index.ts Re-exports composeStacks and related schemas/types from the root entrypoint.
packages/spec/src/compose-stacks.test.ts Adds tests for schema defaults, merge strategies, manifest selection, and integration with defineStack.
ROADMAP.md Marks “Stack Composition” as completed.

Comment on lines +439 to +440
* Optional namespace prefix (reserved for Phase 2 — Marketplace isolation).
* When set, object names from this composition are prefixed for isolation.

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namespace is exposed in ComposeStacksOptionsSchema and its JSDoc says object names are prefixed for isolation when set, but composeStacks() never reads opts.namespace, so the option is currently a no-op and the docs are misleading. Either implement the namespacing behavior (and update all references like apps.objects/report.object/data.object, etc.) or clarify in the schema/JSDoc that this is reserved for Phase 2 and currently unused (and consider omitting it from the schema until implemented).

Suggested change
* Optional namespace prefix (reserved for Phase 2 Marketplace isolation).
* When set, object names from this composition are prefixed for isolation.
* Optional namespace prefix (reserved for Phase 2 Marketplace / tenant isolation).
*
* NOTE: As of the current spec version, this option is **declarative only** and may be
* ignored by runtime composition helpers such as `composeStacks()`. It is preserved in
* the schema for forward-compatibility and tooling, but does not yet trigger automatic
* object-name prefixing or other isolation effects.

Copilot uses AI. Check for mistakes.
Comment on lines +590 to +594
if (stacks.length === 0) return {} as ObjectStackDefinition;
if (stacks.length === 1) return stacks[0];

const opts = ComposeStacksOptionsSchema.parse(options ?? {});

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

composeStacks() returns early for stacks.length === 0 / === 1 before parsing options, so invalid options (e.g. { manifest: -1 }) won't be rejected and callers get inconsistent behavior depending on stack count. Consider parsing/validating options before the early returns (or at least when options is provided) to keep runtime validation consistent.

Suggested change
if (stacks.length === 0) return {} as ObjectStackDefinition;
if (stacks.length === 1) return stacks[0];
const opts = ComposeStacksOptionsSchema.parse(options ?? {});
const opts = ComposeStacksOptionsSchema.parse(options ?? {});
if (stacks.length === 0) return {} as ObjectStackDefinition;
if (stacks.length === 1) return stacks[0];

Copilot uses AI. Check for mistakes.
Comment on lines +510 to +521
case 'override': {
// Replace in-place in the result array
const idx = result.indexOf(existing);
result[idx] = obj;
map.set(obj.name, obj);
break;
}
case 'merge': {
const merged = { ...existing, ...obj, fields: { ...existing.fields, ...obj.fields } } as Obj;
const idx = result.indexOf(existing);
result[idx] = merged;
map.set(obj.name, merged);

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mergeObjects() uses result.indexOf(existing) on every duplicate to locate the item to replace, which makes conflict resolution O(n^2) in the worst case (many objects with many duplicates across stacks). Consider tracking the index alongside the object in the map (e.g., Map<string, { obj, idx }>), or maintaining a separate name -> index map, to make overrides/merges O(1).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 支持多应用 stack 合并原语(composeStacks):解决多配置手工合并痛点

3 participants