jkunz 6fe71505f3 fix: bound build-time Node.js runs and refuse pointless hostLibraries
- buildTimeoutMs (default 10 minutes) kills a hung `node --build-sea`
  or version check and fails with SEA_BUILD_FAILED; smoke.timeoutMs and
  buildTimeoutMs share one check (positive integer up to 2^31-1).
- hostLibraries entries naming a glibc library of every architecture or
  a library the target's Node.js needs itself are refused with
  CONFIG_INVALID before any file is read or any target writes output,
  instead of being dropped silently; libmvec.so.1 stays allowed.
- The ELF parser reads a dynamic string table once.
- package.json files are generated from packageFiles.
2026-10-15 17:57:25 +00:00

@git.zone/tssea

Builds Node.js single executable applications (SEAs) for linux, macOS and Windows from one Linux or macOS host. It is the successor of @git.zone/tsdeno.

From a bundle that @git.zone/tsbundle wrote for Node.js, tssea:

  1. Downloads the official Node.js executable of every target and checks it against a SHA-256 that tssea pins for the release.
  2. Selects the target's prebuilt native addons and embeds them, together with your asset trees and sidecar executables, through the @push.rocks/smartsea asset layout.
  3. Checks that every embedded Linux addon and sidecar loads wherever the pinned Node.js loads (see Linux compatibility), then strips Linux executables before the injection.
  4. Runs node --build-sea with the pinned Node.js of the host. The build uses an ESM main, execArgvExtension: "none", and code cache and snapshot off on every target.
  5. Signs macOS executables ad hoc and removes the stale Authenticode certificate table of Windows executables.
  6. Checks the result: a size floor and budget, a smoke run, and optionally a byte-identical rebuild. It writes third-party notices next to every executable.

Supported: builds run on linux-x64, linux-arm64, darwin-x64 and darwin-arm64 hosts with Node.js 22 or later. The build itself always uses the pinned Node.js release, never the Node.js that runs tssea. Deno is not supported and not tested.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

pnpm add --save-dev @git.zone/tssea @git.zone/tsbundle
pnpm add @push.rocks/smartsea   # the in-executable runtime your bundle imports

Quick start

.smartconfig.json:

{
  "@git.zone/tsbundle": {
    "bundles": [
      { "from": "./ts/cli.ts", "to": "./dist_node/main.mjs", "bundler": "esbuild", "platform": "node", "sea": true, "production": true }
    ]
  },
  "@git.zone/tssea": {
    "nodeVersion": "26.11.1",
    "bundle": "./dist_node/main.mjs",
    "outDir": "./dist/binaries",
    "targets": [
      { "target": "linux-x64", "name": "myapp-linux-x64" },
      { "target": "linux-arm64", "name": "myapp-linux-arm64" },
      { "target": "darwin-arm64", "name": "myapp-macos-arm64" },
      { "target": "win32-x64", "name": "myapp-windows-x64.exe" }
    ],
    "execArgv": ["--use-system-ca"],
    "smoke": { "args": ["--version"], "expectStdoutPattern": "^myapp \\d+\\.\\d+\\.\\d+$", "cleanEnv": true }
  }
}
tsbundle && tssea build

The sea: true bundle mode needs tsbundle 3.4 or later. It rewrites native addon loads to smartsea's loader and writes <bundle>.smartsea.json. tssea refuses a bundle without that file (REFERENCES_MISSING) or one whose file describes other bytes (REFERENCES_STALE). <bundle>.smartsea.json is a build intermediate that holds absolute paths of the build host; keep it out of a package's files.

Configuration

"@git.zone/tssea" in .smartconfig.json. Unknown keys are refused (CONFIG_INVALID).

