Skip to content

fix(cli): derive the temp app's workspace alias table from pnpm-workspace.yaml (#3890) - #4922

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3890-workspace-alias
Aug 17, 2026
Merged

fix(cli): derive the temp app's workspace alias table from pnpm-workspace.yaml (#3890)#4922
yinlianghui merged 1 commit into
mainfrom
claude/issue-3890-workspace-alias

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3890

前提验证(先于实现)

卡面前提在 origin/main = 5ffcc1432完全成立,并且实测发现缺失面比卡面记的更大:

  • commands/dev.ts:228-240 的别名表仍是 11 个包的手工清单,@object-ui/fields 零命中(反证:同文件里别名表本身找得到)。
  • serve.ts / build.tsisMonorepo 零命中,vite 配置里没有 resolve.alias
  • 按卡面脚本复现(仓根 dogfood/app.json + pages/index.json,node packages/cli/dist/cli.js dev … --port 5893 --no-open):Vite 依赖扫描报出卡面点名的那 4 个包,页面模块 500。

但缺的不是 4 个,是 10 个。 生成 app 入口静态 import 9 个平台包,其传递 value-import 闭包是 21 个包,表里只有 11 个:

卡面列出(依赖扫描可见)扫描看不到、藏在它们后面的
@object-ui/fields@object-ui/permissions@object-ui/mobile@object-ui/plugin-detail@object-ui/data-objectstack@object-ui/i18n@object-ui/plugin-map@object-ui/providers@object-ui/react-runtime@object-ui/sdui-parser

Vite 的依赖扫描在第一层解析不了的地方就停,所以它只报得出前 4 个。这条实测直接决定了路线。

路线选择:B(从 pnpm-workspace.yaml 派生)

A(回填 4 条)在本仓当场就是不完整的修法 —— 补完卡面点名的 4 个之后还剩 6 个继续 500,页面照旧空白。而"手工清单必须追传递导入图"这个结构本身才是产生本卡的原因:清单要维护的不是"app import 了什么",而是"app 传递 import 了什么",即表内每个包每个源文件的导入图,没有任何门禁钉住它。

新增 packages/cli/src/utils/workspace-vite.ts,三命令共用:

  • 别名表派生:读 pnpm-workspace.yamlpackages: 模式(用已是 cli 依赖的 glob 展开,! 排除也支持),取 @object-ui/ scope 且有 src barrel 的成员,别名指向其 src目录
  • 入口推断规则(卡面记的 B 的代价):按 Vite 自己的 resolve.extensions 顺序探测 index.*。仓内 .ts / .tsx 两种拼法都在(core vs fields),按解析器的顺序探测不是猜测,而是解析器自己会给的答案。
  • 为什么指向目录而不是 barrel 文件:仓内所有 checked-in bundler 配置都这么写(apps/console/vite.config.ts:117-136);更重要的是别名替换的是匹配到的前缀并保留其余部分,文件目标会把子路径 import 改写成一个不可能存在的路径。
  • 完整性是安全方向:表里列了包但没人 import 是惰性的(别名只对匹配的 import 生效),表里缺了包而有人 import 就是 500。两个方向的代价不对称,所以规则偏向"多列",并由与 pnpm-workspace.yaml 对账的测试兜住"不能少列"。
  • serve / build 补上 monorepo 分支:workspace 内跳过 npm install(那份清单在 workspace 下本就是空的),并共用同一份 vite 覆盖。

一处顺带必须修的:lucide-react 别名

它和平台包在同一张表里,而且是平台包解析通了之后才暴露出来的:原来的目标是 require.resolve 给的入口文件,于是每个 lucide-react/… 子路径 import 都被改写成"入口文件路径 + 子路径",packages/components/src/lib/lazy-icon.tsx 因此 500(它 import 该库的 dynamic-icon 子路径)。改成指向包根目录后,裸 specifier 与子路径落在同一个目录上,入口交给包自己的 exports 决定。不修它本卡的验收状态(页面能渲染)达不到,故在本 PR 内一并改,并有专门的钉。

复现前后读数(同一脚本、同一 fixture,仓根)

按浏览器的方式走模块图(取回一个模块 → 解析它被改写后的 import URL → 继续取回):

前(5ffcc1432,端口 5893)后(端口 5894/5895)
依赖扫描Failed to run dependency scan,报 4 个包无报错
前 400 个模块392 × 200,8 × 500400 × 200,0 非 200
整图(走不下去)2498 × 200,0 非 200
真浏览器(chromium,document.body.innerText)卡面实测:空88 字符,含页面 schema 的 hello from the dogfood app;失败响应 0
objectui build(workspace 内)无 monorepo 分支,不可能解析平台包构建成功,产出 index.html + assets
objectui serve(workspace 内)同上400 × 200,0 非 200;日志显示跳过 npm install

500 的 8 个模块:plugin-grid/src/ObjectGrid.tsxplugin-form/src/{ObjectForm,ModalForm,MasterDetailForm,LineItemsPanel,sectionFields}plugin-kanban/src/ObjectKanban.tsxcomponents/src/lib/lazy-icon.tsx(最后一个即上节的 lucide 子路径)。

