
CLI开发工具语言运行时【免费下载链接】tsx⚡️ TypeScript Execute | The easiest way to run TypeScript in Node.js项目地址https://gitcode.com/gh_mirrors/ts/tsx点击查看免费下载导读本文基于 tsx 仓库的决策文档 notes/tsx/cjs-default-import-interop.md完整解析 tsx 如何对待一个module.exports同时带有__esModule与default属性的 CommonJS 包。你将理解默认导入到底拿到整个module.exports还是它的.default这一歧义的根源、tsx 在六种导入路径下的具体语义含源码级依据以及未来改动必须守住的兼容边界——无论你是包作者、tsx 用户还是想在工具链中复刻该语义的开发者本文都能给出可直接引用的决策依据。一、问题背景为什么__esModuledefault是一个无法消除的歧义CommonJS 世界里有两种完全不同的模块可以暴露同一个运行时值// 转译出来的 ESM如 TypeScript/Babel 产物 Object.defineProperty(exports, __esModule, { value: true }) exports.default handler // 手写的 CommonJS API Object.defineProperty(module.exports, __esModule, { value: true }) module.exports.default handler第一类模块通常期望Babel 风格的互操作默认导入被绑定到.default第二类模块则可能故意把完整对象作为公开 API 暴露此时解包.default反而是错误。问题的关键在于从值的键keys、属性描述符descriptors、源码形态source pattern、包元数据package metadata到声明文件declaration file运行时都无法区分这两类模块TypeScript 也将这两种输出判定为结构上完全相同。因此可以得出一个强结论任何只基于对象形状shape的谓词都不是绝对安全的。真正安全的解包信号只有三种loader 拥有的转换 provenance即转换器自己知道这份 CJS 输出是我从 ESM 转出来的显式的版本化 opt-in用户主动声明某种互操作模式包通过exports.import入口选择原生 ESM从而在加载路径上绕开歧义。二、tsx 的决策按导入方执行模型而非包作者意图决定语义tsx 的立场可以概括为一句话原生 ESM 中tsx 完全跟随 Node从 CommonJS 的默认导入就是完整的module.exports值tsx 不会自动把__esModule重新解释为把导入直接绑定到.default。而当 tsx 把一个导入方文件编译为 CommonJS 时esbuild 会应用其 Babel 兼容的 interop helper对标记过的编译器输出解包到.default。这个区分的核心原则是语义由导入方的执行模型决定而不是去猜测包作者的意图。同一份被导入的包在原生 ESM 环境下和在被 tsx 编译为 CJS 的环境下得到的结果可以不同——这正是下面行为对照表的由来。三、当前行为对照六种导入路径六种明确语义设M为 CommonJS 的module.exports值D为M.defaulttsx 的完整行为如下导入路径结果归属方原生静态 ESM 默认导入MNode被编译为 CommonJS 的静态导入对标记过的编译器输出取Desbuild原生import()命名空间且.default MNodetsx 转换源码中的import()当命名空间默认值是含__esModule的对象时取Mtsx 兼容性转换作用域化的api.import()原生命名空间Nodetsx.require()原始require()值MNode3.1 原生静态 ESM保持 Node 语义ESM hook 在load阶段对 TypeScript/ESM 源调用transform()src/utils/transform/index.ts其中 esbuild 选项固定为format: esmindex.ts产物以format: module返回给 Nodesrc/esm/hook/load.ts。由于产物保留 ESM 形态静态绑定的默认导入完全走 Node 自身的 CJS 互操作即拿到完整的M。3.2 被编译为 CommonJS 的静态导入esbuild 的 Babel 风格互操作CJS 加载路径如tsx.require、register的 CJS 上下文使用transformSync()src/utils/transform/index.ts其 esbuild 选项为format: cjsindex.ts并附带platform: node与 CJS banner/footer 包装。esbuild 的 CJS 输出对被标记的编译器输出带__esModule的exports.default形态生成 Babel 兼容的互操作代码因此默认导入得到D。从源码结构看同一份源码在两条路径ESM hook vs CJS loader下走了两套不同的 esbuild 配置这正是语义跟随导入方执行模型的实现载体导入方是 ESM 则保留 ESM导入方是 CJS 则按 esbuild 惯例互操作。3.3 tsx 转换源码中的动态导入唯一的解包例外这是 tsx 特有的legacy 兼容行为实现在 src/utils/transform/transform-dynamic-import.ts。tsx 会把源码中的import()用 MagicString 追加.then(...)处理transform-dynamic-import.ts其核心谓词逻辑为const toEsmFunctionString (imported { const d default; if ( imported[d] typeof imported[d] object __esModule in imported[d] ) { return imported[d]; } return imported; }).toString();需要特别指出该谓词的三个细节它们都比常见的编译器惯例更宽它解包的目标是命名空间默认值imported.default把它塌缩为M而不是直接塌缩到D——即把命名空间还原成模块本体它通过in操作符检查__esModule因此接受继承来的属性它不要求__esModule true只要该属性存在且默认值为对象即可触发。正因为这个谓词比惯例更宽、属于历史遗留的兼容行为决策文档明确划出红线它绝不能成为原生静态 ESM 导入的政策。3.4 作用域化api.import()回到原生命名空间createScopedImport()src/esm/api/scoped-import.ts本质上是用tsx://包装 specifier 后调用原生import()因此其返回的是 Node 原生命名空间语义即.default M不受 tsx 动态导入转换影响。3.5tsx.require()原始 CJS 值tsxRequire直接暴露 Node 的requiresrc/cjs/api/require.ts不做任何解包结果始终是原始M。3.6 测试佐证测试夹具 tests/fixtures.ts 中的cjs/index.cjs明确断言__esModule被解包// Assert __esModule is unwrapped import (../ts/index.ts).then((m) assert( !(typeof m.default object (default in m.default)), ));同文件的mjs/index.mjstests/fixtures.ts对真实 CJS 包pkg-commonjs做同样断言动态导入后命名空间默认值不应再是带default属性的对象。这两条测试恰好锁定了 3.3 节所述动态导入解包到M的行为。四、其他工具的立场对照大多数工具刻意分裂把视野拉到整个工具链会发现 tsx 的决策并非孤例而是一条普遍规律工具对标记过的 CommonJS 默认导入选择器Node 决策笔记完整module.exports对 CommonJS 目标始终如此TypeScript 决策笔记CJS 产物中取.defaultESM 产物中取完整值导入方输出格式esbuild 决策笔记Babel 模式下取.defaultNode 模式下取完整值导入方 ESM 分类Bun 决策笔记通常取.default在type: module的导入方作用域内取完整值被导入方包作用域Deno完整module.exportsNode 兼容的运行时行为Babelbabel模式取.defaultnode模式取完整值显式importInterop选项Rollup插件auto模式下对标记模块取.default插件或输出 interop 选项Vite/Rolldown对 Node 分类的导入方取完整值否则取.default导入方模块分类webpack严格 ESM 取完整值通过兼容 helper 取.default导入方模块分类ts-node跟随 TypeScript CJS 产物或原生 Node ESM所选模块模式这张对照表揭示的规律是编译器兼容 helper 可以信任__esModule针对被转换的导入方而原生 ESM 运行时保留完整的 CommonJS 值。tsx 的两条腿——esbuild CJS 互操作 Node 原生 ESM 语义——恰好横跨这两端Bun 则是其中实质性的运行时例外默认取.default。五、未来变更的边界哪些事不能做哪些事需要先补测试决策文档为后续演进划定了两条清晰边界5.1 静态导入的红线不要给原生静态 ESM 导入添加自动解包。一旦添加tsx 将偏离 Node 语义并破坏那些故意暴露完整对象的合法 CommonJS API。若未来确实需要一个显式的 opt-in 兼容模式来定义替代语义静态导入支持将要求导入方重写importer rewriting或合成 facade 模块且必须完整保持活绑定live bindings再导出re-exports循环依赖cycles混合命名/默认导入缓存身份cache identity源码映射source maps同步/异步 hook 的对等性sync/async hook parity。5.2 动态导入解包可收窄但必须先补测试单独的 breaking release 可以评估移除或收窄对 ESM 分类源ESM-classified sources的动态导入解包。但在改动该路径之前必须为以下场景补齐行为测试带__esModule与default的故意的CommonJS 对象继承的、值为false的、不可枚举的__esModule属性编译器产出的 CJS 默认导出与命名导出ESM 与 CommonJS 导入方下的静态/动态导入对等性ESM 命名空间包装、循环依赖与缓存身份。这些测试项直接对应 3.3 节谓词的三个比惯例更宽的细节in检查、不要求true、对象形状判定是未来任何语义收窄的回归防线。六、实践建议包作者如何主动避开歧义结合上述决策包作者可以通过三种途径主动消除歧义而不是把命运交给调用方的执行模型提供exports.import条件入口让导入方始终命中原生 ESM——这是决策文档点名的最安全信号之一避免同时暴露__esModule与default的双重形态手写 CJS 包若把完整对象作为公开 API就不要给module.exports打__esModule标记防止被 esbuild/编译器按标记过的编译器输出解包理解并利用 tsx 的分裂语义在 tsx 中被编译为 CJS 的导入方会得到.defaultesbuild 惯例原生 ESM 导入方与tsx.require()会得到完整MNode 惯例——据此设计测试而不是假设所有地方行为一致。延伸阅读本决策笔记所属的 tsx 研究索引notes/tsx/README.md另含模块解析、Node 集成、转换后端三条研究线各工具立场详见仓库内决策笔记notes/node/cjs-esm-interop.md、notes/typescript/cjs-esm-interop.md、notes/esbuild/cjs-esm-interop.md、notes/bun/cjs-esm-interop.md动态导入兼容转换的完整实现src/utils/transform/transform-dynamic-import.tsESM/CJS 双路径转换实现src/utils/transform/index.ts、src/esm/hook/load.ts行为断言测试夹具tests/fixtures.ts赞分享CLI开发工具语言运行时【免费下载链接】tsx⚡️ TypeScript Execute | The easiest way to run TypeScript in Node.js项目地址https://gitcode.com/gh_mirrors/ts/tsx点击查看免费下载相关推荐深入解析 Bun 的 CommonJS 默认导出互操作策略__esModule 解包规则及其与 tsx / Node 的分歧深入解析 Bun 的 CommonJS 默认导出互操作策略 __esModule 解包规则及其与 tsx / Node 的分歧 本篇技术指南围绕 tsx 仓库CLI开发工具语言运行时TypeScript 的 CommonJS/ESM 默认导入互操作__esModule 约定、Node 感知 ESM 输出与 tsx 的实际落地TypeScript 的 CommonJS/ESM 默认导入互操作 __esModule 约定、Node 感知 ESM 输出与 tsx 的实际落地 本文以仓库CLI开发工具语言运行时Rolldown 的 CommonJS 打包实战指南原生 CJS 支持、ESM 互操作与边界行为Rolldown 的 CommonJS 打包实战指南原生 CJS 支持、ESM 互操作与边界行为 RolldownRust 编写的 JavaScript/T构建工具前端构建开发工具上一篇ThriveX-Blog自动化部署零代码实现CI/CD流水线的完整指南下一篇UnityDataTools架构解析三层架构如何实现高效Unity数据读取创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考