Key Type Required Meaning
nodeVersion string yes Exact Node.js release, x.y.z, 26 or later; this tssea must carry its pin (see Node.js releases)
bundle string yes The bundle: one ESM file from tsbundle (platform: "node", sea: true)
outDir string yes Directory of the executables and their notices
targets { target, name }[] yes Targets to build. name is the output file name, used exactly as given; a win32-x64 name must end in .exe
appName string no Names the runtime cache directory of the application. The default comes from package.json: @serve.zone/dcrouter becomes serve.zone-dcrouter
execArgv string[] no Node.js options for every start of the executable, e.g. --use-system-ca, --max-old-space-size=512. NODE_OPTIONS is ignored at run time
trees Record<string, { from, path?, targets? }> no Embedded asset trees (see below)
sidecars Record<string, { tree, path }> no Embedded executables, keyed by the name the application looks up
hostLibraries string[] no Sonames every Linux target host provides beyond glibc and Node.js's own libraries, e.g. ["libssl.so.3"]; naming one of those is CONFIG_INVALID (see Linux compatibility)
minSize, maxSize size, or { <target>: size } no Floor and budget, e.g. "110MiB" (SIZE_BELOW_FLOOR, SIZE_BUDGET_EXCEEDED)
buildTimeoutMs number no Milliseconds one run of the building Node.js may take: --build-sea for one target, or its version check. A run still going then is killed and the build fails with SEA_BUILD_FAILED, naming the command, the timeout and the end of its output. Default 600000 (10 minutes), at most 2147483647
smoke object no Run the executable once after building it (see below)
thirdPartyNotices { enabled?, supplements?, include? } no Notices; on by default (see below)

Targets

Target Node.js download Notes
linux-x64, linux-arm64 node-v<version>-linux-<arch>.tar.gz glibc ≥ 2.28, libstdc++ and libatomic on the host that runs it; Alpine/musl is not supported. Native addons are selected for glibc
darwin-x64, darwin-arm64 node-v<version>-darwin-<arch>.tar.gz Signed ad hoc by tssea
win32-x64 win-x64/node.exe Built and checked structurally, not smoke-tested on Windows

Trees and sidecars

"trees": {
  "webroot": { "from": "./dist_serve" },
  "bin": { "from": { "linux-x64": "./dist_rust/proxy_linux_amd64", "linux-arm64": "./dist_rust/proxy_linux_arm64" }, "path": "proxy", "targets": ["linux-x64", "linux-arm64"] }
},
"sidecars": { "proxy": { "tree": "bin", "path": "proxy" } }
  • from is a directory (every file below it, at its path below the directory) or a single file (at path, default its file name). Give one source for all targets, or one per target (an object keyed by target, covering every target the tree is embedded for).
  • targets limits a tree, and the sidecars in it, to some targets.
  • Files: symbolic links are refused (TREE_SYMLINK). A file's mode comes from its executable bit, and sidecars are always embedded executable. The names addons and addons-* are tssea's.
  • Sidecar names: a sidecar's key is the name the application asks for. smartrust looks embedded binaries up with getSidecarPath(binaryName), so name a smartrust sidecar after its binaryName.
  • Native sidecars: a sidecar that is an ELF, Mach-O or PE file must fit the target like an addon (SIDECAR_TARGET_MISMATCH); scripts are not checked. On Linux targets it also passes the Linux compatibility checks.
  • Notices: every sidecar ships its notices next to its binary, see Third-party notices.
  • At run time: the application reads a tree with smartsea: extractTree(name), readTreeFile(name, path) or getSidecarPath(name).

Native addons

The bundle's references name every addon load by a target-independent id (see smartsea's addon ids). For each target, tssea resolves each reference to one .node file:

  • node-gyp-build: the prebuild node-gyp-build itself would load on the target. tssea uses node-gyp-build 4.8.4's own parsers (prebuilds/<platform>-<arch>[+…], the fewest architectures first, file name tags) and evaluates them for the target: runtime node, the pinned release's ABI and libuv, armv8 on arm64, and glibc. build/Release and build/Debug are host builds and never used. PREBUILDS_ONLY and <NAME>_PREBUILD affect node-gyp-build at run time only.
  • bindings: the locations the bindings package tries, evaluated for the target. A file in a target-independent build directory (build/Release, …) must still fit the target's header check, and its ABI must match the pinned Node.js (an N-API addon, or one built with the same release).
  • file: the .node file in the owner package.
  • package: the specifier resolved from the owner package with Node.js's resolution, to a .node file. napi-rs per-platform packages, which need a different candidate per target, are not supported yet.

