SEA: embedder main cannot dynamically import non-builtin modules on Node 25.5+
还没有人认领这个 Issue。
- 主要语言
- JavaScript
- 星标
- 122k
- 派生
- 37.4k
- 平均合并
- 4 天 3 小时
- 30 天内合并 PR
- 272
描述
Version
v25.9.0 (also reproduces on v25.7.0, v25.8.x). Does not reproduce on v24.x.
Platform
Linux 6.17.0-19-generic x86_64
Subsystem
sea, esm, embedding
What steps will reproduce the bug?
Minimal repro, no external tools beyond postject:
mkdir -p /tmp/sea-repro && cd /tmp/sea-repro
cat > user.mjs <<'JS'
console.log('hello from user module');
JS
cat > bootstrap.js <<'JS'
import('file:///tmp/sea-repro/user.mjs').catch(err => {
console.error(err);
process.exit(1);
});
JS
cat > sea-config.json <<'JSON'
{
"main": "bootstrap.js",
"output": "sea-prep.blob",
"disableExperimentalSEAWarning": true
}
JSON
node --experimental-sea-config sea-config.json
cp "$(command -v node)" ./app
npx postject ./app NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
./app
Same failure occurs when:
mainFormatis set to"module"and the bootstrap usesawait import('file:///...')- a CJS bootstrap calls
require('/abs/path.js')on a non-builtin path
How often does it reproduce? Is there a required condition?
100% on Node 25.5+. The identical repro prints hello from user module on Node 24.14.0, so this is a regression introduced somewhere in the 25.5+ window (around the --build-sea landing in #61167 and the mainFormat: "module" support in #61813).
What is the expected behavior?
Dynamic import() (and require() of absolute paths) from the SEA main should resolve through the normal module loader. This is how SEA worked from v20 through v24 and is what enables the common pattern of a small bootstrap baked into the SEA that stages setup (VFS overlays, monkey-patches, diagnostics, ...) and then hands control off to a real user entrypoint on disk.
What do you see instead?
Error [ERR_UNKNOWN_BUILTIN_MODULE]: No such built-in module: file:///tmp/sea-repro/user.mjs
at loadBuiltinModuleForEmbedder (node:internal/modules/helpers:165:9)
at getBuiltinModuleWrapForEmbedder (node:internal/modules/esm/utils:237:10)
at importModuleDynamicallyForEmbedder (node:internal/modules/esm/utils:250:10)
at importModuleDynamicallyCallback (node:internal/modules/esm/utils:282:12)
at bootstrap.js:1:1
at embedderRunCjs (node:internal/main/embedding:93:10)
at embedderRunEntryPoint (node:internal/main/embedding:128:12) {
code: 'ERR_UNKNOWN_BUILTIN_MODULE'
}
Root cause
From lib/internal/modules/esm/utils.js in v25.9.0:
// For embedder entry point ESM, only allow built-in modules.
if (referrerSymbol === embedder_module_hdo) {
return importModuleDynamicallyForEmbedder(specifier, phase, attributes, referrerName);
}
function importModuleDynamicallyForEmbedder(specifier, phase, attributes, referrerName) {
// Ignore phase and attributes for embedder ESM for now, because this only supports loading builtins.
return getBuiltinModuleWrapForEmbedder(specifier).getNamespace();
}
The CJS path has the same limitation: embedderRequire in lib/internal/main/embedding.js routes through loadBuiltinModuleForEmbedder, so require('/abs/path') from a CJS SEA main on Node 25.5+ also throws ERR_UNKNOWN_BUILTIN_MODULE.
This means the SEA main — whether mainFormat: "commonjs" or mainFormat: "module" — can only load builtin modules via its own require/import(), and cannot hand off to a user script.
Additional information
Workaround (for anyone hitting this in the wild): obtain Module via require('module') (which succeeds because module is a builtin), set process.argv[1] to the real entrypoint, then call Module.runMain(). Module.runMain uses the real CJS loader and, on Node 22.12+, transparently handles ESM entries via require(esm). Caveat: user entrypoints that use top-level await cannot go through this path — require(esm) rejects them — so there is currently no way to load a TLA-using ESM user entrypoint from an SEA main on Node 25.5+.
Request: route importModuleDynamicallyForEmbedder and embedderRequire through the default loaders (the same path source_text_module_default_hdo / vm_dynamic_import_default_internal use), so embedders — including SEA — can keep using dynamic import() / require() to hand off to a user entrypoint after setup.
Context: I'm the maintainer of yao-pkg/pkg (the maintained fork of vercel/pkg); pkg's enhanced SEA mode builds a small bootstrap that mounts a VFS and then hands off to the user entrypoint, which is exactly the pattern this regression breaks.
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
使用提供的 bootstrap.js、sea-config.json 和 postject 命令重现 SEA 失败,然后检查 lib/internal/modules/esm/utils.js 和 lib/internal/main/embedding.js,尤其是 importModuleDynamicallyForEmbedder 和 embedderRequire。将它们的行为与默认 loader 路径进行比较,并验证 SEA main 能够在保留内置模块加载的同时,将处理交给绝对路径的 ESM 和 CommonJS 入口点。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- javascript, nodejs
- 领域
- backend
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 冷清
- 描述清晰度
- 描述清楚
- 新手友好度
- 52/100