Skip to content

fix: prevent pnpm dev startup failures (getService crash, host config mis-wrap, ESM exports)#905

Merged
hotlong merged 4 commits into
mainfrom
copilot/fix-pnpm-dev-start-issue
Mar 11, 2026
Merged

fix: prevent pnpm dev startup failures (getService crash, host config mis-wrap, ESM exports)#905
hotlong merged 4 commits into
mainfrom
copilot/fix-pnpm-dev-start-issue

Conversation

Copilot AI commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

pnpm dev fails with plugin.app.com.example.crm failed to start - rollback complete. Three interrelated issues cause the cascade.

1. AppPlugin.start() crashes on missing services

ctx.getService() throws when a service is not registered, but AppPlugin assumed it returns undefined:

// Before — unhandled throw when i18n service not registered
const i18nService = ctx.getService('i18n') as II18nService | undefined;
if (!i18nService) { return; } // never reached

// After — handles both throw and undefined
let i18nService: II18nService | undefined;
try {
    i18nService = ctx.getService('i18n') as II18nService;
} catch {
    // Service not registered — handled below
}
if (!i18nService) { return; }

Same fix applied to getService('objectql') in start().

2. CLI wraps host configs with redundant AppPlugin

serve.ts auto-wraps any config containing manifest as an AppPlugin. The root dev workspace config has manifest and a plugins[] array of instantiated plugins — wrapping it causes double-registration and plugin.app.dev-workspace failed to start.

  • Added isHostConfig() in packages/cli/src/utils/plugin-detection.ts — detects configs whose plugins array contains objects with init methods (i.e., already-instantiated plugins)
  • serve.ts skips auto-wrap when isHostConfig(config) is true

3. plugin-auth CJS→ESM interop warning

plugin-auth/package.json lacked module and exports fields. Node resolved the CJS build, which require()'d the ESM-only better-auth, triggering ExperimentalWarning.

  • Added conditional exports map preferring .mjs for import(), .js for require()

Tests

  • 2 new AppPlugin tests covering getService throwing (15/15 pass)
  • 7 new host config detection tests
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 时,服务器启动失败并退出:

  Loading objectstack.config.ts...
(node:73691) ExperimentalWarning: CommonJS module .../plugin-auth/dist/index.js is loading ES Module .../better-auth/dist/index.mjs using require().

  ✗ Plugin plugin.app.dev-workspace failed to start - rollback complete
 ELIFECYCLE  Command failed with exit code 1.

经过对启动流程的完整追踪分析,发现这是 三个相互关联的问题 共同导致的,需要整体修复。


问题 1:CLI serve 命令对根聚合配置的误判(核心触发点)

分析

serve.ts 第 192-201 行存在一个有缺陷的自动注册逻辑:

// packages/cli/src/commands/serve.ts L192-201
if (config.objects || config.manifest || config.apps) {
  try {
     const { AppPlugin } = await import('@objectstack/runtime');
     await kernel.use(new AppPlugin(config));  // ← 问题所在
     trackPlugin('App');
  } catch (e: any) {
     // silent
  }
}

根配置 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-workspacestart() 方法尝试:

  1. 获取 ObjectQL 引擎 → 可能失败(因为重复注册冲突)
  2. 调用 runtime.onEnable() → root config 没有 onEnable
  3. 处理 seed data → root config 没有 data
  4. 但更严重的是,init 阶段的 registerService('app.dev-workspace', ...) 传入了整个 config 对象(含 plugins 数组实例),可能导致序列化或处理错误

同时,同一批 plugins 被注册了两次:一次通过 CLI 的 plugins.forEach(kernel.use) 循环,一次通过 AppPlugin(config) 的服务注册。

修复方案

serve.ts 中增加检测逻辑:当 config 已包含 plugins 数组(且数组中存在已实例化的 Plugin 对象),则跳过 auto-AppPlugin 包装。

