Skip to content

feat: nodejs SEA improvements #204

Closed

Description

@robertsLando

RFC: Modernizing pkg with Official Node.js SEA Support and a Transparent VFS

Purpose

The goal is to transition pkg from a legacy "binary-patching" model to a "Native SEA Container + Virtual Bootstrap" model. This will fix long-standing issues with ESM support, native addons, and brittle Node.js version compatibility by leveraging official runtime features introduced in Node.js v20 through v25.5.0.

The Problem with Current SEA

The official Node.js SEA implementation is currently "limited" for the average developer because:

  1. Manual Bundling Required: It expects a single entry-point file. Large applications with complex node_modules must be manually rolled up (e.g., via esbuild).
  2. Opaque Asset Access: Files aren't visible to fs. Developers must rewrite code to use require('node:sea').getAsset().
  3. Native Addon Friction: .node files cannot be loaded from the blob without manual extraction to a temporary directory.

Proposed Solution

We can use the existing pkg Walker to automate the creation of a sophisticated SEA blob that "tricks" the application into thinking it is running in a normal filesystem.

1. Build-Time: Automated SEA Orchestration

Instead of pkg creating its own custom bundle, it will generate an official SEA Preparation Blob:

  • Enhanced Walker: Re-use the existing pkg walker to traverse the dependency graph and identify all necessary scripts, assets, and native addons.
  • Auto-Generated Config: pkg will generate a sea-config.json where every file found by the walker is mapped to the assets field.
  • Native Injection: Use the official Node.js --build-sea flag (Node v25.5.0+) to inject the blob, ensuring the binary is properly signed and utilizes native "fuses" for stability.

2. Runtime: The Bootstrap "Illusion"

pkg will inject a hidden Bootstrap Loader as the main script in the SEA. This script executes before the user's code to set up the virtual environment:

  • Transparent fs Patching: The bootstrap will monkey-patch node:fs (and node:fs/promises). When fs.readFileSync('./data.json') is called, the patch intercepts the request and retrieves the buffer from the SEA asset store using node:sea.
  • ESM/Module Support: By registering a custom loader via module.register(), we can handle import statements natively, pulling code directly from the SEA blob without requiring a single-file CJS rollup.
  • Automatic Native Addon Extraction: When a .node file is required, the bootstrap will:
  1. Extract the addon from the SEA blob to a temporary directory (e.g., os.tmpdir()/pkg-sea/...).
  2. Automatically load it via process.dlopen().
  3. The user's code remains unchanged.

Technical Roadmap

Phase Task Details
Build sea-config Generation Map walker results to assets dictionary.
Build Native Compilation Support V8 Bytecode via useCodeCache: true in SEA config.
Runtime Bootstrap Script Create a CJS entry point that initializes the VFS.
Runtime node:fs Polyfill Redirect standard file calls to node:sea.getAsset().
Runtime Addon Manager Automate the "Extract-and-Load" lifecycle for .node files.

Proposed Developer Experience

The user experience remains identical to the classic pkg workflow:

# pkg detects Node version and uses SEA mode automatically
pkg --sea. -t node24-win-x64

Benefits

  • Future-Proof: Moves away from brittle offsets and toward official Node.js internal APIs (FindSingleExecutableResource).
  • ESM & TS Ready: Native support for modern module graphs without complex CJS transpilation hacks.
  • Security: Better integration with OS-level code signing (especially macOS/Windows) since the binary structure follows official Node.js standards.