反向验证(先书面预判,后跑;变异前已 commit,还原用 git checkout)

三次变异,方向都先写后跑:

  1. 从派生源抠掉一个包(@object-ui/fields)。预判:与 manifest 对账的钉必须红;"别名目标都是目录"那条保持绿(它只遍历表里产出的东西 —— 正是"产出为空所以绿"的形状,也正是对账钉存在的理由)。实测:3 红 11 绿,对账钉与"卡面点名的 4 个"钉如预判红;第 3 条超出预判 —— resolves barrels spelled .ts and .tsx alike 恰好拿 fields.tsx 样本,一起红了。方向一致,数量我预判少了一条,如实记录。"目标都是目录"确实保持绿。
  2. 还原 lucide 的旧入口文件目标。预判:两条 lucide 钉红。实测:2 红,失败信息直接打印出被改写成的那条不存在路径(…/dist/cjs/lucide-react.js/dynamic.mjs),即缺陷的活体。
  3. serve.ts 整个还原到改动前。预判:三命令一致性的两条钉红。实测:2 红,都点名 serve.ts。

还原后全量重跑复绿(147/147)。

测试

新增 packages/cli/src/__tests__/workspace-vite.test.ts(14 条):与 pnpm-workspace.yaml独立走一遍的对账钉(被测函数用 glob 展开,测试用自己的 readdirSync;用被测函数重算期望值只能证明它是确定性的)、卡面点名的 4 个 + 扫描看不到的 6 个、.ts/.tsx barrel 推断、别名目标必须是目录、无 barrel 的包必须被排除(@object-ui/runner)、lucide 指向包根且子路径可解析(子路径从真实 importer 里读出来而不是写死)、临时 fixture workspace 上的派生规则自测(含"barrel 一没就掉出表"的植入缺陷方向)、以及三命令一致性(都走同一 helper、都有 workspace 分支、都不许再出现手写别名条目)。

pnpm exec vitest run packages/cli/ scripts/__tests__/side-effects-declaration-consistency.test.ts --maxWorkers=2
Test Files 6 passed (6) Tests 147 passed (147)
pnpm exec turbo run type-check --concurrency=2
Tasks: 81 successful, 81 total
pnpm --filter @object-ui/cli lint -> 0 errors(12 warnings,全部既有)
node scripts/check-control-bytes.mjs -> OK (4397 tracked text files)
node scripts/check-phantom-dependencies.mjs -> OK

