Skip to content

packages/layout: sideEffects: false contradicts the load-time registerLayout() — a side-effect-only import can be tree-shaken away #3899

Description

@yinlianghui

发现于 #3787 的实现(PR:claude/issue-3786-3787-pageheader-docs-demo),不在该 PR 处理 —— 那一单的面是文档页与 demo JSON,这一条是 packages/layout 的打包声明,是另一类事实。PR 里只是绕开了它(显式调用 registrar 而非依赖 import 副作用),没有修它。

现象

packages/layout/package.json 声明:

"sideEffects": false

packages/layout/src/index.ts:126-131 的唯一注册入口是模块加载副作用:

// Keep backward compatibility for now if called directly
try {
  registerLayout();
} catch (e) {
  // Ignore registration errors during build/test cycles
}

sideEffects: false 是给打包器的承诺:「本包任一模块只被 import 而其导出未被使用时,可以整体丢掉」。而这个包恰恰把 page-header / app-shell / sidebar-nav 等的注册全部放在模块体里。两者直接矛盾。

影响

import '@object-ui/layout';(纯副作用 import,注册组件的标准写法)在任何遵守该 flag 的打包器下可以被静默删除 —— 结果是 ComponentRegistry 里没有 page-header,渲染出红色 Unknown component type 面板(OBJUI-001),而构建全绿、没有任何警告。

对比:PluginLoader 动态 import 的每一个 @object-ui/plugin-* 包都没有 sideEffects 字段(实测 plugin-grid / plugin-view / plugin-form 均为 undefined),只有 @object-ui/layout 独此一家声明了 false

今天没人被打到,是因为现存消费者(packages/app-shellapps/console)都 import 了具名导出,模块因此一定会被求值。这是巧合,不是设计:任何新消费者按「import 一下注册组件」的直觉写法接入,就会踩中。#3787 的 PR 里我按显式 registerLayout() 调用规避,并在 apps/site/app/components/registerLayoutBlocks.ts 的模块头写明了原因 —— 那是绕行,不是修复。

建议处置

两个方向,任选其一(需要维护者定):

  1. 去掉 sideEffects: false,或收窄成 ["dist/index.js"] 之类的数组,让声明与事实一致。最小改动,保住「import 即注册」的直觉写法。
  2. 删掉 index.ts 里的自动 registerLayout(),把注册改成消费者必须显式调用(apps/console/src/register-plugins.ts 就是这个模式)。声明诚实了,但这是一次 breaking:任何依赖自动注册的消费者会静默失去组件 —— 需要先量清有没有这样的消费者。

方向 1 与 2 语义相反(一个承认副作用、一个消灭副作用),不该由实现者随手选。

参考位置

关联:#3787(发现于此)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpm:queuetarget:v17v17 发布窗口工作集(GA 前排查 2026-08-04)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions