ESA Pages 体验之旅(2):部署静态页面和 API
ESA Pages 可以同时交付网页和轻量 API,但它接收的内容有明确结构:浏览器使用静态资源目录,边缘运行时加载 JavaScript 函数入口。一个带 TypeScript、npm 依赖和前端框架的仓库,需要先整理成这两份产物。
这个项目采用 Nuxt 静态前端和同源 API。本地保留 Vue 的开发体验,部署时分别生成静态文件和函数入口。
静态资源与函数入口
浏览器请求 HTML、CSS、图片和前端 JavaScript 时,由静态资源服务返回文件;请求 /api/* 时,由函数处理业务,再访问外部服务。服务端凭据留在函数配置中。
删去组件和测试后,工程可以缩到下面这样:
edge-app/
├─ app/ 浏览器页面
├─ server/application/ 与平台无关的业务 handler
├─ edge/index.ts ESA fetch 入口
├─ scripts/build-edge.mjs
└─ esa.jsonc
入口负责把平台能力转换成业务参数。例如环境变量、可信客户端 IP、Cache API 都在这里取得;业务 handler 使用标准 Request 和 Response。本地框架适配器与 ESA 入口可以调用同一份业务实现。
import { handleRequest } from '../server/application/handler';
export default {
async fetch(request: Request, _context: unknown, env: Record<string, string>) {
return handleRequest(request, { apiKey: env.UPSTREAM_API_KEY });
},
};
这里只示意入口契约,不包含完整路由与静态资源配置。平台如何分发静态和函数请求,还要结合项目域名、路由规则及静态资源策略确认。
函数构建
普通 Node 服务可以只把 TS 转成 JS,再连同生产依赖目录部署。Node 进程拥有文件系统和模块加载器,运行时可以继续寻找 node_modules。ESA 函数则按上传的脚本执行,不能假定它会安装生产依赖,或还原一个普通 Node 服务目录。
函数入口与 npm 依赖可以交给 ESA CLI 打包,也可以先用框架或本地工具生成。最终文件都要符合边缘运行时的加载要求。
| 工程输入 | 交付前的处理 | 结果 |
|---|---|---|
| 前端 SFC、CSS、浏览器依赖 | 框架生成静态网站 | 静态资源目录 |
| TS 函数入口与普通 JS 依赖 | 转译、解析依赖并打包 | 函数 JavaScript 文件 |
| 宿主提供的模块 | 核对支持范围后保留导入 | 运行时取得实现 |
| 运行时读取的文件或原生扩展 | 确认平台有对应机制,或改造依赖 | 不能仅靠打包解决 |
已经整理好的一份 JavaScript 函数可以直接上传。带有 TS 和 npm 依赖的多文件工程,则需要先完成这些交付准备。
使用限制还规定每个函数的 JavaScript 代码文件上限为 4 MB。这是函数文件的限制,静态文件与整个部署包另有约束;不能拿 gzip 后的网络传输大小代替它。2026-10-29 的示例 worker 为 3,321,770 字节,说明 SDK 和兼容层也需要纳入体积检查。
把共享包发布到 npm,只改变安装来源,不会消除目标平台的加载与体积要求。本地 link: 能找到相邻仓库,不代表只上传入口后仍能找到它;发布目录必须包含依赖,或者将需要的代码打进函数。
语法转换、依赖打包与发布包装各自负责不同的内容。转译可以保留多文件结构,bundle 合并可分析的依赖,包装则整理资源、原生扩展和启动信息。框架构建仍需先处理组件、自动导入和文件路由,通用 bundler 不会自动理解这些约定。完整例子见依赖与资源的交付实验弹窗;当前 worker 的字节记录见函数体积快照弹窗。
选择函数打包工具
ncc 和 esbuild 都能打包 Node 程序。ncc 在 Webpack 上封装常见分发需求;esbuild 便于应用显式控制入口、模块格式、外置名单和必要适配。本项目已有 esbuild 构建链,因此继续使用它,资源整理与 ESA 宿主支持另行检查。
| 关注点 | ncc | esbuild |
|---|---|---|
| 配置入口 | 封装好的 Node 打包 CLI / API | 通用构建 CLI / API |
| 外置依赖 | 支持 external | 支持 external、packages 等配置 |
| 运行时资源 | 有追踪能力,仍受版本、扩展名与代码形态限制 | 通常需要显式整理 |
| 验收 | 检查 code 与 assets,并在独立目录执行 | 检查完整交付目录,并在独立目录执行 |
固定在 Windows x64、Node.js 26.12.0、esbuild 0.28.2、ncc 0.45.0 的夹具共检查了 19 个场景,同时保留预期失败。ncc 的同内容 CommonJS 资源读取例子中,.js 自动搬运文本,.cjs 缺失文本而报 ENOENT,补齐后恢复。构建命令成功并不能代替资源验收。完整版本、输出与计数见隔离分发验证记录弹窗。
这些小夹具没有测量真实项目性能。选择 esbuild 的依据是构建边界和已有工具链,不能从产物更小或工具更新直接推断实际性能。
Nuxt 的两份输出
以 Nuxt SPA 为例,ssr: false 配合 nuxt generate 输出静态文件。它不会把服务端业务变成可以执行的静态网页。只上传 .output/public,API 仍然需要函数承接。
示例应用的构建分别生成:
.output/public/ HTML、CSS、浏览器 JS 与图片
.edge/worker.js ESA 函数入口及其依赖
配置中的 assets.directory 和 entry 分别指向它们。以下省略安装、域名和路由细节:
{
"name": "example-edge-app",
"entry": ".edge/worker.js",
"assets": { "directory": ".output/public" }
}
文件路由也在这个阶段处理。框架若在构建时生成路由表,产物不必保留源码路径;如果依赖运行时扫描目录,就要同时保留所需文件,并确认宿主提供相应的文件系统能力。
Git 自动构建与本地部署
控制台接 Git 仓库时,平台可以安装依赖、执行配置的构建命令,再发布产物。本地 CLI 部署则使用当前工作目录和函数入口。CLI 自己的入口打包,与控制台的 buildCommand 是两层动作。
本地发布脚本需要显式执行应用构建,再决定是否让 CLI 处理函数入口。配置里的 buildCommand 与本地 deploy 不是同一个步骤,已经生成的 worker 也可能被默认打包再次处理。
本项目由应用生成最终 worker,再用 CLI 的 --no-bundle 上传。日常页面开发继续使用 Nuxt,函数产物和边缘运行时单独验收。
开发、预览和远端验收
| 验收位置 | 主要检查 | 尚未证明的部分 |
|---|---|---|
| 框架开发服务 | 页面、接口业务、交互 | 最终包与 ESA 宿主 |
| 本机加载发布产物 | 依赖是否完整、资源路径、入口行为 | 边缘 API 支持与配额 |
| 独立 ESA 入口 | 实际加载、平台变量、外部请求和限额 | 其他节点与长期运行状况 |
Nuxt 开发服务检查页面与业务,发布产物预览检查依赖和资源,远端入口检查 ESA 的加载与运行行为。发生错误时,先确认失败的是哪一层,再检查该层的配置。
构建工具、依赖与资源的交付实验
Node 能直接执行代码,为什么还要构建
平时说「构建」,很容易把几种事情混在一起。
| 操作 | 解决什么问题 | 是否把依赖合进一个文件 |
|---|---|---|
| 语法转换 | 把 TypeScript、JSX 或较新的语法转换成目标能执行的 JavaScript | 不一定,tsc 的常规 emit 就会保留模块结构 |
| 依赖打包 | 分析入口的依赖,把需要的模块组合成一个或多个产物 | 通常会组合,但可以外置部分依赖 |
| 发布包装 | 整理资源、运行时、原生扩展、启动器和安装信息 | 可以包含 bundle,也可以保留生产依赖目录 |
三者可以组合,也可以只做其中一部分。压缩、类型检查、生成 source map 又是另外的选项。
Node.js 本身会加载模块。只要语法、模块格式和依赖都符合目标版本的要求,一个使用 ESM 或 CommonJS 的项目完全可以直接执行,不需要先交给 Webpack 或 esbuild。
较新的 Node.js 还支持直接擦除部分 TypeScript 类型,但它有自己的版本和语法限制,也不代替类型检查。例如需要转换的枚举、路径别名等场景,仍需按具体版本确认处理方式。不能简单地把「用了 TypeScript」等同于「必须打成一个 JavaScript 文件」。Node.js TypeScript 文档明确区分了类型擦除和完整工具链支持。
本地链接只解决开发时的查找
假设有两个并排的项目:
workspace/
├─ local-helper/
│ ├─ package.json # 入口指向已经生成的 dist/index.mjs
│ └─ dist/index.mjs
└─ native-host/
├─ package.json # local-helper 使用本地 link 依赖
└─ src/index.mjs
只要链接有效、local-helper 的入口存在,Node 就能直接加载它。库自己从 TypeScript 生成 dist/,和应用把这个库再打进 bundle,是两次不同的构建。
困难发生在交付时:把 native-host/src/index.mjs 发给用户,用户机器上并没有旁边那个 local-helper。原来成功的依赖查找就断了。
这构成了打包的一个强理由:让交付物不依赖开发机的目录关系。但它仍然有替代方案,例如发布 npm 包、提供包文件,或者整理一份包含生产依赖的发布目录。
把本地包发到 npm,解决的是「依赖从哪里取得」。如果用户通过包管理器安装,它可以继续作为运行时依赖;如果希望用户下载产物后直接启动,仍然要决定是否随包提供依赖、是否 bundle,以及 Node 运行时从哪里来。
哪些场景值得引入构建
我更愿意从交付要求出发选择,而不是给所有 Node 项目套一份构建脚本。
| 场景 | 比较合适的起点 |
|---|---|
| 开发者自用的小脚本,环境固定 | 直接运行;只在语法或维护需要时增加转换 |
| 能在部署阶段安装依赖的 Node 服务 | 源码或转译产物,加锁定的生产依赖 |
| 发给用户的 CLI、Native Host | 优先评估 bundle,减少安装步骤与外部目录依赖 |
| serverless Node 函数 | 按平台契约整理入口、包体与外置依赖,可能由平台负责打包 |
| npm 工具库 | 先确定公开入口、模块格式、类型声明与依赖策略,不默认把所有依赖内联 |
| Electron 应用 | 分进程构建代码,再由桌面应用工具整理运行时与安装包 |
bundle 的收益也不只体积。较少的文件可以减少分发时的遗漏和文件读取;把特定版本的纯 JavaScript 依赖编入产物,也能减少用户安装环境对结果的影响。CI 可以对这份产物验收,再交付同一份文件。
但收益有边界。CommonJS、动态加载和有副作用的依赖,未必能充分 tree shaking。打包可能增加包装代码,也增加构建配置和调试成本。启动速度是否改善,要在真实项目里测,不能从「变成一个文件」直接推出性能结论。
如果保留生产依赖目录更适合项目,也有成熟工具可用。例如符合 workspace 结构的项目,可以使用 pnpm deploy 生成包含依赖的可搬运目录,并用 --prod 排除开发依赖。它处理的是发布目录,不会把所有模块转换成一个 JS 文件;也不能把它对 workspace 的支持直接推广成任意外部 link: 都已处理。
平台构建、框架构建和应用打包分别负责什么
部署工具可能包含入口打包器,框架也可能提供自己的构建流程。配置时先确定谁负责语法、文件约定、依赖解析和资源整理,再决定由哪个入口发起。
| 构建步骤 | 处理内容 | 需要核对的边界 |
|---|---|---|
| 框架构建 | 组件编译、自动导入、文件路由、静态资源和目标入口 | 通用 bundler 不会自动理解全部框架约定 |
| 应用打包 | 依赖入口、平台、模块格式、资源与必要适配 | 输出需要满足实际宿主的接口与加载方式 |
| 部署工具 | 执行配置的命令,或者收集并上传已有产物 | 具体工具是否还会改写代码,须按其契约确认 |
这些职责可以由一个工具串行完成,也可以拆成多个步骤。项目已经需要框架编译或自定义依赖适配时,不应因为部署工具带了 bundler,就假定它能替代所有前置处理。
构建发生在本机还是云端,是另一个选择。CI 可以执行同一份锁定依赖与构建命令;验收应围绕真正交付的文件进行,避免验收后的代码又被第二套默认配置重新解析或改写。
本地链接换成 npm 版本,只改变依赖如何取得。网页、路由、资源和运行时适配仍由各自的构建阶段处理。
文件路由在打包之后怎样保留
「文件即路由」通常是框架的输入约定,不要求线上继续保留全部源码文件。例如 server/api/hello.get.ts 可以在构建时被识别为 GET /api/hello,然后转换成路由表和处理函数。产物变成一个或多个 JavaScript 文件后,请求路径仍然可以按这张表匹配。
Nitro 的路由文档明确说明,文件路由会在构建时编译并拆分代码。这里必须先执行框架的构建流程,再把兼容目标宿主的产物交给部署工具。只用 esbuild 打包一个任意入口,不会自动触发 Nitro 的目录扫描、自动导入与路由生成。
另一种实现会在运行时通过 readdir 扫描处理器目录,或者根据请求路径拼接文件名再加载。此时目录本身确实是运行时输入,合并文件可能破坏约定。可以保留框架规定的输出结构,使用受支持的部署适配器,或在构建期生成显式路由表;不能只改成 ESM 就认为路由已经保留。
目标平台还必须能运行框架生成的入口。Node HTTP 监听入口、fetch 处理函数和静态目录分别对应不同的部署约定;应使用适合目标的框架输出或适配器,不能仅靠打包或改名互换。
ncc 和 esbuild,都能构建 Node 程序
两者的区别,主要在于工具替你处理了多少分发细节。先看各自负责的范围,再用实际产物判断是否适合项目。
ncc:把 Node 程序的常见打包需求封装起来
Vercel ncc 面向 Node.js 模块和依赖的打包,支持 JavaScript、TypeScript,也支持输出 ESM。它在内部使用 Webpack,同时提供资源追踪、外置依赖、source map 等能力,使用者通常不需要自己维护一整份 Webpack 配置。
最简单的命令很短:
ncc build src/index.cjs -o dist
这里选择 .cjs 入口,对应输出也使用 .cjs。具体入口与输出规则仍要结合文件扩展名、包的 type 和 ncc 版本确认。
Umi 的依赖预打包也有使用 ncc 的源码实例。它的 scripts/bundleDeps.ts 提供了调用 ncc(entry, options) 的路径,接收返回的 code 和 assets,再整理到目标位置。
准确的说法是:Umi 的部分依赖预打包使用了 ncc。不能进一步推成 Umi 的所有代码或使用 Umi 开发的网页都由 ncc 构建。看到 __webpack_require__ 等样板,只能判断产物带有 Webpack 的运行时代码;是否由 ncc 发起,还要看构建入口。
esbuild:同样适用,配置责任更明确
esbuild 并不局限于前端。它的官方 Node 打包说明直接提供了 Node 场景的用法。
如果项目已经使用 esbuild,或者希望明确掌握入口、模块格式、资源搬运和外置规则,继续用它很合理。对普通纯 JavaScript 依赖,没有必要因为目标是 Node 就另外换成 ncc。
| 关注点 | ncc | esbuild |
|---|---|---|
| 常见定位 | 封装过的 Node 模块打包工具 | 通用构建器,可显式选择 Node 平台 |
| 底层构建器 | Webpack | esbuild 自身 |
| 配置方式 | 少量 CLI 选项或 JavaScript API | CLI 或 JavaScript API,适合清楚定义构建边界 |
| 运行时资源 | 带资源追踪,但受版本、文件类型和代码形态限制 | fs 运行时读文件等场景通常需要显式整理资源 |
| 外部依赖 | 支持 --external 等选项 |
支持 external、packages: 'external' |
| 选型时怎样确认 | 实际构建并检查所有输出文件 | 实际构建并检查所有输出文件 |
这两个工具都不会自动把 Node 可执行文件装进 JS bundle。工具选择也不等于已经解决原生扩展、系统程序和安装器的问题。
把体积当作产物的约束
2026-10-29 读取本项目已有的压缩构建文件,函数 JS 为 3,321,770 字节。这是文件本身的大小,不是网络压缩结果,也不代表 SDK 或 node_modules 单独占了这么多。口径见函数体积原始记录。
这个量级说明体积检查应放到构建末尾。可以借用前端的分析方法:审查依赖图,清理无用分支,确认可裁剪入口,开启适当压缩,再对照行为和字节数。CommonJS、副作用和动态加载也可能限制 tree shaking,不能承诺打包后一定足够小。
超限时,不能把不支持的模块简单标成 external 来骗过构建。外置把加载责任交给宿主;宿主不提供它,产物就从构建失败变成运行失败。保留资源、缩减依赖与使用成熟宿主能力,需要一起验收。gzip、Brotli 和 CDN 压缩改善传输,不自动替代平台代码大小检查。当前只建立体积基线,尚未测量优化收益。
打包后,哪些东西仍然要交付
把 JS 依赖打进 bundle,只覆盖了交付物的一部分。
内置模块和 npm 依赖,外置后的责任不同
node:crypto 外置后,完整 Node.js 会提供实现;some-library 外置后,运行时通常还要能找到对应的包。二者都叫 external,交付要求却不同。
esbuild 的 packages: 'external' 可以把包依赖留给运行时。这适合希望保留 Node 原生依赖加载的项目,但此时仍然需要生产 node_modules 或宿主明确提供的依赖。不能为了消除构建错误,把所有模块都 external,然后只交付一个入口文件。
动态插件同样需要设计边界。require(variable)、根据目录扫描加载的插件、运行时才知道名称的模块,可能无法靠静态分析枚举。如果功能依赖开放的插件目录,就应保留明确的插件发现和安装机制。
资源文件不会因为生成了 JS 而消失
例如这个路径明确指向程序旁边的文件:
fs.readFileSync(path.join(__dirname, 'message.txt'), 'utf8');
esbuild 打包成功后,目标目录里仍然要有 message.txt,或者由应用采用另一种已经验证的资源加载方式。模板、WASM、worker 子入口以及外部可执行程序,也都需要检查。
ncc 提供资源追踪,但也要验收。我在 ncc 0.45.0 下测了两份内容相同、都按 CommonJS 执行的文件:
| 源文件扩展名 | ncc 的资源输出 | 运行结果 |
|---|---|---|
.js |
自动输出 message.txt |
成功 |
.cjs |
没有输出该文件 | ENOENT;手动复制后成功 |
对应版本源码中的资源 loader 匹配式包含 .js、.mjs、.ts、.tsx、.node,没有包含 .cjs。这能解释当前夹具的差异,同时也说明:支持 CommonJS 代码打包,和覆盖所有扩展名的资源追踪,是不同的能力。
使用 ncc 的 JavaScript API 时,返回的 assets 也属于产物,必须写到交付目录。不能只保存 code。
原生扩展、运行时和桌面安装包
像图像处理等依赖可能包含 .node 扩展及配套二进制。它们需要匹配目标操作系统、CPU 架构,以及相应的 ABI 或 Node-API 支持范围。把 JavaScript 外壳合并起来,不会消除这些要求。
Native Host 还需要自己的安装、启动和浏览器注册信息。如果目标用户没有 Node,就需要随交付物提供受支持的运行时,或者另外评估可执行文件方案。Node 的 Single Executable Applications属于另一层能力,有独立的版本和资源加载限制;普通 esbuild/ncc bundle 本身不会变成 .exe。
Electron 也是类似的分层结构:JavaScript 可以先构建,随后还要把应用与 Electron 运行时整理成分发包。Electron 的应用包装文档说明了目录和 ASAR 两种形式。ASAR 把文件归档起来,不能代替语法转换、依赖打包或 tree shaking。
Electron 的主进程与渲染进程还要分开看:主进程运行在 Node 环境,渲染进程执行网页,沙箱 preload 有进一步的能力限制,整个项目不能套同一份 Node 配置。具体边界见进程模型。原生扩展则要核对 Electron 的兼容要求;必要时用官方推荐的 @electron/rebuild 处理。
减少文件,也要保留维护信息
产物还应包含必要的许可证和声明。source map 怎样分发、日志如何定位源码、依赖漏洞修复后如何重新构建,也需要有明确做法。打进 bundle 的依赖更新后,要重新生成并发布产物,用户目录里已经不存在一个可以单独更新的同名 npm 包。
最小化时也不要把运行时凭据编译进去。构建期静态替换和运行时读取环境变量,是两种配置方式,和是否使用 bundle 没有必然绑定关系。
产物应在怎样的环境里验收
最容易产生错误信心的测试,是在开发仓库里运行刚打好的文件。它可能继续向上找到仓库的 node_modules,或者通过链接访问相邻项目。
这组最小实验专门检查交付后能否独立运行。输入、链接包和交付目录全部创建在开发仓库之外;每个产物从自己的目录启动,清除子进程的 NODE_PATH、NODE_OPTIONS。外置依赖的场景会明确复制生产依赖,其余独立 bundle 场景不提供 node_modules。
验证环境是 Windows x64、Node.js 26.12.0、esbuild 0.28.2、ncc 0.45.0。这些结果证明的是固定夹具的行为;示例中的 Node.js 22.22 语法目标,需要再通过最低版本 CI 验证,不能拿 Node.js 26 的执行结果代替。
| 交付方式或条件 | 实际结果 |
|---|---|
| 源码能访问本地链接包 | Node 直接执行成功 |
| 只复制源码入口 | 找不到本地依赖,MODULE_NOT_FOUND |
| esbuild,Node 平台,CJS bundle | 无生产 node_modules 也可执行 |
| ncc,CJS bundle | 无生产 node_modules 也可执行 |
| esbuild,browser,再外置内置模块 | 能执行,但选错了包的实现 |
| esbuild,CJS 入口打成 ESM | 内置模块的动态 require 执行失败 |
| 普通 CJS 包内联进 ESM | 正常执行,说明 CJS 打包本身受支持 |
| ESM 产物显式使用 createRequire | 在完整 Node 中正常执行 |
| 固定 crypto 引用转为原生 import | 正常执行,产物不依赖宿主 require |
| 原生 ESM 入口内联普通 CJS 包 | 内置模块 import 与普通 CJS 包可共同工作 |
| 原生 ESM 入口,包依赖外置并交付 | Node 正常加载 CommonJS 包 |
| esbuild 产物漏掉模板 | 读取失败;复制模板后成功 |
ncc 的 .js / .cjs 资源读取夹具 |
资源输出不同;缺失文件需补齐 |
测试脚本共断言了 19 个场景,预期失败也计入其中。新增加载场景同时记录 external 的实际加载类型。完整结果见19 个场景的原始记录。
这组极小夹具在当前记录中生成的 esbuild CJS bundle 是 916 字节,ncc CJS bundle 是 2127 字节,均未开启压缩和 source map。这个数字只适合检查夹具产物,不适合比较两种工具的真实项目表现,路径注释等细节也会影响字节数。原始入口虽然更小,却缺少依赖,不能当成完整交付物参与体积比较。
完整源码可以在下面浏览。复制 copy/ 的全部文件到独立目录后,执行 pnpm install --frozen-lockfile 和 pnpm test,会生成自己的验证记录。这里展示源码,浏览器不会执行 Node 子进程实验。
真实项目还应覆盖 CLI 参数或服务启动、关键业务路径、运行时资源、最低 Node 版本,以及承诺支持的系统和架构。如果程序依赖数据库或平台 API,模块加载成功也只是其中一层验收。
ESA 函数 JavaScript 体积快照与限制口径
打开原文件 esa-delivery-boundary.json
这是对已有产物的只读快照,没有执行生产构建或部署。官方使用 MB,记录保留字节数,不自行改写成压缩包限额。
字节数与口径
{
"checkedAt": "2026-10-28T16:33:16.044Z",
"artifact": ".edge/worker.js",
"javascriptBytes": 3321770,
"build": "现有项目已生成的 neutral / ESM / minify 产物;本次只读取文件大小,没有重建或部署",
"source": "https://help.aliyun.com/zh/edge-security-acceleration/esa/user-guide/what-is-functions-and-pages/",
"documentedLimit": "每个函数的 JavaScript 代码文件大小上限为 4 MB;未自行假定 MB 与 MiB 的换算",
"excludes": [
"Nuxt 静态目录",
"整个 deploy package",
"gzip/brotli 传输后大小",
"整个 node_modules 目录"
]
}Node.js 构建与隔离分发的验证记录
记录包含工具版本、平台、各场景退出结果、输出文件与字节数。所有操作只针对临时夹具,不调用业务接口。
CJS 资源夹具保留了失败记录及手动复制后的结果;.js 对照夹具的内容与静态 .cjs 夹具一致。后续补充了普通 CJS 内联、createRequire、固定原生模块桥接和原生 ESM 调用 CJS 四个场景。这里没有测量性能,也没有验证原生扩展、安装器、ESA 或其他操作系统。
19 个场景的原始结果
{
"schemaVersion": 1,
"verifiedAt": "2026-10-28T11:46:27.554Z",
"runtime": {
"node": "26.12.0",
"platform": "win32",
"arch": "x64"
},
"tools": {
"esbuild": "0.28.2",
"ncc": "0.45.0"
},
"scope": "仅验证最小 Node 夹具;不包含 Node 可执行文件、原生扩展、安装包或性能比较。",
"cases": [
{
"id": "source-with-local-link",
"status": 0,
"stdout": "{\"text\":\"hello node\",\"implementation\":\"node\",\"digest\":\"c3f5abe3e11d87d645b9e9fda1bad6a8d2f9e54f7e81478138ac134ba7ac7280\"}"
},
{
"id": "source-without-dependencies",
"status": 1,
"expected": "MODULE_NOT_FOUND",
"stdout": "",
"errorLine": "code: 'MODULE_NOT_FOUND',",
"files": [
{
"path": "index.cjs",
"bytes": 268
}
],
"bytes": 268
},
{
"id": "esbuild-node-cjs",
"status": 0,
"expected": "\"implementation\":\"node\"",
"stdout": "{\"text\":\"hello node\",\"implementation\":\"node\",\"digest\":\"c3f5abe3e11d87d645b9e9fda1bad6a8d2f9e54f7e81478138ac134ba7ac7280\"}",
"files": [
{
"path": "index.cjs",
"bytes": 916
}
],
"bytes": 916,
"externalImports": [
{
"path": "node:crypto",
"kind": "require-call",
"external": true
}
]
},
{
"id": "esbuild-browser-unresolved-builtin",
"buildFailed": true,
"errors": [
"Could not resolve \"node:crypto\""
]
},
{
"id": "esbuild-browser-with-external-builtin",
"status": 0,
"expected": "\"implementation\":\"browser\"",
"stdout": "{\"text\":\"hello node\",\"implementation\":\"browser\",\"digest\":\"c3f5abe3e11d87d645b9e9fda1bad6a8d2f9e54f7e81478138ac134ba7ac7280\"}",
"files": [
{
"path": "index.cjs",
"bytes": 913
}
],
"bytes": 913,
"externalImports": [
{
"path": "node:crypto",
"kind": "require-call",
"external": true
}
]
},
{
"id": "esbuild-cjs-entry-to-esm",
"status": 1,
"expected": "Dynamic require of \"node:crypto\" is not supported",
"stdout": "",
"errorLine": "Error: Dynamic require of \"node:crypto\" is not supported",
"files": [
{
"path": "index.mjs",
"bytes": 1447
}
],
"bytes": 1447,
"externalImports": [
{
"path": "node:crypto",
"kind": "require-call",
"external": true
}
]
},
{
"id": "esbuild-pure-cjs-to-esm",
"status": 0,
"expected": "\"implementation\":\"node\"",
"stdout": "{\"text\":\"hello node\",\"implementation\":\"node\"}",
"files": [
{
"path": "index.mjs",
"bytes": 982
}
],
"bytes": 982,
"externalImports": []
},
{
"id": "esbuild-esm-with-create-require",
"status": 0,
"expected": "\"implementation\":\"node\"",
"stdout": "{\"text\":\"hello node\",\"implementation\":\"node\",\"digest\":\"c3f5abe3e11d87d645b9e9fda1bad6a8d2f9e54f7e81478138ac134ba7ac7280\"}",
"files": [
{
"path": "index.mjs",
"bytes": 1569
}
],
"bytes": 1569,
"externalImports": [
{
"path": "node:crypto",
"kind": "require-call",
"external": true
}
]
},
{
"id": "esbuild-esm-with-fixed-native-import",
"status": 0,
"expected": "\"implementation\":\"node\"",
"stdout": "{\"text\":\"hello node\",\"implementation\":\"node\",\"digest\":\"c3f5abe3e11d87d645b9e9fda1bad6a8d2f9e54f7e81478138ac134ba7ac7280\"}",
"files": [
{
"path": "index.mjs",
"bytes": 1265
}
],
"bytes": 1265,
"externalImports": [
{
"path": "node:crypto",
"kind": "import-statement",
"external": true
}
]
},
{
"id": "esbuild-native-esm-with-bundled-cjs",
"status": 0,
"expected": "\"implementation\":\"node\"",
"stdout": "{\"text\":\"hello node\",\"implementation\":\"node\",\"digest\":\"c3f5abe3e11d87d645b9e9fda1bad6a8d2f9e54f7e81478138ac134ba7ac7280\"}",
"files": [
{
"path": "index.mjs",
"bytes": 2109
}
],
"bytes": 2109,
"externalImports": [
{
"path": "node:crypto",
"kind": "import-statement",
"external": true
}
]
},
{
"id": "esbuild-esm-with-external-package",
"status": 0,
"expected": "\"implementation\":\"node\"",
"stdout": "{\"text\":\"hello node\",\"implementation\":\"node\",\"digest\":\"c3f5abe3e11d87d645b9e9fda1bad6a8d2f9e54f7e81478138ac134ba7ac7280\"}",
"files": [
{
"path": "index.mjs",
"bytes": 341
},
{
"path": "node_modules/demo-text-tools/browser.cjs",
"bytes": 79
},
{
"path": "node_modules/demo-text-tools/index.cjs",
"bytes": 76
},
{
"path": "node_modules/demo-text-tools/package.json",
"bytes": 124
}
],
"bytes": 620,
"includesProductionNodeModules": true
},
{
"id": "esbuild-asset-not-copied",
"status": 1,
"expected": "ENOENT",
"stdout": "",
"errorLine": "Error: ENOENT: no such file or directory, open '<sandbox>\\esbuild-runtime-asset\\message.txt'",
"files": [
{
"path": "index.cjs",
"bytes": 240
}
],
"bytes": 240
},
{
"id": "esbuild-asset-copied",
"status": 0,
"expected": "hello from asset",
"stdout": "hello from asset",
"files": [
{
"path": "index.cjs",
"bytes": 240
},
{
"path": "message.txt",
"bytes": 17
}
],
"bytes": 257
},
{
"id": "ncc-node-cjs",
"status": 0,
"expected": "\"implementation\":\"node\"",
"stdout": "{\"text\":\"hello node\",\"implementation\":\"node\",\"digest\":\"c3f5abe3e11d87d645b9e9fda1bad6a8d2f9e54f7e81478138ac134ba7ac7280\"}",
"files": [
{
"path": "index.cjs",
"bytes": 2127
}
],
"bytes": 2127,
"emittedAssets": []
},
{
"id": "ncc-destructured-asset",
"status": 1,
"expected": "ENOENT",
"stdout": "",
"errorLine": "Error: ENOENT: no such file or directory, open '<sandbox>\\ncc-destructured-asset\\message.txt'",
"files": [
{
"path": "index.cjs",
"bytes": 1972
}
],
"bytes": 1972,
"emittedAssets": []
},
{
"id": "ncc-destructured-asset-manual-copy",
"status": 0,
"expected": "hello from asset",
"stdout": "hello from asset",
"files": [
{
"path": "index.cjs",
"bytes": 1972
},
{
"path": "message.txt",
"bytes": 17
}
],
"bytes": 1989
},
{
"id": "ncc-static-cjs-asset",
"status": 1,
"expected": "ENOENT",
"stdout": "",
"errorLine": "Error: ENOENT: no such file or directory, open '<sandbox>\\ncc-static-cjs-asset\\message.txt'",
"files": [
{
"path": "index.cjs",
"bytes": 1954
}
],
"bytes": 1954,
"emittedAssets": []
},
{
"id": "ncc-static-cjs-asset-manual-copy",
"status": 0,
"expected": "hello from asset",
"stdout": "hello from asset",
"files": [
{
"path": "index.cjs",
"bytes": 1954
},
{
"path": "message.txt",
"bytes": 17
}
],
"bytes": 1971
},
{
"id": "ncc-static-js-asset",
"status": 0,
"expected": "hello from asset",
"stdout": "hello from asset",
"files": [
{
"path": "index.cjs",
"bytes": 1957
},
{
"path": "message.txt",
"bytes": 17
}
],
"bytes": 1974,
"emittedAssets": [
"message.txt"
]
}
]
}