fix: prevent pnpm dev startup failures (getService crash, host config mis-wrap, ESM exports)#905
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…xports - serve.ts: detect host/aggregator configs (plugins array with instantiated Plugin objects) and skip auto-AppPlugin wrapping to prevent duplicate registration and plugin.app.dev-workspace startup failure - plugin-auth/package.json: add module and exports fields for proper ESM resolution, eliminating CJS→ESM interop warnings with better-auth - Add test coverage for host config detection logic - Update CHANGELOG.md Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Address code review feedback: move host config detection logic into a dedicated, dependency-free utility module so serve.ts and tests share the same implementation. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
ctx.getService() throws when a service is not registered, but
AppPlugin.start() and loadTranslations() assumed it returned undefined.
This caused plugin.app.com.example.crm to crash during startup when
the i18n service was not registered in the dev workspace.
Wrap both getService('objectql') and getService('i18n') calls in
try/catch blocks while preserving the existing null-check fallback
for backward compatibility with mocked contexts.
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes pnpm dev startup failures by making runtime service lookups resilient, preventing the CLI from double-wrapping host configs as an AppPlugin, and improving @objectstack/plugin-auth ESM/CJS resolution.
Changes:
- Update
AppPluginto handle missing kernel services (getServicethrowing) without crashing, plus add targeted tests. - Add CLI host/aggregator config detection to avoid auto-wrapping configs that already contain instantiated plugins.
- Add
module+ conditionalexportsto@objectstack/plugin-authto prefer ESM onimport()and CJS onrequire().
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/runtime/src/app-plugin.ts | Wraps ctx.getService() calls for objectql/i18n to prevent startup crashes when services are missing. |
| packages/runtime/src/app-plugin.test.ts | Adds tests covering getService throwing for objectql and i18n. |
| packages/cli/src/utils/plugin-detection.ts | Introduces isHostConfig() to identify configs that already contain instantiated plugins. |
| packages/cli/src/commands/serve.ts | Skips auto-AppPlugin wrapping when isHostConfig(config) is true. |
| packages/cli/test/serve-host-config.test.ts | Adds unit tests for host config detection behavior. |
| packages/plugins/plugin-auth/package.json | Adds module and conditional exports for better ESM/CJS interop. |
| CHANGELOG.md | Documents the fixes and related runtime/adapter behavior changes. |
| "import": "./dist/index.mjs", | ||
| "require": "./dist/index.js", | ||
| "types": "./dist/index.d.ts" |
There was a problem hiding this comment.
The new conditional exports map has types after import/require. In this repo’s packages, types is consistently listed first in exports["."] (e.g. @objectstack/core, @objectstack/runtime), which avoids tooling picking the runtime entry when resolving typings. Reorder the keys to put types first to match existing package conventions.
| "import": "./dist/index.mjs", | |
| "require": "./dist/index.js", | |
| "types": "./dist/index.d.ts" | |
| "types": "./dist/index.d.ts", | |
| "import": "./dist/index.mjs", | |
| "require": "./dist/index.js" |
| } catch { | ||
| // Service not registered — handled below |
There was a problem hiding this comment.
The new try/catch around ctx.getService('objectql') swallows all getService errors. In ObjectKernel, getService deliberately re-throws non-"not found" errors (e.g. "Service 'x' is async - use await" or factory execution failures). Swallowing those here can mask real startup failures and incorrectly downgrade them to "service not found". Catch the error, only treat the specific "[Kernel] Service 'objectql' not found" case as optional, and rethrow (or log+rethrow) other errors.
| } catch { | |
| // Service not registered — handled below | |
| } catch (error: unknown) { | |
| const message = error instanceof Error ? error.message : String(error); | |
| if (message !== "[Kernel] Service 'objectql' not found") { | |
| // Re-throw non-"not found" errors so real startup failures are not masked | |
| throw error; | |
| } | |
| // "[Kernel] Service 'objectql' not found" — handled below via !ql check |
| } catch { | ||
| // Service not registered — handled below |
There was a problem hiding this comment.
Same issue as objectql: this try/catch around ctx.getService('i18n') currently swallows all errors, including async-service errors or factory failures that the kernel intentionally rethrows. Narrow the catch to only ignore the explicit "[Kernel] Service 'i18n' not found" case (or equivalent not-found sentinel) and rethrow other errors so real i18n startup failures aren’t silently skipped.
| } catch { | |
| // Service not registered — handled below | |
| } catch (err: unknown) { | |
| if ( | |
| err instanceof Error && | |
| err.message && | |
| err.message.includes("Service 'i18n' not found") | |
| ) { | |
| // Service not registered — handled below | |
| } else { | |
| throw err; | |
| } |
pnpm devfails withplugin.app.com.example.crm failed to start - rollback complete. Three interrelated issues cause the cascade.1.
AppPlugin.start()crashes on missing servicesctx.getService()throws when a service is not registered, butAppPluginassumed it returnsundefined:Same fix applied to
getService('objectql')instart().2. CLI wraps host configs with redundant
AppPluginserve.tsauto-wraps any config containingmanifestas anAppPlugin. The root dev workspace config hasmanifestand aplugins[]array of instantiated plugins — wrapping it causes double-registration andplugin.app.dev-workspace failed to start.isHostConfig()inpackages/cli/src/utils/plugin-detection.ts— detects configs whosepluginsarray contains objects withinitmethods (i.e., already-instantiated plugins)serve.tsskips auto-wrap whenisHostConfig(config)is true3.
plugin-authCJS→ESM interop warningplugin-auth/package.jsonlackedmoduleandexportsfields. Node resolved the CJS build, whichrequire()'d the ESM-onlybetter-auth, triggeringExperimentalWarning.exportsmap preferring.mjsforimport(),.jsforrequire()Tests
getServicethrowing (15/15 pass)Original prompt
This section details on the original issue you should resolve
<issue_title>🐛 [Bug] pnpm dev 启动失败:plugin.app.dev-workspace failed to start — 三重问题合并修复</issue_title>
<issue_description>## 概述
运行
pnpm dev时,服务器启动失败并退出:经过对启动流程的完整追踪分析,发现这是 三个相互关联的问题 共同导致的,需要整体修复。
问题 1:CLI serve 命令对根聚合配置的误判(核心触发点)
分析
serve.ts第 192-201 行存在一个有缺陷的自动注册逻辑:根配置
objectstack.config.ts含有manifest字段,因此该条件为true。CLI 将**整个 root config(包含 plugins 数组)**包装为一个AppPlugin(config),生成名为plugin.app.dev-workspace的插件。但是,root config 是一个聚合器/宿主配置(Host Config),它的
plugins数组中已经包含了实例化的插件对象(ObjectQLPlugin、DriverPlugin、AuthPlugin、AppPlugin(CrmApp) 等)。它不是一个普通的 App Bundle,不应被 AppPlugin 包装。后果
在
kernel.bootstrap()的 Phase 2 (Start) 中,plugin.app.dev-workspace的start()方法尝试:runtime.onEnable()→ root config 没有 onEnableregisterService('app.dev-workspace', ...)传入了整个 config 对象(含 plugins 数组实例),可能导致序列化或处理错误同时,同一批 plugins 被注册了两次:一次通过 CLI 的
plugins.forEach(kernel.use)循环,一次通过AppPlugin(config)的服务注册。修复方案
在
serve.ts中增加检测逻辑:当 config 已包含plugins数组(且数组中存在已实例化的 Plugin 对象),则跳过 auto-AppPlugin 包装。问题 2:plugin-auth CJS→ESM 互操作问题(连锁放大器)
分析
根因链条:
package.json"type": "module"plugin-auth/package.json"main": "dist/index.js", 无"type"tsup.config.tsformat: ['esm', 'cjs'].js(CJS) +.mjs(ESM)plugin-auth/package.json"main": "dist/index.js"better-auth.mjs)require()加载 ESM → 实验性警告plugin-auth通过 tsup 构建输出了 CJS 和 ESM 双格式,但package.json中"main"指向.js(CJS),没有"module"或"exports"字段。Node.js 默认加载 CJS 版本,当它require('better-auth')时触发 CJS→ESM 互操作,导致实验性警告,在某些 Node.js 版本下可能直接抛出异常。修复方案
更新
plugin-auth/package.json添加exports字段,优先使用 ESM:{ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.js", "types": "./dist/index.d.ts" } } }这样
serve.ts通过await import('@objectstack/plugin-auth')加载时,Node.js 会选择.mjs(ESM)版本,避免 CJS→ESM 互操作问题。问题 3:与 #893 的关联(better-auth 字段名映射)
Issue #893 报告了 better-auth 的 camelCase↔snake_case 字段映射问题导致登录失败。这个问题与本 Issue 形成了阻塞链:
objectql-adapter.ts需要实现字段名自动转换本 Issue 修复后,#893 的修复才能被验证,因为服务器必须先能成功启动。
影响范围
packages/cli/src/commands/serve.tspackages/plugins/plugin-auth/package.jsonpackages/cli/src/commands/serve.ts(测试)CHANGELOG.md验收标准
pnpm dev能够成功启动,不再出现plugin.app.dev-workspace failed to start错误ExperimentalWarning: CommonJS module ... is loading ES Module警告消除examples/app-host/objectstack.config.ts等聚合器配置也能正常工作pnpm test相关
packages/cli/src/commands/serve.tspackages/runtime/src/app-plugin.tspackages/core/src/kernel.ts✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.