Skip to content

feat(types,cli,verify)!: 只解析 host app 声明过的包 —— NODE_PATH 不再算数,ADR-0093 D5 的墙与启动方式脱钩 (#4719) - #4999

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4719-host-declared-resolution
Aug 3, 2026
Merged

feat(types,cli,verify)!: 只解析 host app 声明过的包 —— NODE_PATH 不再算数,ADR-0093 D5 的墙与启动方式脱钩 (#4719)#4999
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4719-host-declared-resolution

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4719

维护者已拍板方向 2:只有包名出现在 host app 的 package.json 声明里,才去 host 的 node_modules 里解析它。本 PR 实现该方向,并逐一验证三个消费者。

契约 vs 实际

@objectstack/types/nodecreateHostRequire 返回一个 CJS createRequire,而 CJS 解析认 NODE_PATH(Module.globalPaths)。pnpm 生成的 bin shim 第一件事就是

export NODE_PATH="<workspace>/node_modules/.pnpm/node_modules"

于是任何被工作区里任意一个包传递依赖到的包都躺在那个 hoisted store 里,hostRequire.resolve() 一律成功 —— 跟 host app 声明了什么毫无关系。ADR-0093 D5 那道墙因此只在不经 shim 的调用里生效,而它的报错文案一直在教 operator「declare it in the app's package.json」,那恰恰是 CLI 从来没检查过的那件事。

cloud apps/objectos-ee(当时未声明 @objectstack/organizations)的实测:pnpm start(经 shim)boot 成功、插件表里有 Organizations、D5 一声不吭;node node_modules/@objectstack/cli/bin/run.js serve(不经 shim)则 ✖ FATAL + exit 1。同一个 app、同一份 package.json、同一个 posture,只因进程怎么被拉起来。

方向 2 的实现

落点 packages/types/src/node.ts(#4857 迁移后的唯一源)。

  1. 解析前先读 manifest。 只有包名是 dependencies / devDependencies / optionalDependencies / peerDependencies时,才走 host 解析。「能被解析到」不再算数 —— 那正是让契约失效的偶然。
  2. 未声明 → 退回 importing package 自身的解析(裸 import())。这条保住了框架自有包的加载路径(serve 的 plugin-auth / service-i18n、bootStack 的各 service plugin)。裸 import() 是 ESM,ESM 不认 NODE_PATH,所以退路不会把洞重新打开 —— tsup 的 cjs 产物里 import() 也原样保留,已核对 dist/node.js
  3. 声明了但解析失败 → 不再回退,直接报「安装坏了」。在这里回退等于把「碰巧别的包供应了它」这个偶然请回来,并把安装问题报成缺失。
  4. 求值期崩溃照旧原样上抛(既有契约,有用例钉住)。

两类失败经新导出的 hostImportFailureKind(err) 暴露分类;两者都仍带 code: 'MODULE_NOT_FOUND',isModuleNotFoundError 的既有判定不变。

fail-fast 文案(三个消费者各自分支):

分类 文案指向
undeclared 在 app 的 package.json声明并安装;并说明为什么 hoisting / NODE_PATH 不被接受
declared-unresolvable 这是安装问题(pnpm install、生产 prune 砍掉了它、dist 没构建);别再回去看那份已经写对的 package.json

边界形态的取舍(理由)

  • optionalDependencies 算。 npm/pnpm 会装、装失败可容忍;「装上了就是声明了」成立。而且真没装上时,declared-unresolvable 分支会精确说出来,不会假装 app 没要过它。
  • peerDependencies 算。 一个 app 不是任何人的 peer,把企业插件写在这里很少见;但那仍是在自己的 manifest 里有意点名。决定性理由是已存在的消费场景:serve.ts 的 AI 版次门(hostDeclaresDependency,Make optional-plugin loading intent-driven: fail-fast on declared-but-missing, drop presence-based auto-enable #1597)自诞生起就读这四个字段,理由与本单逐字相同(「Gating on a declared dep — not mere resolvability — makes this reliable in a workspace/monorepo」)。这里收三个、那里收四个,等于把「declared」在同一个文件里分裂成两种方言(Prime Directive Add comprehensive test suite for Zod schema validation #12)。所以本 PR 让那个私有实现改为委托共享 owner,一个问题一个答案。
  • bundleDependencies 不算 —— 它是名字数组,且其成员必须同时出现在 dependencies 里,不可能是唯一声明。
  • scoped / workspace / alias / subpath:按 判定,天然正确。workspace:*link:../xfile: 的值是包管理器的事;"foo": "npm:bar@1" 下可导入的说明符是 foo、manifest 的键也是 foo,所以按键判定恰好对(import('bar') 正确地读作未声明)。subpath 先剥掉再查(@objectstack/platform-objects/plugin@objectstack/platform-objects),scoped 保留两段。
  • 不是裸包名的说明符(相对/绝对路径、node: 内建、file: URL)绕过这道门 —— 那不是 manifest 能声明的东西,不该被拒。
  • hostRoot 没有可读的 package.json:记为 manifestMissing,报错直说「那里没有 package.json」,而不是误导性的「你没声明」。

三个消费者,逐一验证

消费者 接线 结果
packages/cli createHostImporter(hostRoot);D5 文案按分类分支;私有 hostDeclaresDependency 改为委托 isDeclaredByHost pnpm --filter @objectstack/cli test67 files / 589 tests passed
packages/verify createHostImporter(opts.hostRoot ?? cwd);bootStack 的硬错误按分类分支 pnpm --filter @objectstack/verify test3 files / 12 tests passed
packages/qa/dogfood 探测改用新签名;reason / 硬失败文案按分类给出可执行补救 pnpm --filter @objectstack/dogfood test81 passed / 1 skipped(475 passed / 3 skipped)

dogfood 红线(#4700 刚修好的两道 enterprise 门)未被打回。 本仓里 @objectstack/organizations 是 cloud-private、整个工作区任何位置都不存在,已核实:

$ cd packages/qa/dogfood && NODE_PATH=<repo>/node_modules/.pnpm/node_modules \
  node -e "createRequire(cwd+'/package.json').resolve('@objectstack/organizations')"
resolvable in framework workspace WITH pnpm NODE_PATH -> NO (MODULE_NOT_FOUND)

也就是说这里的 skip 在改动前后都成立,且不是本改动造成的 —— 没有任何夹具因新规则从「真能跑」退回 skip,因此没有需要放宽的地方。同时新增两条用例把红线正面钉住:一个 host 装了但没声明 时探测必须报 UNAVAILABLE,且硬失败文案给的是「声明它」而不是「安装它」(对一个明明已装好的包说「去装」是不可执行的建议)。

反向验证(贴真实输出)

node.ts 的核心判断临时改回改前行为(if (declaration.declared)if (true)),其余不动。

1)单元层(packages/types)

FAIL  src/node.test.ts > declaration gates the host lookup (#4719) > REFUSES an undeclared package even though NODE_PATH resolves it
AssertionError: expected undefined to be 'undeclared' // Object.is equality
FAIL  src/node.test.ts > declaration gates the host lookup (#4719) > both failures stay classifiable as module-not-found for existing callers
AssertionError: expected undefined to be 'MODULE_NOT_FOUND' // Object.is equality
FAIL  src/node.test.ts > declaration gates the host lookup (#4719) > a directory with no package.json declares nothing, and says so
AssertionError: promise resolved "{ hoisted: true, …(1) }" instead of rejecting
 Test Files  1 failed | 5 passed (6)
      Tests  3 failed | 88 passed (91)

最后一条最直白:旧代码成功导入了那个只存在于 NODE_PATH store 里、host 从未声明的包({ hoisted: true })。

2)真实进程层(packages/cli e2e,新增用例把 pnpm shim 原样复现)

× still refuses to boot a walled posture when the app does not ship the package 4552ms
× refuses when the package is reachable only through NODE_PATH — the pnpm shim shape (#4719) 6156ms
FAIL … > refuses when the package is reachable only through NODE_PATH — the pnpm shim shape (#4719)
AssertionError: NODE_PATH got an undeclared app past the D5 wall — the #4719 defect
  ✓ Server is ready
 Test Files  1 failed (1)
      Tests  2 failed | 1 passed (3)

✓ Server is ready 就是缺陷本身:改前,一个未声明任何依赖的 app 只因为环境里多了一个 NODE_PATH,就把 walled posture 起起来了。(同批失败的第二条是部分回退的产物 —— 只回退了 node.tsserve.ts 的文案分支还在,于是未声明的 app 走进了 declared-unresolvable 那条文案;全量回退时它是通过的。)

改后:两条 e2e 文件 7/7 passed

其它验证

  • turbo typecheck --filter=types --filter=cli --filter=verify --filter=dogfood63 tasks successful
  • eslint --no-inline-config(仓库 lint 脚本的实际形态)覆盖全部改动文件 → exit 0
  • packages/types/src/node-isolation.test.tsnode: 内建白名单加入 node:fs(读 manifest 需要),并写明理由 —— 这条 pin 存在就是为了防止 subpath 隔离变成空转。

changeset

.changeset/host-declared-package-resolution.md,定级 minor(RC pre-mode 下契约收紧的先例:adr-0119-plugin-reachable-transactions.md;#4798 曾因错标 patch 被打回)。正文诚实写明了哪类部署会从假绿变红(请求了 walled posture 却没声明 @objectstack/organizations、靠 hoisting 跑着的部署)与修法(在那个 app 的 package.json 里声明并安装),并给出 createHostImporter 签名变更的 FROM → TO。


Generated by Claude Code

…'s declaration, not on NODE_PATH reachability (#4719)

`createHostRequire` returns a CJS `createRequire`, and CJS resolution honours
`NODE_PATH` (`Module.globalPaths`). Every pnpm bin shim exports NODE_PATH at
the hoisted workspace store before it execs, so any package transitively
reachable from anywhere in the workspace resolved "from the host app" —
regardless of what that app declared. ADR-0093 D5's wall therefore fired or
not according to HOW the process was launched, and its own message told
operators to "declare it in the app's package.json", the one thing the CLI
never checked.

The host lookup is now gated on the host's declaration: a name is looked up in
the host's node_modules only when it is a key of `dependencies`,
`devDependencies`, `optionalDependencies` or `peerDependencies`. Undeclared
names fall back to the importing package's own (ESM, NODE_PATH-blind)
resolution, so every framework-owned load is unchanged.

The two absences are now reported apart, with their opposite remedies:
undeclared points at declaring + installing; declared-but-unresolvable points
at the install and says the declaration is not the problem.
`hostImportFailureKind()` exposes the classification; both errors keep
`code: 'MODULE_NOT_FOUND'`.

`serve`'s private `hostDeclaresDependency` (#1597) now delegates to the shared
owner, so "declared" cannot mean two things in one file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 7:29pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/dogfood, @objectstack/types, @objectstack/verify.

24 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via packages/qa/dogfood)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa/dogfood)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/types)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/verify)
  • content/docs/releases/v15.mdx (via @objectstack/verify)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 3, 2026 19:31
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit 02dc076 Aug 3, 2026
25 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-4719-host-declared-resolution branch August 3, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

createHostRequire 认 NODE_PATH,于是 #4699 立下的「host app 必须自己声明」在 pnpm 工作区里根本没被强制

2 participants