Every selected file's object header must fit the target: ELF e_machine and a Linux OS/ABI, Mach-O CPU type (including universal binaries), or PE machine. A reference without a file for a configured target fails the build with ADDON_PREBUILD_MISSING, ADDON_FILE_MISSING or ADDON_TARGET_MISMATCH, which name what was searched; nothing is skipped. For example, argon2 0.45.1 ships no darwin-x64 prebuild (0.41.1 does), so a darwin-x64 build of an argon2 0.45.1 application fails. Remove the target, or use a package version that ships a prebuild for it. Linux addons also pass the Linux compatibility checks.

Addons are embedded in trees that mirror node_modules/<package>/…. Two versions of one package go into separate trees. Mach-O addons and sidecars without a code signature are signed ad hoc for darwin targets, and the build result lists them in signedFiles.

Linux compatibility

The official Linux builds of Node.js run on glibc 2.28 and later, so a SEA does too, unless an embedded file needs more. Before the injection, every embedded addon and native sidecar of a Linux target, and every library its tree carries for it, must load wherever the target's pinned Node.js loads. tssea reads the files' dynamic linking data itself (program headers, so stripped files work too) and compares it with the pinned Node.js executable of the same target:

  • C library: a file that asks for another program loader than Node.js's (/lib64/ld-linux-x86-64.so.2, /lib/ld-linux-aarch64.so.1) or needs musl's libc is ELF_LIBC_UNSUPPORTED: a dynamically linked musl file does not load on a glibc host. Static files pass, musl ones included (for example Rust's x86_64-unknown-linux-musl, which links a static PIE).

  • Libraries: every DT_NEEDED soname must be one of:

    • a library Node.js itself needs (libstdc++.so.6, libgcc_s.so.1, libatomic.so.1, …);
    • a glibc library (libc, libm, libpthread, libdl, librt, libresolv, libutil, libanl, libBrokenLocale, the loader, and libmvec on x64);
    • a library the same tree carries, found through the file's DT_RUNPATH (or DT_RPATH) $ORIGIN entries, as the loader finds it once the tree is extracted. Carried libraries are checked the same way;
    • a soname listed in hostLibraries: a requirement on the target hosts. Each Linux target's build result lists the ones its files need (hostLibraries), and the build logs them.
      • Refused declarations: before any file is read, and before any target's output is written, a hostLibraries entry naming a glibc library of every architecture (libc.so.6, libm.so.6, libpthread.so.0, …) or a library a Linux target's Node.js needs itself (libstdc++.so.6, libgcc_s.so.1, libatomic.so.1, …) fails the build with CONFIG_INVALID, whether or not a file needs it. tssea accepts those libraries and checks their versions against the floor anyway, so the declaration would change nothing; the message says which rule applies. Remove the entry.
      • libmvec.so.1 is the one glibc library of a single architecture, and declaring it is allowed. On linux-arm64 (glibc 2.38 and later only) it is a host library: listed in that target's hostLibraries result, its versions not compared, and the declaration promises that the arm64 hosts run glibc 2.38 or later. On linux-x64 it is glibc: checked against the floor, not listed, and no warning.

    Anything else is ELF_LIBRARY_UNAVAILABLE, naming the soname, the file and the target.

  • Symbol versions: the floor is what the target's Node.js needs, read from its version requirements: in 26.11.1, GLIBC_2.28, GLIBCXX_3.4.21, CXXABI_1.3.11, GCC_4.2.0 and LIBATOMIC_1.0. In each of these namespaces, a file needing a higher version, or a non-numeric name Node.js does not need itself (such as GLIBC_PRIVATE or GLIBC_ABI_DT_RELR), is ELF_VERSION_FLOOR_EXCEEDED. Other namespaces, and versions needed from a hostLibraries library, are not compared.

Binaries linked dynamically on a current distribution fail the floor: anything linked against glibc 2.34 or later needs GLIBC_2.34, and so do the linux prebuilds of argon2 0.43.1, 0.44.0 and 0.45.1 (0.41.1's need GLIBC_2.25). Build them on an older system, link them statically, or use a version whose prebuilds stay within the floor.

An addon tree holds the selected .node files only. An addon that loads libraries from a sibling package, such as sharp 0.35's libvips-cpp.so from @img/sharp-libvips-linux-x64 through $ORIGIN/../../sharp-libvips-linux-x64/lib, therefore fails with ELF_LIBRARY_UNAVAILABLE: tssea does not embed such companion packages yet.

Only addons and sidecars are checked, plus the libraries they carry. Other tree files are data: a tree may carry executables for other machines, so tssea cannot know they run on the target.

Smoke check

"smoke": { "args": ["--version"], "expectExitCode": 0, "expectStdoutPattern": "^myapp ", "timeoutMs": 30000, "cleanEnv": true, "env": { "QEMU_LD_PREFIX": "/usr/aarch64-linux-gnu" }, "emulate": true }
  • Where it runs: the executable of the host's target runs once, without a shell, in the project directory. Other targets are skipped with a notice.
  • emulate: true: also runs the other Linux architecture (linux-arm64 on a linux-x64 host, and the reverse) through the kernel's binfmt_misc emulation (qemu-user-static).
    • The probe: before the first emulated run of a target, tssea writes a 132-byte static executable for it next to the executable (.tssea-exec-probe-<target>-<pid>: same mount, same noexec) and runs it once; it only exits with code 73. The probe is removed again and runs once per target and build. tssea never reads /proc/sys/fs/binfmt_misc, because a handler listed there may not run in the build's mount namespace, and a container may run foreign executables while it lists no handler.
    • Containers: a handler registered on the host with flag F (the interpreter is opened at registration) runs foreign executables in every container, without mounting anything into it. Without F, the interpreter path must exist inside the container.
    • Failure: when the kernel does not run the probe, the build fails with SMOKE_EMULATION_UNAVAILABLE, says what happened and what to change. It is never skipped silently.
      • The probe started but did not exit 73, e.g. probe <path> exited 127 (stderr "…"): no handler ran it (Node.js then runs the file with /bin/sh). Register a binfmt_misc handler with flag F on the host (qemu-user-static), or turn smoke.emulate off.
      • The probe could not start with EACCES or EPERM: the kernel refuses to execute files in that directory (a noexec mount or a security policy), and the error names it. Build with outDir on a filesystem that allows execution, or turn smoke.emulate off.
      • The probe could not start with ENOENT (probe <path> could not start: spawn <path> ENOENT), although tssea has just written it: a binfmt_misc handler took the file, but the handler's interpreter does not exist in the build's mount namespace. This is a handler registered without flag F, seen from a container. Register it with flag F on the host, or turn smoke.emulate off. Without any handler, the kernel reports ENOEXEC instead, and the probe exits 2, 126 or 127 through /bin/sh (the first case).
    • Libraries: qemu-user needs the target's libraries: on Debian and Ubuntu, install the cross packages and set QEMU_LD_PREFIX in env. A missing one fails the smoke run itself (SMOKE_FAILED).
  • Environment: cleanEnv: true runs with env only. Recommended in CI, because a failure quotes the end of stdout and stderr.
  • timeoutMs: default 30000, at most 2147483647.
  • Failure: any mismatch is SMOKE_FAILED. On POSIX the executable runs in its own process group, and whatever is left in that group is killed when the check ends.

Third-party notices

On by default. Every executable gets <name>.third-party-notices.txt and .json, covering:

  • The Node.js runtime: its LICENSE, and the pinned source tarball (node-v<version>.tar.gz with its SHA-256) as the corresponding source.
  • The bundle: its npm packages, from tsbundle's <bundle>.third-party-notices.json. tsbundle writes it for production bundles; without it, the build fails with NOTICES_BUNDLE_MISSING.
  • Embedded native addons: the owner package of every addon, plus every package its binding.gyp (with its relative includes) names through require('<name>') or require.resolve('<name>') in a <!(…) or <!@(…) command expansion, such as node-addon-api for its headers, applied recursively to those packages; package.json dependencies never count, and a name that does not resolve is NOTICES_PACKAGE_UNAVAILABLE. Their license and notice files are collected at every depth of each package (node_modules excluded, symbolic links to directories not followed), e.g. argon2's argon2/LICENSE for the reference C sources. A symbolic link to a license file is read, even when its target lies outside the package.
  • Sidecars: <binary>.third-party-notices.json next to each sidecar binary the tree selected for the target (the file from names, or the file below the from directory), in tsbundle's format. @git.zone/tsrust 4.6.0 and later writes it for the binaries it builds; a hand-made sidecar ships its own, { "generator": "<tool>", "packages": [] } for first-party code. Packages that are not npm packages are named by namespace, such as cargo:<crate>, native:<lib>, rust:std or runtime:musl. A missing file is NOTICES_SIDECAR_MISSING, an invalid one NOTICES_SIDECAR_INVALID. In a directory tree, the notices file is embedded with the binary.
  • include: further notices files in tsbundle's format, e.g. those of a web bundle embedded as a tree.

Packages that several sources list must agree (NOTICES_CONFLICT).

A package without license text needs a supplement (supplements, same format as tsbundle's), or the build fails. Turn notices off with "thirdPartyNotices": { "enabled": false }.

Node.js releases and the cache

  • Pins: tssea carries the SHA-256 of every download it uses for each supported Node.js release (ts/tssea.nodereleases.ts; 26.11.1 for now). Every download is compared with it (NODE_CHECKSUM_MISMATCH), and SHASUMS256.txt is never fetched at build time. A release this tssea does not carry fails with NODE_VERSION_UNPINNED: upgrade @git.zone/tssea.
  • Cache: downloads are cached as code assets in $TSSEA_CACHE_DIR, else $XDG_CACHE_HOME/tssea, else ~/.cache/tssea (macOS: ~/Library/Caches/tssea).
  • Integrity and concurrency: each entry records the SHA-256 of its files and is checked on every use. A damaged entry is fetched again. Concurrent first uses are safe without locks: each process stages its copy, and one rename publishes it.
  • The building Node.js: the build runs the pinned Node.js of the host, because the Node.js that builds a SEA must be the release it injects into. Code cache and snapshot are off, since they cannot cross platforms.

Reproducible builds

The same bundle, assets and Node.js release produce the same bytes. tssea passes the main by its file name relative to the build directory, because Node.js embeds the main's path, and it sorts every embedded list. tssea build --reproducible-check (API: reproducibleCheck: true) builds every target a second time in another directory and fails with NOT_REPRODUCIBLE unless both builds are byte-identical. Builds run one target after another; each --build-sea takes about 630 MB of memory.

CLI

tssea build [--target <target>]... [--no-smoke] [--reproducible-check] [--keep-work]
tssea fetch [--target <target>]...
tssea help | --help | -h
tssea --version | -v
  • build: builds every configured target, or only the --target ones.
    • It writes each executable through a work directory (<outDir>/.tssea-work-*, removed afterwards unless --keep-work). The executable is renamed into place only once all its checks passed.
    • The old executable and notices of a target are removed when its build starts.
  • fetch: downloads and verifies the Node.js executables of the targets and of the host into the cache, e.g. to warm a CI cache.
  • Exit codes:
    • 0: success.
    • 1: a build failure, printed as tssea: <message> [CODE].
    • 2: a usage error, followed by the help text.

API

import { TsSea } from '@git.zone/tssea';

const result = await new TsSea(process.cwd(), { reproducibleCheck: true }).build();
for (const target of result.targets) {
  console.log(target.target, target.executablePath, target.size, target.sha256, target.smoke);
}

ITsseaOptions: config (instead of .smartconfig.json), targets, smoke, reproducibleCheck, keepWork, cacheRoot, nodeDistributionUrl, log. TsSea.fetch() fills the cache. Every deliberate failure is a TsseaError with a stable code. FILE_UNREADABLE means a file exists but cannot be read (for example EACCES); missing files keep their specific codes, such as BUNDLE_MISSING. Layout errors keep their SmartSeaError (e.g. LAYOUT_INCOMPATIBLE when the bundle's smartsea major differs from tssea's).

Reading the configuration from other tools (@git.zone/tssea/config)

Tools that render files from a project's targets, such as installers, an npm wrapper or a release workflow, read the configuration through tssea itself, from the project's own install, so the tssea version that builds the project is the only interpreter of its configuration. The subpath loads node built-ins and @push.rocks/smartsea/layout only; tsbundle, smartarchive, smartrequest, smartconfig and node-gyp-build stay unloaded.

import { createRequire } from 'node:module';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';

// Resolve tssea from the project, not from the tool.
const entry = createRequire(path.join(projectDir, 'package.json')).resolve('@git.zone/tssea/config');
const tsseaConfig = await import(pathToFileURL(entry).href);

const { outDir, outputs } = tsseaConfig.resolveOutputs(smartconfigJson[tsseaConfig.tsseaConfigKey], projectDir);
for (const output of outputs) {
  console.log(output.target, output.platform, output.arch, path.join(outDir, output.name), output.noticeFiles);
}
export const tsseaConfigKey = '@git.zone/tssea';
export type TTsseaTarget = 'linux-x64' | 'linux-arm64' | 'darwin-x64' | 'darwin-arm64' | 'win32-x64';
export interface ITsseaOutput {
  target: TTsseaTarget;
  platform: 'linux' | 'darwin' | 'win32';
  arch: 'x64' | 'arm64';
  name: string;          // the executable file name in outDir, which is also the release asset name
  noticeFiles: string[]; // the notices file names next to it in outDir; [] when notices are off
}
export interface ITsseaOutputs { outDir: string; outputs: ITsseaOutput[] }
export function resolveOutputs(rawConfigArg: unknown, projectDirectoryArg: string): ITsseaOutputs;
export { TsseaError, type TTsseaErrorCode };
  • resolveOutputs runs the complete configuration validation of tssea build and returns what the build writes: outDir absolute and resolved against the project directory as the build resolves it, outputs in the order of targets. It is synchronous and reads nothing but the project's package.json, and only for the default appName. It checks no file the configuration names; a missing bundle or tree source fails at build time.
  • Errors: it throws a TsseaError (name "TsseaError", a string code) and never returns a partial result:
    • CONFIG_MISSING: rawConfigArg is undefined (the project has no "@git.zone/tssea" block).
    • CONFIG_INVALID: a bad value; the message starts with its key path, such as "@git.zone/tssea".targets[1].name.
    • NODE_VERSION_UNSUPPORTED: nodeVersion is not an exact release of Node.js 26 or later.
    • FILE_UNREADABLE: the default appName needs package.json, which exists but cannot be read or is not JSON. Without a package.json, a missing appName is CONFIG_INVALID.

Reading the cache from other tools (@git.zone/tssea/cache)

The layout of tssea's Node.js download cache is public and stable within a major version. @git.zone/tssea/cache exports it as pure code that imports nothing, and tssea writes and checks its entries with the same definitions, so disk-usage reports and cleanup tools classify the cache without a copy of its rules:

<cache>/node/v<version>/<target>/entry.json                 the entry record, written last
<cache>/node/v<version>/<target>/node | node.exe | LICENSE  the executable (and Node.js's LICENSE for tar.gz targets)
<cache>/node/v<version>/.tmp-<target>-<pid>-<random>/       staging of an entry in progress; its download is .download
<cache>/node/v<version>/.trash-<random>/                    a damaged entry being removed
export const nodeCacheDirName = 'node';
export function isVersionDirName(name: string): boolean; // v<major>.<minor>.<patch>
export const nodeEntryFileName = 'entry.json';
export const nodeEntryFormat = 'tssea-node-entry';
export const cacheStagingDirPrefix = '.tmp-';
export const cacheTrashDirPrefix = '.trash-';
export const staleScratchMs = 3_600_000; // 1 h
export function isNodeEntry(text: string): boolean;
export type TCacheEntryState = 'entry' | 'scratch' | 'stale-scratch';
export interface ICacheEntryFacts { name: string; mtimeMs: number; entryText?: string }
export interface ICacheEntryClassifyOptions { now: number }
export function classifyCacheEntry(facts: ICacheEntryFacts, options: ICacheEntryClassifyOptions): TCacheEntryState | undefined;
  • Finding the cache: by its structure. A nodeCacheDirName (node) directory whose children pass isVersionDirName holds the releases; pass every child directory of a version directory to classifyCacheEntry (files and symbolic links are never entries, and the classifier cannot tell them from directories). There is no marker file, and tools must not derive the location from the environment: it depends on the building process's TSSEA_CACHE_DIR and XDG_CACHE_HOME.
  • classifyCacheEntry classifies one directory of a node/v<version>/ level. entryText is the text of its entry.json (omitted when absent). A .tmp- or .trash- directory is scratch while now - mtimeMs < staleScratchMs and stale-scratch from then on, whatever it holds. Any other directory is an entry if its entry.json passes isNodeEntry, and undefined (not tssea's) otherwise. Files and symlinks are never entries.
  • mtimeMs is the newest lstat mtime of the directory itself and its direct children. Writing a file updates that file's mtime, not its directory's, and a stage's in-flight download (.download) and the files it unpacks are direct children. A download fails after 60 seconds without data, so a live stage's newest mtime is never more than about a minute old, and it never looks an hour idle; the directory's own mtime alone stays at its creation time for the whole download.
  • isNodeEntry identifies a record by its format. tssea itself also checks every file's size and SHA-256 against the record before it uses an entry.
  • Who deletes: tssea removes only what it is working on: its own stage once the entry is published or failed, and a damaged entry it renamed into the trash. It never removes stages or trash that other, killed, processes left behind; that is left to tools that classify the cache with this module, such as @git.zone/tsdisk.

Platform status

  • linux-x64 and linux-arm64: built and run in tssea's own tests (arm64 under qemu-user emulation).
  • darwin-arm64 and darwin-x64: built and signed in the tests. The signature is verified structurally, page hash by page hash; runs on a Mac are part of the release gate.
  • win32-x64: built in the tests; the certificate table is removed and the PE checksum is valid. Not smoke-tested on Windows.

Migrating from tsdeno

tssea builds from a bundle instead of compiling TypeScript itself:

  1. Add a tsbundle bundle with "platform": "node" and "sea": true.
  2. Move compileTargets to targets:
tsdeno tssea
compileTargets[].target x86_64-unknown-linux-gnu targets[].target linux-x64
aarch64-unknown-linux-gnu linux-arm64
x86_64-apple-darwin darwin-x64
aarch64-apple-darwin darwin-arm64
x86_64-pc-windows-msvc win32-x64
compileTargets[].name targets[].name, the same file name. Deno appended .exe for Windows; tssea does not, so write it
compileTargets[].outDir outDir, one for all targets
compileTargets[].entryPoint bundle, the tsbundle output
denoVersion nodeVersion (exact)
v8Flags execArgv
minSize, maxSize, smoke same names; sizes may be per target. smoke.emulate/env are new
thirdPartyNotices.{enabled, supplements} same; now on by default
compileTargets[].thirdPartyNoticesInclude thirdPartyNotices.include
denoExecutable dropped: tssea downloads and verifies the pinned Node.js
dependencyAge dropped: nothing is resolved at build time
permissions, noCheck dropped: Node.js has no Deno permissions; type checking belongs to tsbuild
selfExtracting, keepStaging dropped: smartsea extracts trees lazily into its cache and prunes old generations
excludeUnusedNpm, include, pruneNatives, pruneExclude, pruneKeep dropped: the bundle holds only reachable code, and only the target's addon files are embedded. Extra files go into trees
DENO_TLS_CA_STORE=system "execArgv": ["--use-system-ca"]

Linux executables now need libatomic on the host that runs them.

Third-party code

The ad-hoc Mach-O signer is ported from Go's cmd/internal/codesign (BSD-3-Clause); see third-party-notices.md.

This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license.md file within this repository.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.

Company Information

Task Venture Capital GmbH Registered at District court Bremen HRB 35230 HB, Germany

For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
Builds Node.js single executable applications for every target platform from a tsbundle Node bundle, with verified official Node binaries, embedded assets and native addons.
Readme
680 KiB
Languages
TypeScript 99.9%
JavaScript 0.1%