Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f7dd471
feat: enhanced SEA mode with walker integration and VFS (#204)
robertsLando Apr 7, 2026
7b55262
fix: move @platformatic/vfs to optionalDependencies for Node 20 CI co…
robertsLando Apr 7, 2026
26ddcb0
feat: drop Node.js 20 support, require Node.js >= 22
robertsLando Apr 7, 2026
69c0010
fix: restore test-00-sea Node version guard to < 20
robertsLando Apr 7, 2026
13541e1
fix: restore assertSeaNodeVersion to check Node >= 20
robertsLando Apr 7, 2026
49ad5a9
test: enhance SEA mode tests for pkg compatibility and error handling
robertsLando Apr 7, 2026
0842c82
fix: improve error handling and conditionally remove Mach-O signature…
robertsLando Apr 7, 2026
862df2a
refactor: extract shared runtime code into bootstrap-shared.js
robertsLando Apr 7, 2026
8a42a62
fix: address Copilot review feedback on SEA pipeline
robertsLando Apr 7, 2026
f539f92
fix: throw when JSON config targets Node < 22 in SEA mode
robertsLando Apr 7, 2026
6f580b6
fix: normalize paths for Windows compatibility in SEA bootstrap
robertsLando Apr 7, 2026
151f25c
docs: add technical architecture documentation
robertsLando Apr 7, 2026
e49e17e
fix: normalize all SEA provider paths to POSIX for Windows compatibility
robertsLando Apr 7, 2026
4d0ef4a
docs: mention --no-bytecode flag in architecture comparison
robertsLando Apr 7, 2026
3399b66
fix: address Copilot review feedback for SEA enhanced mode
robertsLando Apr 7, 2026
f3d47fe
feat: add DEBUG_PKG diagnostics to SEA mode, deduplicate diagnostic code
robertsLando Apr 7, 2026
e6f5753
fix: address remaining Copilot review feedback
robertsLando Apr 7, 2026
10a84db
feat: add worker thread support and fix SEA walker warnings
robertsLando Apr 7, 2026
8e612e3
docs: update architecture documentation to include worker thread supp…
robertsLando Apr 7, 2026
0808a05
fix: mount VFS at POSIX /snapshot on all platforms for Windows compat
robertsLando Apr 7, 2026
5e4a53a
fix: use Module._compile for worker threads instead of raw eval
robertsLando Apr 7, 2026
a4dcb29
fix: use postject JS API instead of npx to inject SEA blobs
robertsLando Apr 7, 2026
c2d3b91
fix: avoid eager file loading and unnecessary temp files in SEA walker
robertsLando Apr 8, 2026
2cdf100
fix: pass defaultEntrypoint in SEA bootstrap for API consistency
robertsLando Apr 8, 2026
8293cf8
feat: add snapshotify function to convert file paths to snapshot paths
robertsLando Apr 8, 2026
d7229f7
docs: fix version comments and Node 22 requirement notes
robertsLando Apr 8, 2026
1e80fed
refactor: reuse replaceSlashes from common.ts instead of duplicate to…
robertsLando Apr 8, 2026
2801ce4
fix: handle VFS-incompatible fs calls in SEA bootstrap
robertsLando Apr 8, 2026
ebc1162
feat: add SKILL.md for packaging performance benchmarks comparison
robertsLando Apr 8, 2026
3d0b118
perf: optimize SEA provider startup and add DEBUG_PKG_PERF instrument…
robertsLando Apr 8, 2026
219d978
docs: split project instructions into modular .claude/rules files
robertsLando Apr 8, 2026
8255a4a
feat: enhance SEA asset generation with single archive blob and offse…
robertsLando Apr 8, 2026
58ccfa5
docs: update README and ARCHITECTURE.md for single archive blob and r…
robertsLando Apr 8, 2026
3fb1387
fix: enable model invocation for performances-compare skill
robertsLando Apr 8, 2026
051c73b
refactor: replace @platformatic/vfs with @roberts_lando/vfs@0.3.0
robertsLando Apr 8, 2026
f64bf63
refactor: update Node.js version requirements and testing commands in…
robertsLando Apr 8, 2026
9ed8ce5
feat: track body modifications in stepStrip for enhanced record handling
robertsLando Apr 8, 2026
a536430
chore: update @roberts_lando/vfs to version 0.3.1 in package.json, pa…
robertsLando Apr 8, 2026
394deb1
chore: update @roberts_lando/vfs to version 0.3.2
robertsLando Apr 8, 2026
af5b851
fix: address Copilot review — patchDlopen regression, SEA blob fallba…
robertsLando Apr 10, 2026
f2f1d9f
refactor: use assertSeaOutput helper in test-00-sea
robertsLando Apr 10, 2026
dd99f48
fix: address Copilot review — dlopen VFS, docs, prettierignore
robertsLando Apr 13, 2026
7829973
feat(sea): support ESM entrypoints with top-level await
robertsLando Apr 14, 2026
7476440
docs(readme): document ESM entry + top-level await in SEA mode
robertsLando Apr 14, 2026
3155538
fix(sea): harden SEA pipeline — CJS bootstrap for ESM+TLA, blob gener…
robertsLando Apr 14, 2026
33d43f9
fix(sea): self-review fixes — symlink fallback, archive bounds, short…
robertsLando Apr 14, 2026
beca977
docs(sea): align README + ARCHITECTURE with single-bootstrap dispatch
robertsLando Apr 14, 2026
2ac0177
perf(sea): read SEA blob once and share buffer across targets
robertsLando Apr 14, 2026
90352a3
fix(sea): address copilot review — mixed-major guard, emitWarning lea…
robertsLando Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false # prevent test to stop if one fails
matrix:
node-version: [20.x, 22.x, 24.x]
node-version: [22.x, 24.x]
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
Expand All @@ -23,7 +23,7 @@ jobs:

- run: yarn install

- if: matrix['node-version'] == '20.x' && matrix['os'] == 'ubuntu-latest'
- if: matrix['node-version'] == '22.x' && matrix['os'] == 'ubuntu-latest'
run: yarn lint
- run: yarn build
test_host:
Expand All @@ -36,11 +36,6 @@ jobs:
with:
npm_command: test:22

test_20:
uses: ./.github/workflows/test.yml
with:
npm_command: test:20

test_24:
uses: ./.github/workflows/test.yml
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false # prevent test to stop if one fails
matrix:
node-version: [20.x, 22.x, 24.x]
node-version: [22.x, 24.x]
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/update-dep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Use Node.js 20
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x
cache: 'yarn'

- name: Update dependency
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ examples/express/node_modules

# Editors
yarn-error.log
tsconfig.tsbuildinfo
tsconfig.tsbuildinfo
prelude/sea-bootstrap.bundle.js
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ After installing it, run `pkg --help` without arguments to see list of options:
--no-native-build skip native addons build
--no-dict comma-separated list of packages names to ignore dictionaries. Use --no-dict * to disable all dictionaries
-C, --compress [default=None] compression algorithm = Brotli or GZip
--sea (Experimental) compile give file using node's SEA feature. Requires node v20.0.0 or higher and only single file is supported
--sea (Experimental) compile using node's SEA feature. With package.json input and node >= 22, uses enhanced mode with full dependency walking and VFS

Examples:

Expand All @@ -58,7 +58,7 @@ After installing it, run `pkg --help` without arguments to see list of options:
– Makes executable for particular target machine
$ pkg -t node22-win-arm64 index.js
– Makes executables for target machines of your choice
$ pkg -t node20-linux,node22-linux,node22-win index.js
$ pkg -t node22-linux,node24-linux,node24-win index.js
– Bakes '--expose-gc' and '--max-heap-size=34' into executable
$ pkg --options "expose-gc,max-heap-size=34" index.js
– Consider packageA and packageB to be public
Expand Down Expand Up @@ -87,9 +87,9 @@ The entrypoint of your project is a mandatory CLI argument. It may be:
`pkg` can generate executables for several target machines at a
time. You can specify a comma-separated list of targets via `--targets`
option. A canonical target consists of 3 elements, separated by
dashes, for example `node20-macos-x64` or `node22-linux-arm64`:
dashes, for example `node22-macos-x64` or `node24-linux-arm64`:

- **nodeRange** node20, node22, node24 or latest
- **nodeRange** node22, node24 or latest
- **platform** alpine, linux, linuxstatic, win, macos, (freebsd)
- **arch** x64, arm64, (armv6, armv7)

Expand Down
405 changes: 405 additions & 0 deletions docs/ARCHITECTURE.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = [
'test/test-51-esm-import-meta/esm-module/test-import-meta-basic.js',
'lib/log.js', // ESM re-export file
'test/test-50-extensions/test-y-esnext.js', // ESM test file
'prelude/sea-bootstrap.bundle.js', // Generated by esbuild
],
},

