From aebf9c863b4a19bf7a7b4861d4f31dbfab73adfc Mon Sep 17 00:00:00 2001 From: kingwl Date: Wed, 29 Jul 2026 13:15:24 +0800 Subject: [PATCH] feat(cli): launch dsh source through the tsx ESM hook Node 26.0.0 removed --experimental-transform-types, so the native source-launch chain cannot start anywhere on that line, and strip-only mode rejects the vendored syntax (parameter properties, decorators, runtime enums/namespaces). Switch bin/dsh, the root dsh/demo:tui/ demo:web scripts, and the Code Mode TUI overlay to node --import tsx/esm: one launch vector across the whole engines range, ~0.4s faster than the full tsx default (the CJS hook stays off; the graph is ESM-only). Delete scripts/tspath-loader.ts and apps/cli/src/tsconfig-paths-loader.ts: tsx owns both transformation and tsconfig paths projection. Add dsh-source-launch-smoke to the node-compat gates so the 22.19/26 matrix executes the real launch vector; no CI job did, which is how the Node 26 breakage shipped silently. Supersedes the native-TypeScript-source-launch Agent Note (new note records the profiling evidence and rejected alternatives). --- ...-native-typescript-source-launch.i18n.yaml | 4 +- ...-28-dsh-native-typescript-source-launch.md | 2 + ...-dsh-native-typescript-source-launch.zh.md | 2 + ...-07-29-dsh-source-launch-tsx-esm.i18n.yaml | 6 + .../2026-07-29-dsh-source-launch-tsx-esm.md | 38 +++ ...2026-07-29-dsh-source-launch-tsx-esm.zh.md | 38 +++ AGENTS.md | 2 +- apps/cli/README.i18n.yaml | 4 +- apps/cli/README.md | 2 +- apps/cli/README.zh.md | 2 +- apps/cli/src/tsconfig-paths-loader.ts | 216 ------------------ apps/cli/tests/source-launch.compat.spec.ts | 36 +++ apps/cli/tests/tsconfig-paths-loader.spec.ts | 180 --------------- bin/dsh | 17 +- package.json | 6 +- scripts/demo-code-mode.mjs | 3 +- scripts/run-gates.ts | 5 + scripts/tspath-loader.ts | 14 -- 18 files changed, 148 insertions(+), 429 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md create mode 100644 .agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md delete mode 100644 apps/cli/src/tsconfig-paths-loader.ts create mode 100644 apps/cli/tests/source-launch.compat.spec.ts delete mode 100644 apps/cli/tests/tsconfig-paths-loader.spec.ts delete mode 100644 scripts/tspath-loader.ts diff --git a/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.i18n.yaml index a96c124e92..f3a3226c11 100644 --- a/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.md -2026-07-28-dsh-native-typescript-source-launch.md: 019389f3e5e9229f4359bbd58c95dbb2f14eb24b -2026-07-28-dsh-native-typescript-source-launch.zh.md: 2cfff25d228e67ac85a9bc9087fa09ddb64213a0 +2026-07-28-dsh-native-typescript-source-launch.md: 1ba1dd2663038ad7c49af71f8b428245f7fa3e2b +2026-07-28-dsh-native-typescript-source-launch.zh.md: 02f84f34820469ad9e810ae17d79d3fe12b0cd4c diff --git a/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.md b/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.md index 019389f3e5..1ba1dd2663 100644 --- a/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.md +++ b/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.md @@ -4,6 +4,8 @@ Status: implemented English | [中文](2026-07-28-dsh-native-typescript-source-launch.zh.md) +> The Node-native launch vector is superseded by [dsh source launch through the tsx ESM hook](2026-07-29-dsh-source-launch-tsx-esm.md): Node 26.0.0 removed `--experimental-transform-types`, and the paths loader described here is deleted. The Cordis-config declaration gate (`verify-cordis-config`), the app-boot fail-loud plugin diagnostic, and the vendored `import type` marks remain current. + ## 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. diff --git a/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.zh.md b/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.zh.md index 2cfff25d22..02f84f3482 100644 --- a/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.zh.md @@ -4,6 +4,8 @@ Status: implemented [English](2026-07-28-dsh-native-typescript-source-launch.md) | 中文 +> Node 原生启动向量已被 [dsh 通过 tsx ESM hook 源码启动](2026-07-29-dsh-source-launch-tsx-esm.md) 取代:Node 26.0.0 移除了 `--experimental-transform-types`,本文描述的 paths loader 已删除。Cordis 配置声明门禁(`verify-cordis-config`)、app-boot 的 fail-loud 插件诊断以及 vendor 中的 `import type` 标注仍然有效。 + ## 问题 `dsh` 源码入口原本使用 `tsx` 运行 `apps/cli/src/bin.ts`,TypeScript 转换和根 tsconfig 的 `paths` 解析都由同一个第三方 loader 隐式处理。改由 Node 原生处理 TypeScript 后,Node 不会应用 tsconfig 路径映射;如果改为通过包导出解析,源码启动会混入可能陈旧或不存在的 `lib/` 产物。 diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml new file mode 100644 index 0000000000..ab618103c2 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md +2026-07-29-dsh-source-launch-tsx-esm.md: 93fbb248b45efde37d5fbdb1ec4b812ab3332088 +2026-07-29-dsh-source-launch-tsx-esm.zh.md: 48f410bd846e5808cc95180279348a0ac5ba1c95 diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md new file mode 100644 index 0000000000..93fbb248b4 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md @@ -0,0 +1,38 @@ +# Agent Note: dsh source launch through the tsx ESM hook + +Status: implemented + +English | [中文](2026-07-29-dsh-source-launch-tsx-esm.zh.md) + +> Supersedes [native TypeScript source launch](2026-07-28-dsh-native-typescript-source-launch.md): Node removed the capability that decision was built on. + +## Problem + +The [native source-launch decision](2026-07-28-dsh-native-typescript-source-launch.md) ran `apps/cli/src/bin.ts` under `node --experimental-transform-types` with a resolve-only paths loader, so Node owned TypeScript transformation. Node 26.0.0 removed `--experimental-transform-types` (the process rejects the flag with `bad option`), keeping only strip mode, and strip mode rejects syntax this source graph requires: vendored Cordis parameter properties (`constructor(private ctx: Context)`), the `@Inject` decorators in `vendor/hmr`, and runtime enums/namespaces throughout `vendor/` and `packages/workflow`. The repository's engines range (`^22.19.0 || >=24.0.0`) includes Node 26, so the native launch chain could not start at all there — and no CI job executed the real launch vector, so the incompatibility shipped silently. + +Startup latency also mattered: the off-thread `module.register()` hooks worker serialized every resolution across threads (~440ms of `makeSyncRequest` wait during TUI boot), and the full tsx default (`--import tsx`) pays ~0.4s in its CJS hook's resolution amplification. + +## Decision + +The `dsh` TUI, Web, and headless source launches run `node --import tsx/esm`: tsx's ESM-only hook owns both TypeScript transformation and tsconfig `paths` projection. `bin/dsh`, the root `dsh`/`demo:tui`/`demo:web` scripts, and the Code Mode TUI overlay use the same vector; `bin/dsh` references the hook and tsconfig by absolute checkout paths (bare `tsx/esm` does not resolve from an arbitrary cwd) and pins `TSX_TSCONFIG_PATH` to the root tsconfig. The CJS hook stays off because the CLI source graph is ESM-only; measured TUI time-to-banner is ~0.7s versus ~1.1s under the full tsx default and ~0.75s under the removed native chain. + +`scripts/tspath-loader.ts` and `apps/cli/src/tsconfig-paths-loader.ts` are deleted. With them went the loader's runtime rule of mapping a workspace import only for declared runtime dependencies — tsx applies the `paths` map unconditionally. Declaration completeness now rests on the static gates alone: `verify-cordis-config` for configured bare plugins, and workspace constraints for manifests. (That runtime rule found real bugs: `dsh-plan-mode` and `dsh-tool-tasks` imported `@deepseek-ai/dsh-llm` while declaring it only in devDependencies; fixed alongside this change.) + +The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`apps/cli/tests/source-launch.compat.spec.ts`): a keyless piped-stdio launch of the exact production vector asserting the non-zero-exit TTY refusal. Any future Node change to module hooks or TypeScript handling turns this gate red instead of breaking developers' `pnpm dsh`. + +## Alternatives considered + +**Keep the native chain on Node ≤25 and branch by version.** Rejected: two transformation semantics (amaro versus esbuild) diverge on edge syntax, the launcher grows version probing, and the node-compat matrix must cover both paths — heavy maintenance for an experimental flag that already changed under us. amaro also rejects the `@Inject` decorators `vendor/hmr` uses, so the native path could not boot the shipped default TUI config anyway. + +**Make the source graph erasable-only so Node 26 strip mode accepts it.** Rejected: parameter properties and value namespaces pervade vendored Cordis/cosmokit/loader/schemastery; rewriting them is unbounded churn re-applied on every vendor sync. + +**A repo-owned in-thread loader (`module.registerHooks()` + esbuild or `@swc/core` transform).** Rejected for now: prototypes measured ~0.45s (esbuild path untested end-to-end; SWC breaks on `vendor/hmr`'s decorator + namespace merge in both decorator modes), but it means owning transform correctness and a resolve hook that tsx already provides. Revisit only if the ~0.3s gap becomes a real cost; the profiling evidence lives in the PR discussion. + +**Run built `lib/` for Node 26 and keep native for 24.** Rejected: loses the zero-build development loop on the newest Node line and mixes source and artifact planes. + +## Consequences + +- One launch vector across the whole engines range, including future Node lines that change native TypeScript support; the smoke gate enforces it per matrix line. +- TypeScript transformation is delegated to tsx/esbuild again, reversing the prior note's goal of proving Node-native transformation; that goal is unreachable while vendored sources use non-erasable syntax and Node ships no transform mode. +- The runtime declared-dependency enforcement in source launches is gone; undeclared workspace imports now surface only through static gates or built-mode resolution failures. +- Startup improves ~0.4s over the full tsx default (`demo:headless` and ACP keep `--import tsx`; their graphs were not audited for CJS-hook dependence and their launch latency is not on the interactive path). diff --git a/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md new file mode 100644 index 0000000000..48f410bd84 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.zh.md @@ -0,0 +1,38 @@ +# Agent Note: dsh 通过 tsx ESM hook 源码启动 + +Status: implemented + +[English](2026-07-29-dsh-source-launch-tsx-esm.md) | 中文 + +> 取代[原生 TypeScript 源码启动](2026-07-28-dsh-native-typescript-source-launch.md):Node 移除了该决策所依赖的能力。 + +## 问题 + +[原生源码启动决策](2026-07-28-dsh-native-typescript-source-launch.md)让 `apps/cli/src/bin.ts` 在 `node --experimental-transform-types` 下运行,配合一个只做解析的 paths loader,由 Node 负责 TypeScript 转换。Node 26.0.0 移除了 `--experimental-transform-types`(进程以 `bad option` 拒绝该 flag),只保留 strip 模式,而 strip 模式无法接受这个源码图必需的语法:vendor Cordis 中的参数属性(`constructor(private ctx: Context)`)、`vendor/hmr` 中的 `@Inject` 装饰器,以及遍布 `vendor/` 与 `packages/workflow` 的运行时 enum/namespace。仓库的 engines 范围(`^22.19.0 || >=24.0.0`)包含 Node 26,因此原生启动链在其上完全无法启动——且没有任何 CI 任务执行过真实启动向量,这一不兼容悄然发布。 + +启动延迟同样是问题:off-thread 的 `module.register()` hooks worker 把每次解析都跨线程序列化(TUI 启动期间约 440ms 的 `makeSyncRequest` 等待),而完整 tsx 默认形态(`--import tsx`)的 CJS hook 解析放大要多付约 0.4s。 + +## 决策 + +`dsh` 的 TUI、Web 与无头源码启动运行 `node --import tsx/esm`:由 tsx 的 ESM-only hook 同时负责 TypeScript 转换与 tsconfig `paths` 投影。`bin/dsh`、根目录的 `dsh`/`demo:tui`/`demo:web` 脚本以及 Code Mode TUI overlay 使用同一向量;`bin/dsh` 以 checkout 的绝对路径引用 hook 与 tsconfig(裸的 `tsx/esm` 无法从任意 cwd 解析),并将 `TSX_TSCONFIG_PATH` 固定到根 tsconfig。CJS hook 保持关闭,因为 CLI 源码图是纯 ESM;实测 TUI 到 banner 约 0.7s,对比完整 tsx 默认形态约 1.1s、已移除的原生链约 0.75s。 + +`scripts/tspath-loader.ts` 与 `apps/cli/src/tsconfig-paths-loader.ts` 已删除。随之消失的还有该 loader "仅为已声明运行时依赖映射 workspace import" 的运行时规则——tsx 无条件应用 `paths` 映射。声明完整性现在仅由静态门禁保障:配置的裸插件走 `verify-cordis-config`,manifest 走 workspace constraints。(该运行时规则确实发现过真实缺陷:`dsh-plan-mode` 与 `dsh-tool-tasks` 导入 `@deepseek-ai/dsh-llm` 却只声明在 devDependencies;已随本变更修复。) + +node-compat CI 矩阵(Node 22.19 与 26)新增 `dsh-source-launch-smoke`(`apps/cli/tests/source-launch.compat.spec.ts`):以精确的生产启动向量做 keyless 管道 stdio 启动,断言非零退出的 TTY 拒绝。未来 Node 对模块 hook 或 TypeScript 处理的任何改动都会让该门禁变红,而不是破坏开发者的 `pnpm dsh`。 + +## 备选方案 + +**在 Node ≤25 保留原生链并按版本分叉。** 拒绝:两套转换语义(amaro 与 esbuild)在边缘语法上会分歧,启动器要加版本探测,node-compat 矩阵要覆盖两条路径——为一个已经变动过的 experimental flag 付出沉重维护。而且 amaro 也不支持 `vendor/hmr` 使用的 `@Inject` 装饰器,原生路径本来就无法启动随附的默认 TUI 配置。 + +**把源码图改成 erasable-only 以适配 Node 26 strip 模式。** 拒绝:参数属性与值 namespace 遍布 vendor 的 Cordis/cosmokit/loader/schemastery;改写是无界 churn,且每次 vendor sync 都要重做。 + +**仓库自有的同线程 loader(`module.registerHooks()` + esbuild 或 `@swc/core` 转换)。** 暂拒:原型实测约 0.45s(esbuild 路径未端到端验证;SWC 在 `vendor/hmr` 的装饰器 + namespace 合并上两种装饰器模式都会崩),但意味着自行负责转换正确性和一个 tsx 已经提供的 resolve hook。仅当约 0.3s 的差距成为真实成本时再重启;profiling 证据在 PR 讨论中。 + +**Node 26 运行构建产物 `lib/`,24 保留原生。** 拒绝:在最新 Node 版本线上失去零构建开发循环,且混淆源码面与产物面。 + +## 结果 + +- 整个 engines 范围(包括未来改变原生 TypeScript 支持的 Node 版本线)只有一个启动向量;冒烟门禁按矩阵行强制执行。 +- TypeScript 转换重新委托给 tsx/esbuild,逆转了前一篇 note "证明 Node 原生转换可用" 的目标;在 vendor 源码使用不可擦除语法且 Node 不再提供 transform 模式的情况下,该目标不可达。 +- 源码启动中的运行时依赖声明强制不复存在;未声明的 workspace import 现在只能通过静态门禁或构建模式的解析失败暴露。 +- 启动相比完整 tsx 默认形态快约 0.4s(`demo:headless` 与 ACP 保持 `--import tsx`:其依赖图未就 CJS hook 依赖性做审计,且其启动延迟不在交互路径上)。 diff --git a/AGENTS.md b/AGENTS.md index a89bb82bf1..bf8321d618 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,7 +89,7 @@ Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, ## Conventions - Every npm package is `@deepseek-ai/dsh-`; vendored packages keep upstream names and are `private: true`. `cordis` is a peerDependency (+ dev) of every harness package. -- ESM everywhere (`"type": "module"`). Cross-package imports use package names; in-package relative imports include `.ts`. Config subprocesses run built `lib/` under plain Node; source regressions use their declared launcher ([testing policy](docs/testing.md#test-subprocess-launch-modes)). CLI source-launch code and every module it reaches must support Node `--experimental-transform-types`: use `import type` for erased bindings and native ESM exports, with no TSX/JSX or tsx/esbuild-only transforms. TUI/Web `cordis.yml` bare plugins must appear in their resolver manifest's `dependencies`; `verify-cordis-config` enforces the [source-launch contract](.agents/notes/implemented/architecture/2026-07-28-dsh-native-typescript-source-launch.md). +- ESM everywhere (`"type": "module"`). Cross-package imports use package names; in-package relative imports include `.ts`. Config subprocesses run built `lib/` under plain Node; source regressions use their declared launcher ([testing policy](docs/testing.md#test-subprocess-launch-modes)). The `dsh` CLI source launch runs through tsx's ESM-only hook (`node --import tsx/esm`); modules it reaches must stay ESM (no CJS-only shapes) — Node's native TypeScript modes are unavailable across the engines range ([source-launch contract](.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md)). TUI/Web `cordis.yml` bare plugins must appear in their resolver manifest's `dependencies`; `verify-cordis-config` enforces it. - **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer. - **Runtime invariants assert owned relationships.** Check authoritative event streams or mutable data, not service or method presence, plugin metadata or effects, or fixed pure examples. If a package has no plausible relationship, an explained empty companion is correct ([package contract](packages/AGENTS.md)). - **Typed events use declaration merging** and merge-extensible maps. Event JSDoc needs `@mode` and payload `@param`; scoped keys absent from payloads need `@dshScopeScan unsupported`. Public service methods document parameters and non-void returns. diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index e9e5df7630..457f0aba98 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/README.md -README.md: 13a80b1d0e0105bc0c30c019209b2e0295b7bef9 -README.zh.md: 2a5d9c15c57351ef03ebe60a5cdf90f0d0c8f18b +README.md: 3d029e5c97647526357c34df1d87edc904b92e33 +README.zh.md: 70930fa7035effad3ab65926e474fa53883546d7 diff --git a/apps/cli/README.md b/apps/cli/README.md index 13a80b1d0e..3d029e5c97 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -26,6 +26,6 @@ Symlink the source-running launcher onto your PATH; it resolves the checkout thr ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh ``` -Source launches run `apps/cli/src/bin.ts` through Node's `--experimental-transform-types`; `scripts/tspath-loader.ts` only projects tsconfig `paths` into module resolution and does not transform code. Every module reachable from the CLI source entry follows Node's transform-types contract: erased bindings use `import type`, exports use native ESM, and the graph contains no TSX/JSX or transforms that only tsx/esbuild provides. The loader reads `TSX_TSCONFIG_PATH` when set (relative paths resolve from the invoking cwd), otherwise the repository's root tsconfig, using the root TypeScript development tool rather than an application dependency. It maps a workspace import only for a package self-reference or a declared runtime dependency. The TUI configs resolve bare plugins through `examples/package.json`, while the Web/headless `cordis.yml` resolves them through this package's `dependencies`; `verify-cordis-config` requires every configured bare plugin to be declared, while allowing unrelated dependencies. +Source launches run `apps/cli/src/bin.ts` through tsx's ESM-only hook (`node --import tsx/esm`), which transforms TypeScript and projects the root tsconfig `paths` map into module resolution. Node's native TypeScript modes are not used: Node 26 removed `--experimental-transform-types`, and strip-only mode rejects syntax the source graph relies on (vendored parameter properties, decorators, runtime enums/namespaces). The CJS hook stays off because the source graph is ESM-only and the CJS resolver adds ~0.4s of startup. `bin/dsh` pins `TSX_TSCONFIG_PATH` to the checkout's root tsconfig so resolution is cwd-independent, and the `dsh-source-launch-smoke` node-compat gate runs this exact launch vector on every supported Node line. tsx applies the `paths` map without checking dependency declarations, so declaration completeness rests on the static gates: the TUI configs resolve bare plugins through `examples/package.json`, the Web/headless `cordis.yml` through this package's `dependencies`, and `verify-cordis-config` requires every configured bare plugin to be declared, while allowing unrelated dependencies. `pnpm run dsh` runs the same entry from the repo root and forwards arguments directly, for example `pnpm run dsh -p "task"`. The built form (`lib/bin.js`, via `pnpm run build`) boots the same config under plain Node. diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index 2a5d9c15c5..70930fa703 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -26,6 +26,6 @@ Web 和无头界面启动同一个共享组合(`cordis.yml`):两者都将 ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh ``` -源码启动会通过 Node 的 `--experimental-transform-types` 运行 `apps/cli/src/bin.ts`;`scripts/tspath-loader.ts` 只会将 tsconfig 的 `paths` 映射投射到模块解析中,而不会转换代码。从 CLI 源码入口可达的每个模块都遵守 Node transform-types 契约:会被擦除的绑定使用 `import type`,export 使用原生 ESM,整个依赖图不含 TSX/JSX,也不依赖仅由 tsx/esbuild 提供的转换。设置 `TSX_TSCONFIG_PATH` 时,loader 会读取该路径(相对路径从调用方的 cwd 解析),否则读取仓库根 tsconfig;它使用根目录的 TypeScript 开发工具,而不是应用依赖。仅当 workspace import 是包自身引用或已声明的运行时依赖时,loader 才会映射该 import。TUI 配置通过 `examples/package.json` 解析裸插件,而 Web/无头 `cordis.yml` 则通过本包的 `dependencies` 解析;`verify-cordis-config` 要求每个已配置的裸插件均已声明,同时允许存在无关依赖。 +源码启动会通过 tsx 的 ESM-only hook(`node --import tsx/esm`)运行 `apps/cli/src/bin.ts`,由它转换 TypeScript 并将根 tsconfig 的 `paths` 映射投射到模块解析中。不使用 Node 原生 TypeScript 模式:Node 26 移除了 `--experimental-transform-types`,而 strip-only 模式无法接受源码图依赖的语法(vendor 中的参数属性、装饰器、运行时 enum/namespace)。CJS hook 保持关闭,因为源码图是纯 ESM,而 CJS 解析器会增加约 0.4s 启动耗时。`bin/dsh` 将 `TSX_TSCONFIG_PATH` 固定到 checkout 的根 tsconfig,使解析与 cwd 无关;node-compat 门禁 `dsh-source-launch-smoke` 会在每条受支持的 Node 版本线上运行这一精确启动向量。tsx 应用 `paths` 映射时不检查依赖声明,声明完整性由静态门禁保障:TUI 配置通过 `examples/package.json` 解析裸插件,Web/无头 `cordis.yml` 通过本包的 `dependencies` 解析;`verify-cordis-config` 要求每个已配置的裸插件均已声明,同时允许存在无关依赖。 `pnpm run dsh` 从仓库根目录运行同一入口并直接转发参数,例如 `pnpm run dsh -p "task"`。构建形式(`lib/bin.js`,通过 `pnpm run build`)会在普通 Node 下启动同一配置。 diff --git a/apps/cli/src/tsconfig-paths-loader.ts b/apps/cli/src/tsconfig-paths-loader.ts deleted file mode 100644 index b7337998bc..0000000000 --- a/apps/cli/src/tsconfig-paths-loader.ts +++ /dev/null @@ -1,216 +0,0 @@ -/** - * Node module resolve hook for the `dsh` source launcher. It projects the root - * tsconfig `paths` map into Node resolution while leaving all TypeScript syntax - * handling to Node's native transform-types runtime. - * @module @deepseek-ai/dsh/tsconfig-paths-loader - */ - -import { readFile, stat } from 'node:fs/promises' -import { dirname, extname, join, resolve } from 'node:path' -import { fileURLToPath, pathToFileURL } from 'node:url' -import type { ResolveHookContext, ResolveFnOutput } from 'node:module' -import ts from 'typescript' - -interface LoaderData { - tsconfigPath: string -} - -interface PackageManifest { - name?: string - dependencies?: Record - optionalDependencies?: Record - peerDependencies?: Record -} - -interface PathRule { - pattern: string - prefix: string - suffix: string - targets: readonly string[] -} - -interface PathsCompilerOptions { - readonly baseUrl?: string - readonly paths?: ts.MapLike - readonly pathsBasePath?: string -} - -// Node's native TypeScript transform cannot parse JSX, so `.tsx` is excluded. -const SOURCE_EXTENSIONS = ['.ts', '.mts', '.cts'] as const - -/** - * Resolve package imports through one parsed tsconfig paths table. - * - * Manifest reads are process-scoped and memoized by path. Only matched source - * aliases enter the cache, bounding it to directories participating in source - * resolution. - */ -export class TsconfigPathsResolver { - private readonly rules: readonly PathRule[] - private readonly configDirectory: string - private readonly manifests = new Map>() - - private constructor(configDirectory: string, paths: ts.MapLike) { - this.configDirectory = configDirectory - this.rules = Object.entries(paths) - .map(([pattern, targets]) => { - const wildcard = pattern.indexOf('*') - return { - pattern, - prefix: wildcard === -1 ? pattern : pattern.slice(0, wildcard), - suffix: wildcard === -1 ? '' : pattern.slice(wildcard + 1), - targets, - } - }) - .sort((left, right) => { - const leftExact = left.pattern.includes('*') ? 0 : 1 - const rightExact = right.pattern.includes('*') ? 0 : 1 - return rightExact - leftExact || right.prefix.length - left.prefix.length || right.suffix.length - left.suffix.length - }) - } - - /** - * Parse a tsconfig including its `extends` chain. - * @param tsconfigPath Absolute tsconfig path supplying `compilerOptions.paths`. - * @returns A resolver backed by that path table. - */ - static create(tsconfigPath: string): TsconfigPathsResolver { - let unrecoverable: ts.Diagnostic | undefined - const parsed = ts.getParsedCommandLineOfConfigFile(tsconfigPath, {}, { - ...ts.sys, - onUnRecoverableConfigFileDiagnostic(diagnostic) { unrecoverable = diagnostic }, - }) - if (parsed === undefined) { - const detail = unrecoverable === undefined - ? 'unknown configuration error' - : ts.flattenDiagnosticMessageText(unrecoverable.messageText, '\n') - throw new Error(`dsh source loader could not parse ${tsconfigPath}: ${detail}`) - } - const options = parsed.options as PathsCompilerOptions - const paths = options.paths - if (paths === undefined) throw new Error(`dsh source loader requires compilerOptions.paths in ${tsconfigPath}`) - const configDirectory = options.baseUrl ?? options.pathsBasePath ?? dirname(tsconfigPath) - return new TsconfigPathsResolver(configDirectory, paths) - } - - /** - * Resolve one bare package specifier to a source file when the importing - * package (or config-directory owner) declares that package at runtime. - * @param specifier Module specifier passed to Node. - * @param parentURL Importing file or Loader config-directory URL. - * @returns Source file URL, or `undefined` when normal Node resolution owns the request. - */ - async resolve(specifier: string, parentURL: string | undefined): Promise { - const packageName = packageNameFromSpecifier(specifier) - if (packageName === undefined || parentURL === undefined || !parentURL.startsWith('file:')) return undefined - const matched = this.match(specifier) - if (matched === undefined) return undefined - const configParent = parentURL.endsWith('/') - const parentPath = fileURLToPath(parentURL) - const startDirectory = configParent ? parentPath : dirname(parentPath) - if (!await this.isDeclaredRuntimeDependency(startDirectory, packageName, configParent)) return undefined - - for (const target of matched.targets) { - const substituted = target.replace('*', matched.wildcard) - const candidate = await existingSourcePath(resolve(this.configDirectory, substituted)) - if (candidate !== undefined) return pathToFileURL(candidate).href - } - return undefined - } - - private match(specifier: string): { targets: readonly string[]; wildcard: string } | undefined { - for (const rule of this.rules) { - if (!rule.pattern.includes('*')) { - if (specifier === rule.pattern) return { targets: rule.targets, wildcard: '' } - continue - } - if (!specifier.startsWith(rule.prefix) || !specifier.endsWith(rule.suffix)) continue - const wildcard = specifier.slice(rule.prefix.length, specifier.length - rule.suffix.length) - return { targets: rule.targets, wildcard } - } - return undefined - } - - private async isDeclaredRuntimeDependency( - startDirectory: string, - packageName: string, - searchAncestors: boolean, - ): Promise { - for (let directory = startDirectory; ; directory = dirname(directory)) { - const manifest = await this.readManifest(join(directory, 'package.json')) - if (manifest !== undefined) { - if (declaresRuntimeDependency(manifest, packageName)) return true - if (!searchAncestors) return false - } - const parent = dirname(directory) - if (parent === directory) return false - } - } - - private readManifest(path: string): Promise { - let pending = this.manifests.get(path) - if (pending !== undefined) return pending - pending = readFile(path, 'utf8').then( - content => JSON.parse(content) as PackageManifest, - (error: unknown) => { - if (error instanceof Error && (error as NodeJS.ErrnoException).code === 'ENOENT') return undefined - throw error - }, - ) - this.manifests.set(path, pending) - return pending - } -} - -let resolver: TsconfigPathsResolver | undefined - -/** Initialize the hook worker from the source-launch preloader. */ -export function initialize(data: LoaderData): void { - resolver = TsconfigPathsResolver.create(data.tsconfigPath) -} - -/** Resolve declared workspace packages to source and delegate every other request to Node. */ -export async function resolveHook( - specifier: string, - context: ResolveHookContext, - nextResolve: (specifier: string, context: ResolveHookContext) => Promise, -): Promise { - const url = await resolver?.resolve(specifier, context.parentURL) - return url === undefined ? nextResolve(specifier, context) : { url, shortCircuit: true } -} - -// Node customization hooks discover this exact export name. -export { resolveHook as resolve } - -function packageNameFromSpecifier(specifier: string): string | undefined { - if (specifier.startsWith('.') || specifier.startsWith('/') || /^[a-z][a-z+.-]*:/i.test(specifier)) { - return undefined - } - const segments = specifier.split('/') - return specifier.startsWith('@') - ? segments.length >= 2 ? `${segments[0]}/${segments[1]}` : undefined - : segments[0] || undefined -} - -function declaresRuntimeDependency(manifest: PackageManifest, packageName: string): boolean { - return manifest.name === packageName - || packageName in (manifest.dependencies ?? {}) - || packageName in (manifest.optionalDependencies ?? {}) - || packageName in (manifest.peerDependencies ?? {}) -} - -async function existingSourcePath(base: string): Promise { - const extension = extname(base) - if (extension === '.tsx') return undefined - const candidates = extension === '' - ? [base, ...SOURCE_EXTENSIONS.map(extension => `${base}${extension}`), ...SOURCE_EXTENSIONS.map(extension => join(base, `index${extension}`))] - : [base] - for (const candidate of candidates) { - try { - if ((await stat(candidate)).isFile()) return candidate - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error - } - } - return undefined -} diff --git a/apps/cli/tests/source-launch.compat.spec.ts b/apps/cli/tests/source-launch.compat.spec.ts new file mode 100644 index 0000000000..bc6959261a --- /dev/null +++ b/apps/cli/tests/source-launch.compat.spec.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { execa } from 'execa' +import { describe, expect, it } from 'vitest' + +/** + * Keyless smoke for the SOURCE `dsh` launcher: run `apps/cli/src/bin.ts` + * with the exact production launch vector (`node --import tsx/esm`, the same + * shape as `bin/dsh` and the root `dsh`/`demo:tui`/`demo:web` scripts) and + * assert the piped-stdio TTY refusal. The Node compatibility matrix runs this + * WHOLE file, so a Node release changing module hooks or TypeScript handling + * breaks this gate instead of every developer's `pnpm dsh`; the built-bin + * suite covers the published `lib/` entry, not this source chain. + */ + +const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)) +const dshSourceBin = 'apps/cli/src/bin.ts' + +describe('dsh SOURCE launcher (node --import tsx/esm)', () => { + it('boots the source entry and refuses pipes LOUD (non-zero exit + stderr)', async () => { + const result = await execa(process.execPath, ['--import', 'tsx/esm', dshSourceBin], { + cwd: repoRoot, + input: '', + timeout: 25_000, + killSignal: 'SIGKILL', + reject: false, + }) + if (result.timedOut) { + throw new Error(`dsh source launch did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`) + } + expect(result.exitCode).not.toBe(0) + expect(result.stderr).toContain('requires stdin and stdout to be interactive TTYs') + expect(result.stderr).toContain('dsh -p') + // The refusal happens before any plugin mounts: stdout stays silent. + expect(result.stdout).toBe('') + }, 30_000) +}) diff --git a/apps/cli/tests/tsconfig-paths-loader.spec.ts b/apps/cli/tests/tsconfig-paths-loader.spec.ts deleted file mode 100644 index 834ee8c4d0..0000000000 --- a/apps/cli/tests/tsconfig-paths-loader.spec.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' -import type { ResolveFnOutput, ResolveHookContext } from 'node:module' -import { tmpdir } from 'node:os' -import { dirname, join } from 'node:path' -import { pathToFileURL } from 'node:url' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { initialize, resolveHook, TsconfigPathsResolver } from '../src/tsconfig-paths-loader.ts' - -class ResolverFixture { - readonly root = mkdtempSync(join(tmpdir(), 'dsh-tsconfig-paths-')) - - path(relativePath: string): string { - return join(this.root, relativePath) - } - - write(relativePath: string, content = 'export {}\n'): string { - const path = this.path(relativePath) - mkdirSync(dirname(path), { recursive: true }) - writeFileSync(path, content) - return path - } - - writeJson(relativePath: string, value: unknown): string { - return this.write(relativePath, `${JSON.stringify(value)}\n`) - } - - createResolver(paths: Record): TsconfigPathsResolver { - const tsconfigPath = this.writeJson('tsconfig.json', { compilerOptions: { paths } }) - return TsconfigPathsResolver.create(tsconfigPath) - } - - parentURL(relativePath = 'consumer/src/nested/index.ts'): string { - return pathToFileURL(this.path(relativePath)).href - } - - dispose(): void { - rmSync(this.root, { recursive: true, force: true }) - } -} - -const fixtures: ResolverFixture[] = [] - -function fixture(): ResolverFixture { - const value = new ResolverFixture() - fixtures.push(value) - return value -} - -afterEach(() => { - for (const value of fixtures.splice(0)) value.dispose() -}) - -describe('TsconfigPathsResolver', () => { - it('orders exact, longer-prefix, and longer-suffix path rules', async () => { - const files = fixture() - files.writeJson('consumer/package.json', { - dependencies: { - '@scope/feature-name': '*', - '@scope/feature-other': '*', - '@scope/plain-suffix': '*', - }, - }) - files.write('targets/exact.ts') - files.write('targets/prefix/other.ts') - files.write('targets/generic/feature-other.ts') - files.write('targets/suffix/plain.ts') - files.write('targets/generic/plain-suffix.ts') - const resolver = files.createResolver({ - '@scope/*': ['./targets/generic/*'], - '@scope/*-suffix': ['./targets/suffix/*'], - '@scope/feature-*': ['./targets/prefix/*'], - '@scope/feature-name': ['./targets/exact.ts'], - }) - - await expect(resolver.resolve('@scope/feature-name', files.parentURL())) - .resolves.toBe(pathToFileURL(files.path('targets/exact.ts')).href) - await expect(resolver.resolve('@scope/feature-other', files.parentURL())) - .resolves.toBe(pathToFileURL(files.path('targets/prefix/other.ts')).href) - await expect(resolver.resolve('@scope/plain-suffix', files.parentURL())) - .resolves.toBe(pathToFileURL(files.path('targets/suffix/plain.ts')).href) - }) - - it('resolves only self-references and runtime dependencies from the nearest ancestor manifest', async () => { - const files = fixture() - files.writeJson('consumer/package.json', { - name: 'self-package', - dependencies: { dependency: '*' }, - optionalDependencies: { optional: '*' }, - peerDependencies: { peer: '*' }, - }) - for (const name of ['self-package', 'dependency', 'optional', 'peer', 'undeclared']) { - files.write(`targets/${name}.ts`) - } - const resolver = files.createResolver(Object.fromEntries( - ['self-package', 'dependency', 'optional', 'peer', 'undeclared'] - .map(name => [name, [`./targets/${name}`]]), - )) - - for (const name of ['self-package', 'dependency', 'optional', 'peer']) { - await expect(resolver.resolve(name, files.parentURL())) - .resolves.toBe(pathToFileURL(files.path(`targets/${name}.ts`)).href) - } - await expect(resolver.resolve('undeclared', files.parentURL())).resolves.toBeUndefined() - }) - - it('probes native TypeScript extensions and index files but excludes TSX and missing targets', async () => { - const files = fixture() - const names = ['plain-ts', 'module-mts', 'common-cts', 'directory', 'tsx-implicit', 'tsx-explicit', 'missing'] - files.writeJson('consumer/package.json', { - dependencies: Object.fromEntries(names.map(name => [name, '*'])), - }) - files.write('targets/plain.ts') - files.write('targets/module.mts') - files.write('targets/common.cts') - files.write('targets/directory/index.ts') - files.write('targets/component.tsx') - const resolver = files.createResolver({ - 'plain-ts': ['./targets/plain'], - 'module-mts': ['./targets/module'], - 'common-cts': ['./targets/common'], - 'directory': ['./targets/directory'], - 'tsx-implicit': ['./targets/component'], - 'tsx-explicit': ['./targets/component.tsx'], - 'missing': ['./targets/missing'], - }) - - for (const [name, target] of [ - ['plain-ts', 'targets/plain.ts'], - ['module-mts', 'targets/module.mts'], - ['common-cts', 'targets/common.cts'], - ['directory', 'targets/directory/index.ts'], - ] as const) { - await expect(resolver.resolve(name, files.parentURL())) - .resolves.toBe(pathToFileURL(files.path(target)).href) - } - await expect(resolver.resolve('tsx-implicit', files.parentURL())).resolves.toBeUndefined() - await expect(resolver.resolve('tsx-explicit', files.parentURL())).resolves.toBeUndefined() - await expect(resolver.resolve('missing', files.parentURL())).resolves.toBeUndefined() - }) - - it('anchors inherited paths at the config that declared them', async () => { - const files = fixture() - files.writeJson('consumer/package.json', { dependencies: { custom: '*' } }) - files.write('targets/custom.ts') - files.writeJson('base.json', { compilerOptions: { paths: { custom: ['./targets/custom'] } } }) - const customTsconfig = files.writeJson('configs/custom.json', { extends: '../base.json' }) - const resolver = TsconfigPathsResolver.create(customTsconfig) - - await expect(resolver.resolve('custom', files.parentURL())) - .resolves.toBe(pathToFileURL(files.path('targets/custom.ts')).href) - }) - - it('short-circuits matched aliases and delegates unsupported schemes or unmatched requests', async () => { - const files = fixture() - files.writeJson('consumer/package.json', { dependencies: { matched: '*' } }) - const target = files.write('targets/matched.ts') - const tsconfigPath = files.writeJson('tsconfig.json', { - compilerOptions: { paths: { matched: ['./targets/matched'] } }, - }) - initialize({ tsconfigPath }) - const context: ResolveHookContext = { - conditions: [], - importAttributes: {}, - parentURL: files.parentURL(), - } - const nextResolve = vi.fn(async ( - specifier: string, - _context: ResolveHookContext, - ): Promise => ({ url: `next:${specifier}` })) - - await expect(resolveHook('matched', context, nextResolve)) - .resolves.toEqual({ url: pathToFileURL(target).href, shortCircuit: true }) - expect(nextResolve).not.toHaveBeenCalled() - - for (const specifier of ['unmatched', 'node:fs', 'data:text/javascript,export default 1', 'https://example.test/mod.ts']) { - await expect(resolveHook(specifier, context, nextResolve)).resolves.toEqual({ url: `next:${specifier}` }) - expect(nextResolve).toHaveBeenLastCalledWith(specifier, context) - } - }) -}) diff --git a/bin/dsh b/bin/dsh index 319d915b18..f85f28a5cd 100755 --- a/bin/dsh +++ b/bin/dsh @@ -1,7 +1,7 @@ #!/bin/sh -# dsh launcher: runs the apps/cli `dsh` bin FROM SOURCE through Node's native -# TypeScript transform, so a symlink from anywhere (e.g. ~/.local/bin/dsh) -# always executes the current working tree without a build step. +# dsh launcher: runs the apps/cli `dsh` bin FROM SOURCE through the tsx ESM +# hook, so a symlink from anywhere (e.g. ~/.local/bin/dsh) always executes the +# current working tree without a build step. set -eu # Resolve symlink chains without readlink -f (not on every macOS). @@ -15,8 +15,11 @@ while [ -L "$script" ]; do done root=$(CDPATH='' cd -- "$(dirname -- "$script")/.." && pwd) -# The preloader projects this checkout's tsconfig paths into Node resolution; -# TypeScript transformation itself remains Node-owned (no tsx/esbuild hook). -exec node --experimental-transform-types \ - --import "$root/scripts/tspath-loader.ts" \ +# The ESM-only tsx hook transforms TypeScript and projects this checkout's +# tsconfig paths into Node resolution (the CJS hook stays off: the graph is +# ESM-only and the CJS resolver costs ~0.4s of startup). Absolute paths keep +# both the hook and the tsconfig anchored to this checkout when the launcher +# runs from any cwd, where bare `tsx/esm` would not resolve. +TSX_TSCONFIG_PATH="$root/tsconfig.json" \ + exec node --import "$root/node_modules/tsx/dist/esm/index.mjs" \ "$root/apps/cli/src/bin.ts" "$@" diff --git a/package.json b/package.json index afa74fdf1e..a51261a4c2 100644 --- a/package.json +++ b/package.json @@ -96,13 +96,13 @@ "constraints": "tsx scripts/check-workspace-constraints.ts", "doc-sync": "tsx scripts/run-gates.ts doc-sync", "hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-package-invariants && pnpm run verify-built-package-invariants && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure", - "dsh": "node --experimental-transform-types --import ./scripts/tspath-loader.ts apps/cli/src/bin.ts", + "dsh": "node --import tsx/esm apps/cli/src/bin.ts", "demo:headless": "node --import tsx packages/examples/cli-demo/src/bin.ts --config examples/headless-agent/cordis.yml", - "demo:tui": "node --experimental-transform-types --import ./scripts/tspath-loader.ts apps/cli/src/bin.ts", + "demo:tui": "node --import tsx/esm apps/cli/src/bin.ts", "demo:code-mode": "node scripts/demo-code-mode.mjs", "demo:cordis": "node scripts/demo-cordis.mjs", "demo:acp": "node --import tsx packages/examples/acp-demo/src/bin.ts --config examples/acp-agent/cordis.yml", - "demo:web": "npm run build && node --experimental-transform-types --import ./scripts/tspath-loader.ts apps/cli/src/bin.ts web", + "demo:web": "npm run build && node --import tsx/esm apps/cli/src/bin.ts web", "mock:llm": "node --import tsx packages/support/llm-mock-server/src/bin.ts", "dev:web": "tsx scripts/dev-web.ts --poll", "postinstall": "node scripts/install-lefthook.mjs" diff --git a/scripts/demo-code-mode.mjs b/scripts/demo-code-mode.mjs index d790fe15d5..994bea32d0 100644 --- a/scripts/demo-code-mode.mjs +++ b/scripts/demo-code-mode.mjs @@ -8,9 +8,8 @@ import { spawn } from 'node:child_process' // Each UI's node invocation matches its base demo script plus the overlay config. const UIS = new Map([ ['tui', [ - '--experimental-transform-types', '--import', - './scripts/tspath-loader.ts', + 'tsx/esm', 'apps/cli/src/bin.ts', '--config', 'examples/tui-agent/code-mode.cordis.yml', diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index aa2378b24f..d10a9ccba7 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -287,6 +287,11 @@ function nodeCompatSmokeGates(): Gate[] { 'run', 'packages/session-persistence/session-persistence-jsonl/tests/zstd.compat.spec.ts', ], { label: 'JSONL Zstandard smoke' }), + pnpmExec('dsh-source-launch-smoke', [ + 'vitest', + 'run', + 'apps/cli/tests/source-launch.compat.spec.ts', + ], { label: 'dsh source-launch smoke' }), ] } diff --git a/scripts/tspath-loader.ts b/scripts/tspath-loader.ts deleted file mode 100644 index adeecb6b64..0000000000 --- a/scripts/tspath-loader.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** Register source-only tsconfig paths resolution before a TypeScript entry loads. */ - -import { register } from 'node:module' -import { resolve } from 'node:path' -import { fileURLToPath } from 'node:url' - -const tsconfigPath = process.env.TSX_TSCONFIG_PATH === undefined - ? fileURLToPath(new URL('../tsconfig.json', import.meta.url)) - : resolve(process.env.TSX_TSCONFIG_PATH) - -register(new URL('../apps/cli/src/tsconfig-paths-loader.ts', import.meta.url), { - parentURL: import.meta.url, - data: { tsconfigPath }, -})