Skip to content

feat(console): OBJECTSTACK_SPEC_DIST hook so a framework build can bundle its own @objectstack/spec (#4854) - #4927

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4854-console-spec-dist-hook
Aug 17, 2026
Merged

feat(console): OBJECTSTACK_SPEC_DIST hook so a framework build can bundle its own @objectstack/spec (#4854)#4927
yinlianghui merged 2 commits into
mainfrom
claude/issue-4854-console-spec-dist-hook

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4854

apps/console/vite.config.ts 增加 OBJECTSTACK_SPEC_DIST 环境钩子,与既有的 OBJECTSTACK_CLIENT_DIST 对称:设了就把 @objectstack/spec 的解析指向给定的本地 spec 包;未设时完全惰性,现行为零变化。

⚠️ 依赖方向按分诊评论(5311163356)执行:本卡先落,framework 的 .objectui-sha pin 随后推进,再落 objectstack#8134 —— 正文的 Blocked-by: 行不适用。

前提验证(先做,后写码)

探针卡面读数本 PR 实测(base 5ffcc1432)结论
OBJECTSTACK_SPEC_DIST 仓内出现次数00一致
@objectstack/spec exports map 条目数1818(installed 17.0.0-rc.6)一致
objectui 实际写出的 spec specifier17 subpaths / 29 包17 个不同 specifier(裸 + 16 subpath),/ui 214 处、/data 87 处一致
OBJECTSTACK_CLIENT_DIST 钩子形状前缀别名到包目录同(:161:167)一致

前提成立,premise_still_valid: true

只有一处卡面表述需要修正,而且它恰好是不能照抄的那处:卡面给的映射草案是「@objectstack/spec/NAME 映到 SPEC_PKG/dist/NAME/index.mjs,openapi.jsonpackage.json 作两个非目录例外」。实测 ./openapi.json 的目标是 ./json-schema/openapi.json —— 不在 dist/。按草案手写表会得到 dist/openapi.json/index.mjs,一个不存在的路径。所以本 PR 不写死规则,而是从覆盖目标自己的 exports map 派生每一条别名。卡面也明说了「shape is yours to choose」。

机制

新增 scripts/vite-objectstack-spec-dist.ts(放在 scripts/vite-*.ts,turbo.json 的 build inputs 与 apps/console/tsconfig.node.json 的 include 都已按该 glob 覆盖,不需要新增声明)。

  • 入参接受包目录 / 其 dist/ / 其中的入口文件三种写法,向上走到最近的 package.json要求它就是 @objectstack/spec,再取 realpath。
  • 读该包 exports map,按 importmodulebrowserdefault 取叶子(不取 types —— 它在每个条件对象里排第一,按声明顺序遍历会把每条 subpath 别名到 .d.mts)。
  • 每条 exports 条目生成一条别名,subpath 在前、裸 specifier 在后。顺序是正确性的一部分:Vite 的字符串 find 命中规则是「相等,或以 find + / 开头」,先命中先赢,裸条目排前面会吞掉所有 subpath。排最后则它还兼任兜底 —— 覆盖目标没有声明的 subpath 会被重写成一个不可能存在的路径,构建响亮报错,而不是静默落回已安装的 spec。
  • 所有失败路径都抛错并指名犯错的值:路径不存在、不是 spec 包、exports 条目指向构建产物里缺失的文件、通配 pattern。不做宽容回退 —— 静默落回已安装的 spec 会重建这个钩子要消灭的偏差类,而 framework 侧的守卫分辨不出来。

四个标记面逐个判定

判定依据
server.fs.allow已处理注入包目录进 allow 列表。理由与 client 相同:override 在 workspace 根之外,Vite 默认 fs.allow 会 403(表现为空白页,不是构建错误)。client 推两条(dirname 与其父)是因为它拿到的可能是文件;spec 侧解析出的就是包目录本身,一条即可。未设任一 override 时 server.fs 整个键不存在,与基线逐字一致。
optimizeDeps已处理env 设了就把四条 spec 条目从 include 摘掉。依据是 vite 8 源码实测:pre-alias 插件对「别名到 node_modules 之外」的解析,只有optimizeDeps.include 点名该 specifier 时才注册为 dep(node_modules/vite/dist/node/chunks/node.js:27820,(isInNodeModules(resolvedId) || optimizeDeps.include?.includes(id)))。留着 = 把注入的 spec 塞进 node_modules/.vite 预打包缓存,而该缓存 key 不会因为 framework 原地重建 spec 而变化 —— 陈旧预打包供着昨天的 schema,正是本钩子要消灭的偏差类。代价只是 dev 冷启动略慢。另需记明:build 期预打包在 Vite 5.1 已移除(node.js:37128 的告警文案;depsOptimizerEnabled 要求 !isBuild,node.js:32743),所以 framework 的 vite build 路径根本不读这张表,这条处理是给 pnpm dev 用的。
manualChunks(本仓已是 rolldown 的 advancedChunks.groups)已处理卡面说「client 今天已经有这个性质,可能可以接受」。spec 侧量级不同 —— client 是单入口,spec 是 29 个包、400+ 处 import 的最大 vendor 面,掉出 vendor-objectstack 会散进各自 importer 的 chunk。注入构建应当与发布构建只差 spec 内容、不差 chunk 布局,所以把覆盖目录并进该 group 的 test(基线.source | 转义后的包目录 + 分隔符),只加不换:原来的 node_modules/@objectstack/ 与 pnpm @objectstack+ 两条臂都保留。未设 env 时该 group 的 test 就是原来那个字面量对象本身(identity 相同)。
turbo.json 的 env 声明已处理tasks.build.envOBJECTSTACK_SPEC_DIST。turbo v2 严格 env 模式会剥掉未声明的变量,那会让钩子读到 undefined 而调用方以为自己注入成功 —— 正是要防的静默失效。卡面提到 framework 直接调 console 的 build 脚本、但 deps 构建走 turbo;根 pnpm build 也走 turbo,所以这条是必需的。

subpath 对账读数

scripts/__tests__/vite-objectstack-spec-dist.test.ts 把派生表与 Node 自己的解析器(import.meta.resolve,即算法本身,不是对 map 的第二次解读)逐条对账:

  • exports map 条目:18;生成别名:18;与 Node 解析结果不一致:0;无别名:0
  • 三个形状各钉一条:/uidist/ui/index.mjs(目录型)、/openapi.jsonjson-schema/openapi.json(非 dist 例外)、/package.jsonpackage.json
  • 仓内扫描(packages / apps / examples,排除 node_modulesdist):实际写出的 spec specifier 全部走一遍别名表并落到存在的文件,未解析:0。这条随仓库生长自动收紧 —— 将来某个 subpath 覆盖目标没有,它会红。
  • 裸 specifier 必须是最后一个 key;未声明的 subpath 被重写成 .../dist/index.mjs/not-a-real-subpath(实测不存在)。

另加两条真实 vite build(约 0.3s,不落盘):注入后的别名表能把裸 + /ui + /data + /package.json 全部打进产物,chunk 的外部 import 列表为空;而字面照搬 client 钩子的单条前缀别名会直接构建失败(2 个解析错误)。卡面「client hook 不能原样搬」由此从断言变成实测。

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

预判两条都是普通 RED(派生表是 case 1 的唯一输入,null 分支是 case 2 的唯一输入,变异只能增加发现),既不是计数型也不是反转型。实测:

  1. 从映射抠掉一个 subpath(… && s !== '@objectstack/spec/ui')→ 5 红。这里有一处值得记下的反直觉:被抠掉的 specifier 不会表现为「无别名」。它会前缀命中裸条目,被报成 @objectstack/spec/ui -> …/dist/index.mjs/ui(不可能存在的路径),所以发现落在仓内扫描的 unresolved 里,而 missing 保持为空;对账用例更早一步就在 18-vs-17 上失败了。
  2. 把钩子改成常开(env 读取处兜底成已安装的 spec 目录)→ 2 红,都在 console config 那组:alias 表多出 18 个 @objectstack 键;optimizeDeps.include 从 7 条掉到 3 条。

惰性证明本身读的是真实 console config(以带 query 的模块 id 二次求值拿到 env 设了的那份),不是 helper 的返回值 —— helper 正确但没接线,是本仓付过学费的失败形状。

边界(留给 objectstack#8134 的一句)

这个钩子是打包器层的,tsc 不读 resolve.alias。console 的 build 是 tsc && vite build,所以类型仍然按 lockfile 里的 spec 声明检查。如果注入的 spec 带了尚未发布的类型,framework 侧需要自己决定怎么处理那一半(路径映射,或接受类型按已发布版本校验)。本卡按维护者裁定只做打包器半边,不在此扩围。

验证

pnpm exec vitest run scripts/__tests__/vite-objectstack-spec-dist.test.ts → 18 passed
pnpm exec vitest run scripts/__tests__/vite-objectstack-spec-dist.test.ts apps/console/
→ 51 files / 570 passed
pnpm exec vitest run scripts/__tests__/side-effects-declaration-consistency.test.ts
scripts/__tests__/turbo-build-inputs.test.ts scripts/__tests__/scripts-type-check.test.ts
scripts/__tests__/check-type-check-coverage.test.ts
scripts/__tests__/vitest-config-alias-targets-3944.test.ts → 5 files / 155 passed
pnpm type-check:scripts → exit 0
pnpm exec turbo run type-check --concurrency=2 → 81/81 successful
node scripts/check-control-bytes.mjs → OK(4400 files)
node scripts/check-phantom-dependencies.mjs → OK
node scripts/check-changeset-presence.mjs / check-changeset-no-major.mjs → OK

#3943 的 side-effects 门按要求连带跑过:它的别名解析只匹配 @object-ui/ 作用域 + path.resolve(…) 形状,本 PR 的 @objectstack/spec 条目是运行时 Object.assign 进去的,不进它的扫描面,aliasEntries 计数不变。

changeset:空 frontmatter(纯构建工具改动,没有任何发版包的 src/ 被触及;check-changeset-presence 也确认「无 changeset 义务」,这份是主动声明)。

相邻发现(未在本 PR 修)


Generated by Claude Code

…ndle its own @objectstack/spec (#4854)
apps/console/vite.config.ts 增加 OBJECTSTACK_SPEC_DIST 环境钩子,与既有的
OBJECTSTACK_CLIENT_DIST 对称:设了就把 @objectstack/spec 的解析指向给定的本地
spec 包,未设时完全惰性(alias 表、optimizeDeps.include、vendor-objectstack 分
块判据、dev server 的 fs.allow 全部保持基线值)。
client 钩子不能原样搬:@objectstack/client 的 exports map 只有 1 条,而
@objectstack/spec 有 18 条且每条都重定向进 dist/,Vite 的字符串 alias 是前缀替
换、不读 exports map。所以映射从**覆盖目标自己的 exports map** 派生,逐条生成
alias(subpath 在前、裸 specifier 在后),而不是按「dist/NAME/index.mjs」这类手
写规则——后者对 ./openapi.json 就是错的(它指向 json-schema/openapi.json)。
任一失败路径都抛错(路径不存在、不是 spec 包、exports 条目指向构建产物里缺失的
文件、通配 pattern),不做宽容回退:静默落回已安装的 spec 正是这个钩子要消灭的
偏差类。
Co-authored-by: Claude <noreply@anthropic.com>
18 条 exports map 逐条与 Node 自己的解析器(import.meta.resolve)对账;仓内实
际写出的每个 @objectstack/spec specifier 都走一遍别名表并落到存在的文件;裸
specifier 必须排在最后(未声明的 subpath 由此重写成不存在的路径,响亮报错而不
是静默落回已安装的 spec)。
四个标记面在 env 未设时逐个钉在基线值上(alias 表无 @objectstack 键、
optimizeDeps.include 七条原样、vendor-objectstack 判据源码逐字、server.fs 不
存在),读的是真实 console config 而不是 helper;env 设了时同一 config 以带
query 的模块 id 再求值一次,四面同时到位。
外加两条真实 vite build:注入后的别名表能把 bare + /ui + /data + /package.json
全部打进产物且 chunk 无外部 import;而字面照搬 client 钩子的单条前缀别名会直接
构建失败——卡面前提由此从断言变成实测。
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-WguO_FjA.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.61KB111.16KB
core (index.js)4.06KB1.61KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)157.05KB43.28KB
fields (index.js)232.86KB58.11KB
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)39.16KB10.97KB
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.42KB29.03KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.58KB53.00KB
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.55KB2.07KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.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(#4854,批次 17,PM 会话 session_01GTRjn8xBqp75dk7kFupVRt)

实物核验(已过):5 文件 +799/−14 逐字对账(vite.config +82/−14、helper 240、测试 465、turbo.json 单行 env、changeset 空 frontmatter 主动声明);标识 0;releases 0;与最新 main merge-tree 干净。

机制裁量(接受,且有实证):映射从覆盖目标自己的 exports map 派生而非按卡面草案手写 —— 草案对 ./openapi.json 是错的(真实目标 json-schema/openapi.json 不在 dist 下,照抄会得到不存在路径),「派生而非规定”由此从偏好变实证。裸条目排序即正确性(排前吞 subpath、排后兼兜底且未声明 subpath 响亮报错不静默回落);所有失败路径抛错。

四面判定表(逐面显式,全部带证据):fs.allow 注入包目录(否则 403 空白页);optimizeDeps 摘四条 spec 条目 —— 依据 vite 8 源码行级实测(pre-alias 对 node_modules 外解析仅在 include 点名时注册为 dep;build 期预打包 5.1 已移除,此处理只服务 dev);manualChunks(rolldown advancedChunks)把覆盖目录并进 vendor-objectstack 的 test,只加不换、未设 env 时 identity 相同;turbo.json env 声明防严格模式剥变量的静默失效。

subpath 对账:18 条 exports ↔ 18 条别名,用 import.meta.resolve(算法本身)逐条对账零不一致;仓内 17 个实际 specifier 全部落到存在文件。两条真实 vite build收尾:注入后四形态全打进产物、chunk 外部导入为空;字面照搬 client 钩子的对照组构建失败 2 处 —— 「client hook 不能原样搬」由断言变实测。

反向验证:两变异预判先行命中;M1 的反直觉形态(被抠 subpath 表现为裸条目前缀吞并的 unresolved 而非 missing)按实测写进测试头注而非按预期模板 —— 纪律正确。惰性证明读真实 console config(带 query 二次求值)而非 helper 返回值。

有据偏离(全部接受):派生式映射(实证)、测试落 scripts/tests(被测对象性质 + #3943 门互动已确认不进扫描面)、运行时拼接 specifier 绕 TS5097(共享 tsconfig 未动,缺口立 #4926)、空 frontmatter changeset(照先例)。

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

给 framework 侧的边界注记已在 PR:钩子是打包器层,tsc 不读 alias,类型半边归 os#8134 自决 —— 未扩围,正确。os#8134 的 .objectui-sha pin 可在本单落 main 后推进。

→ undraft + auto-merge (SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 05:53
@yinlianghui
yinlianghui added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit bf2a960Aug 17, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4854-console-spec-dist-hook branch August 17, 2026 05:54
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