sideEffects 门(#3943)按 PM 提示单独确认:它只扫 workspace 包目录下 basename 匹配 vite.config.* 的文件,本改动落在 packages/cli/src/,不在其扫描面内,实测同批绿。

未做的事(如实记录)

objectui build改动前状态没有实测跑:那条路径会先无条件 npm install 把整套已发布平台包装进 .objectui-tmp(联网、慢),不划算。改动前的判据用的是 origin/main 上的静态事实(serve.ts/build.tsisMonoreporesolve.alias 皆零命中)+ 变异 3 的红。改动后的构建成功是实测的。

changeset:.changeset/cli-workspace-alias-derivation-3890.md(patch)。


Generated by Claude Code

…pace.yaml (#3890)
Inside a pnpm workspace the generated temp app installs nothing and the repo
root declares no `@object-ui/*`, so a Vite alias table is the only thing that
resolves a platform package there. `dev` kept that table by hand — eleven names
— while the property it needed was the transitive import graph of every package
already in it. Measured on the reported commit: the generated entry closes over
21 packages, ten were unlisted, and 8 of the first 400 modules a browser walk
reaches answered 500. Vite's dependency scan named only four of the ten, because
a scan stops at the first layer it cannot resolve, so a backfill of "the missing
four" would have left six behind.
The table is now derived from the workspace manifest: every scoped package that
exposes a source barrel, aliased at its `src` directory. `serve` and `build` had
no workspace branch at all — no aliases, plus an unconditional `npm install`
against a manifest that is empty here — and now share the same helper, so the
three cannot drift apart again.
The `lucide-react` entry moves from the entry file `require.resolve` reports to
the package root. An alias replaces the matched prefix and keeps the rest, so
the old target rewrote every subpath import of it into `<entry-file>/<subpath>`;
with the platform packages finally reachable, `components/src/lib/lazy-icon.tsx`
answered 500 for exactly that reason.
Measured after, same repro: 0 of 2498 modules non-200, `objectui build` produces
a bundle in-workspace for the first time, and the page renders its schema.
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-DzXFL076.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)38.46KB10.17KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)498.37KB111.05KB
core (index.js)4.06KB1.61KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)157.05KB43.28KB
fields (index.js)231.73KB57.60KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.86KB10.83KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)239.81KB59.97KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)120.43KB29.04KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.59KB53.01KB
plugin-kanban (index.js)52.72KB14.54KB
plugin-list (index.js)111.23KB26.97KB
plugin-map (index.js)17.91KB5.72KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.97KB11.33KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.53KB9.41KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 验收:ACCEPT(#3890,批次 17,PM 会话 session_01GTRjn8xBqp75dk7kFupVRt)

路线裁量(B,派生式)—— 以实测定案而非偏好:卡面点名的 4 个缺失包只是 Vite 依赖扫描在第一层失败处能看见的;静态量出传递 value-import 闭包 = 21 包、手表 11 条、实缺 10 —— 路线 A(回填 4 条)当场就是不完整修法。派生规则读 pnpm-workspace.yaml(含 ! 排除),别名指 src 目录,与 console 现行写法一致;「多列惰性、少列即 500」的完备性取向 + 与 manifest 独立对账的钉兜住少列方向 —— 消灭了产生本卡的手表结构。

复现读数(实物级):BEFORE 依赖扫描报错 + 模块图 8×500(ObjectGrid/ObjectForm/MasterDetailForm 等);AFTER 全图 2498 模块零非 200,真 chromium 页面渲染出 schema 文本;serve 400×200,build 在 workspace 内首次构建成功 —— dev/serve/build 三命令补齐一致。

实物核验(已过):按真实 merge-base 对账 —— 本 PR 自身改动恰为 6 文件 +659/−122,全在 packages/cli + changeset;与 main merge-tree 零冲突;标识 0;releases 0;#3943 sideEffects 门同批绿(其扫描面按 basename 匹配包目录 vite config,不含 cli 内部代码,dev 已单独确认)。

反向验证:三变异全部预判先行 —— M1 抠 fields:3 红 11 绿,其中一条超预判的红(.tsx barrel 样本恰是 fields)按「方向一致、数量欠预判」如实记录;「目标都是目录」钉如预判保持绿(空产出假绿形状,依赖对账钉互补);M2 lucide 还原:2 红且失败信息打印出被改写的不存在路径;M3 serve 整体还原:2 红点名 serve.ts。

有据偏离(全部接受):①lucide-react 条目同表同修 —— 非相邻缺陷而是本卡验收状态的必要条件(平台包通了它才暴露,不修页面仍不渲染),单列论证 + 两钉(子路径从真实 importer 读出);②build 改动前态未实跑(无条件 npm install 联网慢),以静态零命中 + M3 红代偿,「未做的事」如实列明 —— 不假称实测,正确。

CI(亲读终态):20 项全 completed,18 success + 2 skipped,零失败。

附带产出:#4923(serve/build 项目根定位与 dev 分叉,待分诊)、#4924(serve 无 --no-open 观察卡)、#4925(console 34 条手表同类风险记录)。

→ undraft + auto-merge (SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 05:46
@yinlianghui
yinlianghui added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit 4102bfcAug 17, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3890-workspace-alias branch August 17, 2026 05:46
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 23, 2026
…ld (objectstack-ai#4923) (objectstack-ai#5038)
* fix(cli): resolve the project root the same way in dev, serve and build (objectstack-ai#4923)
For a project with an app config and a `pages/` directory beside it, the three
commands answered one invocation two different ways. `dev` anchored on the
schema argument: `dirname(<schema>)` is the project root, `pages/` beside it
means file-system routing, the named file is the app config. `serve` and `build`
looked for a `pages/` directory in the current working directory and nowhere
else, so from any directory above the project they fell through to single-schema
mode and handed the app config to the renderer as if it were a page.
Measured on the reported fixture (`<root>/app.json` + `<root>/pages/index.json`,
invoked from the directory above): `dev` reported the project and one route,
`serve` reported `Loading schema: <root>/app.json`, and `build` did the same and
exited 0 — the emitted bundle embedded the app config as the page schema and
contained no page from `pages/` at all. A wrong artifact, produced silently.
PR objectstack-ai#4922 made `serve`/`build` able to resolve modules in a workspace, which is
what turned this from "it could not run anyway" into a reachable wrong output.
Detection now lives in one helper the three share (`utils/project-source.ts`),
resolving in a fixed order: a `pages/` directory beside the schema argument,
else one under the current directory, else single-schema mode. The reporting is
split out beside it, so a reader comparing two commands' output is comparing one
function with itself. `serve` and `build` also pass the resolved app config to
the routed generator, which they never did — a routed project kept its routes
but lost its layout otherwise.
No documented intent was found for `serve`/`build` to be single-schema commands;
the documentation says the opposite in three places (`cli.ts` calls `dev` an
"alias for serve", the README and the docs page call `serve` a "legacy alias of
`dev`", and the routing section promises file-system routing whenever "the
project contains a `pages/` directory"). The docs now also say WHICH directory
that is.
A lone schema file with no `pages/` beside it is unchanged and pinned: it is the
fallback, not a casualty.
Co-authored-by: Claude <noreply@anthropic.com>
* test(cli): pin the shared detection by its call site, not the identifier (objectstack-ai#4923)
Reverse verification exposed the gap: reverting serve.ts to the cwd-only
reading while leaving its now-unused import behind kept the bare-name
assertion green. The pin now names the call form, so an import alone
cannot satisfy it.
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude