DSH / Atlas
2026-07-28implementedarchitecturearchived 2026-08-07

Native TypeScript source launch for dsh

dsh 原生 TypeScript 源码启动

The `dsh` source entry point originally used `tsx` to run `apps/cli/src/bin.ts`, with the same third-party loader implicitly handling both TypeScript transformation and the root tsconfig's `paths` resolution. With Node handling TypeScript natively, it does not apply tsconfig path mappings; resolving through package exports would instead mix potentially stale or nonexistent `lib/` artifacts into the source launch. Nod

English

Problem

The dsh source entry point originally used tsx to run apps/cli/src/bin.ts, with the same third-party loader implicitly handling both TypeScript transformation and the root tsconfig's paths resolution. With Node handling TypeScript natively, it does not apply tsconfig path mappings; resolving through package exports would instead mix potentially stale or nonexistent lib/ artifacts into the source launch.

Node's transform also does not perform type analysis. A type imported through an ordinary value import remains a runtime ESM request, and TypeScript's export = becomes a CommonJS assignment rather than an ESM default export. The source graph therefore has to use explicit type-only imports and native ESM exports; a resolve hook cannot repair incompatible source syntax.

Cordis configuration introduces a separate resolution boundary. Bare plugins in cordis.yml do not pass through TypeScript import analysis, so their resolver manifest may omit the required dependencies. The Cordis Loader logs plugin import errors and leaves an entry without a fiber, but does not fail startup itself; a typo in the configuration can therefore produce an incomplete application with exit code 0.

Decision

The dsh TUI, Web, and headless source launches use node --experimental-transform-types; Node performs TypeScript transformation without loading tsx or esbuild. bin/dsh, the root-level dsh/TUI/Web demos, and Code Mode TUI enter the same apps/cli/src/bin.ts launch chain. Test and E2E launchers retain their existing strategies, and the built lib/bin.js continues to run under ordinary Node.

scripts/tspath-loader.ts registers only a module resolve hook. It uses TSX_TSCONFIG_PATH when set (resolving relative values from the invoking cwd) and otherwise reads the root tsconfig.json; TsconfigPathsResolver follows that config's extends chain through the repository's existing TypeScript development tool, selects exact or wildcard paths entries according to tsconfig rules, and maps matching workspace bare specifiers to .ts/.mts/.cts source files or directory index files. Node remains solely responsible for code transformation. The source-only loader is not part of the built CLI and apps/cli does not declare typescript as a runtime dependency.

Source imports are redirected only when the target package is either the nearest package manifest's own name or one of that manifest's declared runtime dependencies. The Cordis Loader uses the configuration directory URL as the import parent; the resolver then searches upward for the workspace manifest that declares the plugin, so dependency ownership for the shipped apps/cli/config/base.cordis.yml plus its surface overlay lies with apps/cli/package.json. Specifiers that do not match tsconfig paths, refer to undeclared dependencies, or are not bare all fall back to Node's default resolution.

verify-cordis-config performs a one-way completeness check on the resolver manifest: every bare plugin package in a configuration must appear in the corresponding manifest's dependencies, while the manifest may contain extra dependencies not referenced by that configuration. The root AGENTS.md makes updating the configuration and dependencies together a standing rule.

After the Loader settles, the shared dsh-app-boot checks every enabled entry that has no fiber and rejects startup with plugin(s) failed to load: ...; Cordis startup failed because these plugin(s) could not be resolved, listing all failed plugins. This diagnostic lives at the app layer and does not change the vendored Loader's startup behavior.

Node-compatible TypeScript is part of this source-launch contract. Vendored Cordis, Loader, Include, HMR, and Schemastery mark erased imports with import type. Schemastery uses a native ESM default export and declares type: module; its .mjs and .cjs build outputs retain the existing ESM-default and callable-require() behavior. These divergences are recorded in vendor/README.md; no runtime behavior is added to the vendored frameworks.

Alternatives considered

Continue using tsx. Rejected because tsx/esbuild would continue to own TypeScript transformation, so this launch chain could not prove that Node's native transformation works.

Load the built lib/ through package exports from the source entry point. Rejected because this would mix the source plane with the artifact plane; a zero-build development launch could read stale artifacts or fail outright.

Apply the root tsconfig paths unconditionally. Rejected because this would allow undeclared cross-package imports and Cordis plugins to keep resolving, hiding mismatches between the manifest and the actual runtime graph.

Transform imports inside the custom loader. Rejected because type-aware source rewriting would reintroduce a compiler-style transform and make the loader, rather than Node, responsible for TypeScript execution. Making the checked-in source Node-compatible keeps the launch boundary explicit.

Consequences

  • TUI/headless retain a zero-build source loop, while Web still builds its frontend artifacts before starting the CLI source entry. TypeScript syntax passes only through Node's native transform; the URL-only loader uses the checkout's root development dependencies and adds no CLI runtime dependency.
  • Workspace package imports and Cordis configuration dependencies must both be declared explicitly in the resolver manifest; the static gate prevents configuration from landing before its dependencies, while extra dependencies are not errors.
  • Plugin import failures no longer leave an incomplete application with exit code 0; the final error identifies both the Cordis startup failure and the specific plugin names, while the Loader's original error remains earlier in the logs.
  • Vendored source in the CLI graph must remain compatible with Node's transform-types module semantics; the local-modification log makes the upstream sync obligation explicit.
  • CI's lib mode, test/E2E launchers, and other example launchers retain their existing strategies; this native source loader covers only the dsh CLI application chain.

中文

问题