Activity

  1. robertsLando commented on Feb 23, 2026

    @robertsLando
    MemberAuthor

    Seems there is some work in progress for supporting VFS on Nodejs that will make this easier: nodejs/node#61478

  2. robertsLando commented on Feb 23, 2026

    @robertsLando
    MemberAuthor

    Do some investigations on: nodejs/node#61813 as well

  3. pinned this issue on Apr 7, 2026
  4. viceice commented on Apr 22, 2026

    @viceice

    it seems sea builds are not yet reproducible?

    https://yao-pkg.github.io/pkg/guide/bytecode#reproducible-builds

  5. robertsLando commented on Apr 22, 2026

    @robertsLando
    MemberAuthor

    @viceice they don't ship bytecode so them are reproducible

  6. viceice commented on Apr 22, 2026

    @viceice
    > Warning Executable /home/kriese/src/containerbase/containerbase-base/dist/cli/containerbase-cli-x64 already exists, will be overwritten
    > Warning Executable /home/kriese/src/containerbase/containerbase-base/dist/cli/containerbase-cli-arm64 already exists, will be overwritten
    
    ...
    
    
    #8 [containerbase 2/4] COPY dist/docker/ /
    #8 CACHED
    
    #9 [containerbase 3/4] COPY dist/cli/containerbase-cli-amd64 /usr/local/containerbase/bin/containerbase-cli
    #9 DONE 0.3s
    

    it seems docker doesn't like the new time stamp, it's always doing that copy step. 😕
    i think the old non sea mode didn't had that issue.
    maybe don't overwrite files if checksum is same? 🤔

  7. robertsLando commented on Apr 22, 2026

    @robertsLando
    MemberAuthor

    @viceice I'm not sure I understand what you mean here, could you be more specific pleae?

  8. viceice commented on Apr 22, 2026

    @viceice

    you can run pnpm test:docker -t build-test -l debug jb -b twice and the docker layer isn't using the cached value.
    i can verify the binaries are same when using diff old new.
    so i wonder if it's because the created / changed timestamp of the binaries 🤔

  9. viceice commented on Apr 22, 2026

    @viceice

    pnpm test:docker -t build-test -l debug jb (without -b build` it uses the cache for bulding the docker image

    #11 [containerbase 3/4] COPY dist/cli/containerbase-cli-amd64 /usr/local/containerbase/bin/containerbase-cli
    #11 CACHED
    
  10. viceice commented on Apr 22, 2026

    @viceice

    yes, it seems to be the modified timestamp wich invalidates the context copy, so it always rebuilds.

    btw: i build the binaries on host, not in a docker layer.

  11. viceice commented on Apr 22, 2026

    @viceice

    interesting, when compiling as cjs then caching works no, also doesn't work

  12. viceice commented on Apr 22, 2026

    @viceice

    some more findings when compiling as cjs:

    /snapshot/app/main.js (entrypoint) calls new Worker(path) /snapshot/app/worker.js` (woker)

    • path='./worker.js' -> Error: Cannot find module '~/dist/thread-stream-worker.js' (searches on host)
    • path=new URL('./worker.js', require("url").pathToFileURL(__filename).href) -> Error: Cannot find module '/snapshot/dist/app/worker.js'
    • path=new URL('./worker.js', require("url").pathToFileURL(__filename).href).pathname -> works
    • path='/snapshot/dist/app/worker.js' -> works

    my idea was to not hardcode the /snapshot/dist/app path inside the js code.


    ☝️ My fault. Worker takes a filename relative to process.cwd, so i need to manually need to ensure it's using full path to vfs! ⚠️

  13. viceice commented on Apr 22, 2026

    @viceice

    another strange warning when using esm target:

    > pkg@6.18.0
    > compression: Zstd
    > Walking dependencies...
    > Warning Babel parse has failed: import.meta may appear only with 'sourceType: "module"' (5:62) in /home/kriese/src/containerbase/containerbase-base/dist/app/containerbase-cli.js
    > Warning Babel parse has failed: import.meta may appear only with 'sourceType: "module"' (24:46) in /home/kriese/src/containerbase/containerbase-base/dist/app/chunk-q7NCDQ7-.js
    > Refining file records...
    
  14. robertsLando commented on Apr 22, 2026

    @robertsLando
    MemberAuthor

    could you collect all legit findings on an issue plesase? Consider also opening a PR in case you find the coolprit in the code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions