跳到正文

ESA Pages 体验之旅(2):部署静态页面和 API

ESA Pages 体验之旅 · 1 · 第 2 篇(当前) · 3 · 4 · 5 · 6 · 7 · 8 · 9

ESA Pages 可以同时交付网页和轻量 API,但它接收的内容有明确结构:浏览器使用静态资源目录,边缘运行时加载 JavaScript 函数入口。一个带 TypeScript、npm 依赖和前端框架的仓库,需要先整理成这两份产物。

这个项目采用 Nuxt 静态前端和同源 API。本地保留 Vue 的开发体验,部署时分别生成静态文件和函数入口。

静态资源与函数入口

浏览器请求 HTML、CSS、图片和前端 JavaScript 时,由静态资源服务返回文件;请求 /api/* 时,由函数处理业务,再访问外部服务。服务端凭据留在函数配置中。

静态页面与 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 应用 分进程构建代码,再由桌面应用工具整理运行时与安装包

Node 程序的三种交付方式:源码与生产依赖、JavaScript bundle 与资源、包含运行时的安装包

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,ncc 内部调用 Webpack;应用页面构建是另一条链路

准确的说法是: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 0.45.0 下,js 自动搬运资源,cjs 缺少资源,补齐后恢复运行

使用 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 构建与隔离分发的验证记录

打开原文件 verification.json

记录包含工具版本、平台、各场景退出结果、输出文件与字节数。所有操作只针对临时夹具,不调用业务接口。

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"
      ]
    }
  ]
}