// 检测是否为聚合器配置(已有 plugins 数组含实例化插件)
const isHostConfig = Array.isArray(config.plugins) && 
  config.plugins.some((p: any) => typeof p?.init === 'function');

if (!isHostConfig && (config.objects || config.manifest || config.apps)) {
  // 只为纯 App Bundle 创建 AppPlugin 包装
  const { AppPlugin } = await import('@objectstack/runtime');
  await kernel.use(new AppPlugin(config));
  trackPlugin('App');
}

问题 2:plugin-auth CJS→ESM 互操作问题(连锁放大器)

分析

ExperimentalWarning: CommonJS module .../plugin-auth/dist/index.js is loading 
ES Module .../better-auth/dist/index.mjs using require()

根因链条:

层级 配置 结果
Root package.json "type": "module" 默认 CJS
plugin-auth/package.json "main": "dist/index.js", 无 "type" 继承 CJS
共享 tsup.config.ts format: ['esm', 'cjs'] 输出 .js(CJS) + .mjs(ESM)
plugin-auth/package.json "main": "dist/index.js" Node.js 加载 CJS 版本
better-auth 只导出 ESM (.mjs) CJS 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 形成了阻塞链

  1. 即使修复了问题 1 和 2,AuthPlugin 成功启动后
  2. 用户注册/登录时仍会遇到 [Bug] ObjectQL Adapter field name mapping error: camelCase vs snake_case causes 'Credential account not found' on sign-in #893 的字段映射错误
  3. objectql-adapter.ts 需要实现字段名自动转换

本 Issue 修复后,#893 的修复才能被验证,因为服务器必须先能成功启动。


影响范围

文件 修改类型
packages/cli/src/commands/serve.ts 修复自动 AppPlugin 包装逻辑
packages/plugins/plugin-auth/package.json 添加 ESM exports 字段
packages/cli/src/commands/serve.ts (测试) 添加聚合器配置测试
CHANGELOG.md 记录修复

验收标准

  • pnpm dev 能够成功启动,不再出现 plugin.app.dev-workspace failed to start 错误
  • ExperimentalWarning: CommonJS module ... is loading ES Module 警告消除
  • Root config 中的 plugins 不会被重复注册
  • examples/app-host/objectstack.config.ts 等聚合器配置也能正常工作
  • 纯 App Bundle 配置(无 plugins 数组)的 auto-AppPlugin 包装功能不受影响
  • 所��现有测试通过:pnpm test
  • 更新 CHANGELOG.md

相关


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel

vercel Bot commented Mar 11, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Mar 11, 2026 6:43am
spec Ready Ready Preview, Comment Mar 11, 2026 6:43am

Request Review

Copilot AI and others added 2 commits March 11, 2026 05:18
…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>
Copilot AI changed the title [WIP] Fix pnpm dev startup failure due to plugin issues fix: prevent host config AppPlugin mis-wrap and add plugin-auth ESM exports Mar 11, 2026
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>
Copilot AI changed the title fix: prevent host config AppPlugin mis-wrap and add plugin-auth ESM exports fix: prevent pnpm dev startup failures (getService crash, host config mis-wrap, ESM exports) Mar 11, 2026
@hotlong
hotlong marked this pull request as ready for review March 11, 2026 06:47
Copilot AI review requested due to automatic review settings March 11, 2026 06:47
@hotlong
hotlong merged commit 808bade into main Mar 11, 2026
5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AppPlugin to handle missing kernel services (getService throwing) without crashing, plus add targeted tests.
  • Add CLI host/aggregator config detection to avoid auto-wrapping configs that already contain instantiated plugins.
  • Add module + conditional exports to @objectstack/plugin-auth to prefer ESM on import() and CJS on require().

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.

Comment on lines +11 to +13
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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"

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +69
} catch {
// Service not registered — handled below

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
} 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

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +216
} catch {
// Service not registered — handled below

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
} 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;
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 [Bug] pnpm dev 启动失败:plugin.app.dev-workspace failed to start — 三重问题合并修复

3 participants