Expand Down Expand Up @@ -96,6 +97,14 @@ module.exports = [
files: ['prelude/**/*'],
rules: {
strict: 'off',
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},

Expand Down
106 changes: 58 additions & 48 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from 'assert';
import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
import { existsSync, readFileSync, copyFileSync } from 'fs';
import { mkdir, readFile, rm, stat } from 'fs/promises';
import minimist from 'minimist';
import { need, system } from '@yao-pkg/pkg-fetch';
Expand All @@ -16,9 +16,9 @@ import { shutdown } from './fabricator';
import walk, { Marker, WalkerParams } from './walker';
import { Target, NodeTarget, SymLinks } from './types';
import { CompressType } from './compress_type';
import { patchMachOExecutable, signMachOExecutable } from './mach-o';
import { signMachOExecutable } from './mach-o';
import pkgOptions from './options';
import sea from './sea';
import sea, { seaEnhanced, signMacOSIfNeeded } from './sea';

const { version } = JSON.parse(
readFileSync(path.join(__dirname, '../package.json'), 'utf-8'),
Expand All @@ -28,6 +28,23 @@ function isConfiguration(file: string) {
return isPackageJson(file) || file.endsWith('.config.json');
}

function buildMarker(
configJson: Record<string, unknown> | undefined,
config: string,
inputJson: Record<string, unknown> | undefined,
input: string,
): Marker {
const marker: Marker = configJson
? { config: configJson, base: path.dirname(config), configPath: config }
: {
config: inputJson || {},
base: path.dirname(input),
configPath: input,
};
marker.toplevel = true;
return marker;
}

// http://www.openwall.com/lists/musl/2012/12/08/4

const {
Expand Down Expand Up @@ -529,10 +546,41 @@ export async function exec(argv2: string[]) {
}

if (argv.sea) {
await sea(inputFin, {
targets,
signature: argv.signature,
});
const minTargetMajor = Math.min(
...targets.map((t) => {
const v = parseInt(t.nodeRange.replace('node', ''), 10);
// Treat unparseable ranges (e.g. "latest") as current host major
return Number.isNaN(v) ? parseInt(process.version.slice(1), 10) : v;
}),
);

if ((inputJson || configJson) && minTargetMajor < 22) {
throw wasReported(
'Enhanced SEA mode requires Node >= 22 targets. ' +
`Minimum target version resolved to Node ${minTargetMajor}.`,
);
}

if (inputJson || configJson) {
// Enhanced SEA mode — use walker pipeline
const marker = buildMarker(configJson, config, inputJson, input);

pkgOptions.set(configJson?.pkg ?? inputJson?.pkg);

await seaEnhanced(inputFin, {
targets,
signature: argv.signature,
marker,
params: { seaMode: true },
addition: isConfiguration(input) ? input : undefined,
});
} else {
// Simple SEA mode — plain .js file without package.json
await sea(inputFin, {
targets,
signature: argv.signature,
});
}
return;
}

Expand Down Expand Up @@ -596,23 +644,8 @@ export async function exec(argv2: string[]) {

let marker: Marker;

if (configJson) {
pkgOptions.set(configJson?.pkg);
marker = {
config: configJson,
base: path.dirname(config),
configPath: config,
};
} else {
pkgOptions.set(inputJson?.pkg);
marker = {
config: inputJson || {}, // not `inputBin` because only `input`
base: path.dirname(input), // is the place for `inputJson`
configPath: input,
};
}

marker.toplevel = true;
pkgOptions.set(configJson?.pkg ?? inputJson?.pkg);
marker = buildMarker(configJson, config, inputJson, input);

// public

Expand Down Expand Up @@ -684,30 +717,7 @@ export async function exec(argv2: string[]) {
});

if (target.platform !== 'win' && target.output) {
if (argv.signature && target.platform === 'macos') {
// patch executable to allow code signing
const buf = patchMachOExecutable(readFileSync(target.output));
writeFileSync(target.output, buf);

try {
// sign executable ad-hoc to workaround the new mandatory signing requirement
// users can always replace the signature if necessary
signMachOExecutable(target.output);
} catch {
if (target.arch === 'arm64') {
log.warn('Unable to sign the macOS executable', [
'Due to the mandatory code signing requirement, before the',
'executable is distributed to end users, it must be signed.',
'Otherwise, it will be immediately killed by kernel on launch.',
'An ad-hoc signature is sufficient.',
'To do that, run pkg on a Mac, or transfer the executable to a Mac',
'and run "codesign --sign - <executable>", or (if you use Linux)',
'install "ldid" utility to PATH and then run pkg again',
]);
}
}
}

await signMacOSIfNeeded(target.output, target, argv.signature);
await plusx(target.output);
}
}
Expand Down
8 changes: 7 additions & 1 deletion lib/packer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const bootstrapText = readFileSync(

const commonText = readFileSync(require.resolve('./common'), 'utf8');

const sharedText = readFileSync(
require.resolve('../prelude/bootstrap-shared.js'),
'utf8',
);

const diagnosticText = readFileSync(
require.resolve('../prelude/diagnostic.js'),
'utf8',
Expand Down Expand Up @@ -169,10 +174,11 @@ export default function packer({
}
}
const prelude =
`return (function (REQUIRE_COMMON, VIRTUAL_FILESYSTEM, DEFAULT_ENTRYPOINT, SYMLINKS, DICT, DOCOMPRESS) {
`return (function (REQUIRE_COMMON, REQUIRE_SHARED, VIRTUAL_FILESYSTEM, DEFAULT_ENTRYPOINT, SYMLINKS, DICT, DOCOMPRESS) {
${bootstrapText}${
log.debugMode ? diagnosticText : ''
}\n})(function (exports) {\n${commonText}\n},\n` +
`(function () { var module = { exports: {} };\n${sharedText}\nreturn module.exports; })(),\n` +
`%VIRTUAL_FILESYSTEM%` +
`\n,\n` +
`%DEFAULT_ENTRYPOINT%` +
Expand Down
Loading
Loading