dsh 源码入口原本使用 tsx 运行 apps/cli/src/bin.ts,TypeScript 转换和根 tsconfig 的 paths 解析都由同一个第三方 loader 隐式处理。改由 Node 原生处理 TypeScript 后,Node 不会应用 tsconfig 路径映射;如果改为通过包导出解析,源码启动会混入可能陈旧或不存在的 lib/ 产物。

Node 的转换也不执行类型分析。通过普通值 import 导入的类型会保留为运行时 ESM 请求,而 TypeScript 的 export = 会转换成 CommonJS 赋值,而不是 ESM default export。因此,源码图必须显式使用仅类型导入和原生 ESM 导出;resolve hook 无法修复不兼容的源码语法。

Cordis 配置还引入了另一条解析边界。cordis.yml 中的 bare plugin 不经过 TypeScript import 分析,其解析方的 manifest(元数据清单)可能漏掉所需依赖。Cordis Loader 会记录插件 import 错误,并留下没有 fiber 的 entry,但不会让启动本身失败;配置中的拼写错误因此可能得到退出码为 0 的残缺应用。

决策

dsh 的 TUI、Web 和无头源码启动使用 node --experimental-transform-types,由 Node 完成 TypeScript 转换,不加载 tsx 或 esbuild。bin/dsh、根级 dsh/TUI/Web demo 以及 Code Mode TUI 都进入同一条 apps/cli/src/bin.ts 启动链路。测试与 e2e 启动器保留各自现有策略,构建后的 lib/bin.js 继续由普通 Node 运行。

scripts/tspath-loader.ts 只注册一个模块解析钩子。设置 TSX_TSCONFIG_PATH 时,它会使用该路径(相对路径从调用方的 cwd 解析),否则读取根 tsconfig.jsonTsconfigPathsResolver 使用仓库已有的 TypeScript 开发工具沿该配置的 extends 链解析,按 tsconfig 规则选择精确或 wildcard paths 条目,并将命中的 workspace bare specifier 映射到 .ts/.mts/.cts 源文件或目录 index 文件。代码转换始终只由 Node 负责。该源码专用 loader 不属于构建后的 CLI,apps/cli 也不会把 typescript 声明为运行时依赖。

只有当目标包是最近一层包 manifest 的自身名称或该 manifest 已声明的运行时依赖时,源码 import 才会重定向。Cordis Loader 使用配置目录 URL 作为 import parent;此时 resolver 会向上查找声明该插件的 workspace manifest。因此,已交付的 apps/cli/config/base.cordis.yml 及其界面覆盖层所需依赖由 apps/cli/package.json 持有。未命中 tsconfig paths、引用未声明依赖或不是 bare specifier 的说明符全部交回 Node 默认解析。

verify-cordis-config 对该解析方 manifest 执行单向完整性检查:配置中的每个 bare plugin package 都必须出现在对应 manifest 的 dependencies 中,manifest 可以包含该配置未引用的额外依赖。根 AGENTS.md 将同步更新配置和依赖定为常驻规则。

Loader 完全停稳后,共享的 dsh-app-boot 会检查每个已启用但没有 fiber 的 entry,并拒绝启动,报错为 plugin(s) failed to load: ...; Cordis startup failed because these plugin(s) could not be resolved,同时列出全部加载失败的插件。该诊断位于应用层,不改变 vendor 中 Loader 的启动行为。

Node-compatible TypeScript 是这项源码启动契约的一部分。vendor 中的 Cordis、Loader、Include、HMR(热模块替换)和 Schemastery 使用 import type 标记会被擦除的导入。Schemastery 使用原生 ESM default export 并声明 type: module;其 .mjs.cjs 构建产物分别保留现有的 ESM default export 行为和 require() 返回可调用值的行为。这些差异记录在 vendor/README.md 中;没有为 vendor 中的框架新增运行时行为。

曾考虑的替代方案

继续使用 tsx 不采用,因为 tsx/esbuild 会继续负责 TypeScript 转换,本启动链路无法因此证明 Node 原生转换可用。

让源码入口通过包导出加载构建后的 lib/ 不采用,因为这会混合 source plane 与 artifact plane;无需预先构建的开发启动可能读取陈旧产物或直接失败。

无条件应用根 tsconfig paths 不采用,因为这会让未声明的跨包 import 和 Cordis 插件继续成功解析,从而掩盖 manifest 与实际运行图之间的不一致。

在自定义 loader 内转换 import。 不采用,因为感知类型的源码改写会重新引入编译器式转换,并让 loader 而非 Node 负责执行 TypeScript。使签入仓库的源码兼容 Node,可以让启动边界保持显式。

后果

  • TUI/无头界面保留零构建源码回路,Web 仍会在启动 CLI 源码入口前构建前端产物。TypeScript 语法只经过 Node 原生转换;仅处理 URL 的 loader 使用 checkout 根目录的开发依赖,不增加 CLI 运行时依赖。
  • workspace package import 和 Cordis 配置依赖都必须在解析方 manifest 中明确声明;静态门禁防止配置先于依赖落地,额外依赖不构成错误。
  • 插件 import 失败不再留下退出码为 0 的残缺应用;最终错误同时说明 Cordis 启动失败及具体插件名,Loader 的原始错误仍会保留在更早的日志中。
  • CLI 源码图中的 vendor 源码必须与 Node 的 transform-types 模块语义兼容;本地修改记录明确了上游同步义务。
  • CI 的 lib 模式、测试/e2e 启动器和其他示例启动器保留各自现有策略;该原生源码 loader 只覆盖 dsh CLI 应用链路。