@hyperfrontend/builder/bin/nativenative
Node SEA (single-executable application) primitives: turn an already-built CJS bin bundle into a self-contained native binary via the node --experimental-sea-config + postject pipeline.
buildNativeBin({ bin, ctx, cjsOutputPath }) orchestrates the full pipeline: validates the bin declares a CJS format, skips silently with an info log when the current host doesn't match any declared sea.platforms entry (CI orchestrates the matrix so each declared platform is built on the matching runner), generates the SEA config JSON via generateSeaConfig, runs generateSeaBlob to spawn node --experimental-sea-config <path> and emit the SEA preparation blob, resolves the Node host via resolveHostBinary (current platform only — defaults to process.execPath), and dispatches a forked inject worker via dispatchInjectWorker (resolved through resolveDefaultInjectWorkerPath) that clones the host to <outputPath>/bin/<name>.<platform>-<arch> (with .exe on Windows) and embeds the blob through postject's programmatic API. It then calls removeCodesign to strip the macOS signature the injection invalidated so the unsigned binary still runs, and finally deletes the SEA build intermediates (the config JSON + prep blob) so only the runtime binary remains in the output. Re-signing with a real Apple Developer identity is left to release tooling — applyCodesign provides an ad-hoc default for local execution. Native binaries are intentionally not auto-wired into package.json#bin; they are shipped as separate release artifacts. The currentPlatformMatches / currentPlatformTarget helpers, NODE_SEA_RESOURCE_NAME / NODE_SEA_MACHO_SEGMENT / NODE_SEA_FUSE constants, and individual primitive functions (including the standalone injectBlob) are exported for callers that need finer-grained composition.
API Reference
ƒ Functions
Defaults to
--sign - (ad-hoc signing), which is sufficient for local execution and satisfies macOS Catalina+ launch requirements without an Apple Developer identity. Release tooling can pass a real identity via inputs.identity.Parameters
| Name | Type | Description |
|---|---|---|
§inputs | ApplyCodesignInputs | Binary path and optional signing identity. |
Returns
CodesignResultExample
Ad-hoc signing the produced SEA binary on macOS
applyCodesign({ binary: '/abs/dist/libs/builder/bin/hf-build.darwin-arm64' })Pipeline (current-platform-only — cross-platform matrices are orchestrated externally):
- Validate the bin declares CJS — SEA requires a CJS bundle as the embedded script.
- Skip silently with an info log if the current host doesn't match any declared platform.
- Generate the SEA config JSON and write it to disk.
- Spawn
node --experimental-sea-config <path>to emit the SEA preparation blob. - Resolve the Node host binary for the current platform (defaults to
process.execPath). - Dispatch a forked inject worker that clones the host, embeds the blob via
- On macOS, strip the signature the injection invalidated so the unsigned binary still runs.
- Delete the SEA build intermediates (config JSON + prep blob) so only the
Native binaries are not auto-wired into
package.json#bin — they are shipped as separate release artifacts.Parameters
| Name | Type | Description |
|---|---|---|
§inputs | BuildNativeBinInputs | Bin declaration, resolved context, and the path to the already-built CJS bundle. |
Returns
Promise<BinOutput[]>native for the current platform, or [] if skipped.Example
Producing the SEA binary for the current runner
const outputs = await buildNativeBin({
bin: { name: 'hf-build', format: 'cjs', sea: { platforms: ['linux-x64'] } },
ctx: context,
cjsOutputPath: '/abs/dist/libs/builder/bin/hf-build.js',
})true when the current Node process matches one of the declared SEA target platforms — i.e., when <process.platform>-<process.arch> is present in declaredPlatforms. Builder uses this gate to skip native emission silently on runners that weren't asked to produce a binary for the current host. CI orchestrates the matrix so each declared platform is built on the matching runner.
Parameters
| Name | Type | Description |
|---|---|---|
§declaredPlatforms | unknown | Platforms declared on the bin's sea config. |
Returns
booleanExample
Gating native emission to declared targets
if (!currentPlatformMatches(bin.sea?.platforms ?? [])) {
log.info(`skipping native build for ${bin.name} on ${currentPlatformTarget()}`)
return []
}<process.platform>-<process.arch> (e.g., linux-x64). The string is shaped to match SeaPlatform but is returned as a plain string because callers commonly compare against arbitrary user-declared platform lists, which may legally include values not in the supported union.
Returns
string<platform>-<arch> for the current process.Example
Identifying the current target
const target = currentPlatformTarget() // 'linux-x64'dispatchInjectWorker(job: InjectWorkerJob, options: DispatchInjectWorkerOptions): Promise<InjectWorkerReport>
reportPath is overwritten with a temp-dir path created by this function. The worker writes a JSON report at the temp path; this function reads it after the child exits and returns the per-job statistics. If the worker exits non-zero or fails to produce a report, the function throws with a label-rich message.
The temp report directory is cleaned up before returning, regardless of success or failure.
Parameters
Returns
Promise<InjectWorkerReport>Example
Dispatching a SEA inject for hf-build
const report = await dispatchInjectWorker(
{ hostBinary, outputBinary, blobPath, resourceName, machoSegmentName, sentinelFuse, reportPath: '' },
{ workerPath: '/abs/dist/.../worker.cjs.js', label: 'hf-build' }
)node --experimental-sea-config <seaConfigPath> to produce the SEA preparation blob declared by the config's output field. Spawn errors and non-zero exit codes are surfaced as thrown
Errors with the captured stderr included; this function does not retry.Parameters
| Name | Type | Description |
|---|---|---|
§inputs | GenerateSeaBlobInputs | Resolved SEA config path + the blob path declared in that config. |
Returns
GenerateSeaBlobResultExample
Generating the SEA prep blob
const result = generateSeaBlob({
seaConfigPath: '/abs/dist/libs/builder/bin/hf-build.sea-config.json',
outputBlobPath: '/abs/dist/libs/builder/bin/hf-build.sea-prep.blob',
})node --experimental-sea-config <path>. Builder always emits
disableExperimentalSEAWarning: true to silence the runtime warning banner on every native binary invocation — published bins are expected to behave like first-class executables.Parameters
| Name | Type | Description |
|---|---|---|
§inputs | SeaConfigInputs | Resolved absolute paths for the SEA main script and output blob. |
Returns
SeaConfigDocumentExample
Generating a SEA config for a bin
const config = generateSeaConfig({
mainPath: '/abs/dist/libs/builder/bin/hf-build.cjs.js',
outputPath: '/abs/dist/libs/builder/bin/hf-build.sea-prep.blob',
})outputBinary and injects the SEA preparation blob via [postject](https://www.npmjs.com/package/postject)'s programmatic API. The host binary itself is never modified — postject mutates the cloned copy at
outputBinary. The injection uses Node's expected SEA resource name and fuse so the produced executable is recognized by Node's SEA runtime. Callers are responsible for adjusting code-signing afterwards (see removeCodesign for macOS).
Parameters
| Name | Type | Description |
|---|---|---|
§inputs | InjectBlobInputs | Host binary, target output path, and blob path. |
Example
Producing a SEA binary on a Linux runner
await injectBlob({
hostBinary: process.execPath,
outputBinary: '/abs/dist/libs/builder/bin/hf-build.linux-x64',
blobPath: '/abs/dist/libs/builder/bin/hf-build.sea-prep.blob',
})inputs.binary (via codesign --remove-signature) after postject's blob injection has invalidated it, leaving an unsigned binary that runs locally. No-op on non-macOS hosts. Re-signing the produced binary (with a real Apple Developer ID) is left to release tooling; this step only produces an unsigned executable that runs locally.
Parameters
| Name | Type | Description |
|---|---|---|
§inputs | RemoveCodesignInputs | Path to the binary to clean up. |
Returns
CodesignResultExample
Removing the signature on macOS before postject inject
removeCodesign({ binary: '/abs/dist/libs/builder/bin/hf-build.darwin-arm64' })@swc-node/register (bootstrap case where builder is building itself for the first time and the dist worker doesn't exist yet). Looks at, in order:
<workspaceRoot>/dist/libs/builder/bin/native/worker/index.cjs.js<workspaceRoot>/node_modules/@hyperfrontend/builder/bin/native/worker/index.cjs.js<workspaceRoot>/libs/builder/src/bin/native/worker/index.ts(with--require \@swc-node/register)
Parameters
| Name | Type | Description |
|---|---|---|
§workspaceRoot | string | Absolute workspace root. |
Returns
InjectWorkerInvocationundefined if no candidate exists.Example
Locating the worker for an in-workspace consumer
const invocation = resolveDefaultInjectWorkerPath('/abs/repo')
if (!invocation) throw new Error('builder inject worker artifact not found')The resolver is current-platform-only: the host binary is always
process.execPath. Cross-platform builds are orchestrated by an external CI matrix where each runner produces the binary for its own platform. Calling this function with a target platform that doesn't match the current host throws — the caller is expected to gate via currentPlatformMatches before invoking the SEA pipeline. The throw exists as a defense-in-depth check should that gate be bypassed.
Parameters
| Name | Type | Description |
|---|---|---|
§inputs | ResolveHostBinaryInputs | Target platform and optional current-host overrides for testability. |
Returns
stringExample
Resolving the host for the current runner
const host = resolveHostBinary({ platform: 'linux-x64' }) // process.execPath◈ Interfaces
Properties
blobPath:string— node --experimental-sea-config.machoSegmentName?:string— NODE_SEA.resourceName?:string— NODE_SEA_BLOB.sentinelFuse?:string— --require \@swc-node/register when the worker is loaded from TypeScript source during a bootstrap build).JSON.stringify / JSON.parse — no functions, no class instances.Properties
blobPath:string— node --experimental-sea-config.hostBinary:string— sentinelFuse:string— reportPath when the worker exits cleanly. Captured from the worker process — the parent never observes the ~138 MB postject buffer because the entire load lives and dies in the child.
Properties
Properties
node --experimental-sea-config. Mirrors the [Node SEA configuration schema] (https://nodejs.org/api/single-executable-applications.html). Builder always sets
disableExperimentalSEAWarning so the produced binary doesn't print the warning banner on every invocation.● Variables
0 → 1 to mark a SEA-injected binary. Built at runtime from two parts so the contiguous sentinel literal never appears in the bundled JS of a bin that imports this module: that JS is embedded in the SEA blob, and a contiguous copy there collides with the host binary's own sentinel, making postject reject the inject with "Multiple occurences of sentinel". Keep it split — do not inline into a single string literal.