- 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.
@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:
- Downloads the official Node.js executable of every target and checks it against a SHA-256 that tssea pins for the release.
- Selects the target's prebuilt native addons and embeds them, together with your asset trees and sidecar executables, through the
@push.rocks/smartseaasset layout. - 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.
- Runs
node --build-seawith the pinned Node.js of the host. The build uses an ESM main,execArgvExtension: "none", and code cache and snapshot off on every target. - Signs macOS executables ad hoc and removes the stale Authenticode certificate table of Windows executables.
- 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" } }
fromis a directory (every file below it, at its path below the directory) or a single file (atpath, 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).targetslimits 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 namesaddonsandaddons-*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 itsbinaryName. - 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)orgetSidecarPath(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: runtimenode, the pinned release's ABI and libuv,armv8on arm64, and glibc.build/Releaseandbuild/Debugare host builds and never used.PREBUILDS_ONLYand<NAME>_PREBUILDaffect node-gyp-build at run time only.bindings: the locations thebindingspackage 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.nodefile in the owner package.package: the specifier resolved from the owner package with Node.js's resolution, to a.nodefile. 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'slibcisELF_LIBC_UNSUPPORTED: a dynamically linked musl file does not load on a glibc host. Static files pass, musl ones included (for example Rust'sx86_64-unknown-linux-musl, which links a static PIE). -
Libraries: every
DT_NEEDEDsoname 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, andlibmvecon x64); - a library the same tree carries, found through the file's
DT_RUNPATH(orDT_RPATH)$ORIGINentries, 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
hostLibrariesentry 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 withCONFIG_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.1is 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'shostLibrariesresult, 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.
- Refused declarations: before any file is read, and before any target's output is written, a
Anything else is
ELF_LIBRARY_UNAVAILABLE, naming the soname, the file and the target. - a library Node.js itself needs (
-
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.0andLIBATOMIC_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 asGLIBC_PRIVATEorGLIBC_ABI_DT_RELR), isELF_VERSION_FLOOR_EXCEEDED. Other namespaces, and versions needed from ahostLibrarieslibrary, 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'sbinfmt_miscemulation (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, samenoexec) 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. WithoutF, 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 flagFon the host (qemu-user-static), or turnsmoke.emulateoff. - The probe could not start with
EACCESorEPERM: the kernel refuses to execute files in that directory (anoexecmount or a security policy), and the error names it. Build withoutDiron a filesystem that allows execution, or turnsmoke.emulateoff. - 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 flagF, seen from a container. Register it with flagFon the host, or turnsmoke.emulateoff. Without any handler, the kernel reportsENOEXECinstead, and the probe exits 2, 126 or 127 through/bin/sh(the first case).
- The probe started but did not exit 73, e.g.
- Libraries: qemu-user needs the target's libraries: on Debian and Ubuntu, install the cross packages and set
QEMU_LD_PREFIXinenv. A missing one fails the smoke run itself (SMOKE_FAILED).
- The probe: before the first emulated run of a target, tssea writes a 132-byte static executable for it next to the executable (
- Environment:
cleanEnv: trueruns withenvonly. Recommended in CI, because a failure quotes the end of stdout and stderr. timeoutMs: default30000, at most2147483647.- 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.gzwith its SHA-256) as the corresponding source. - The bundle: its npm packages, from tsbundle's
<bundle>.third-party-notices.json. tsbundle writes it forproductionbundles; without it, the build fails withNOTICES_BUNDLE_MISSING. - Embedded native addons: the owner package of every addon, plus every package its
binding.gyp(with its relativeincludes) names throughrequire('<name>')orrequire.resolve('<name>')in a<!(…)or<!@(…)command expansion, such as node-addon-api for its headers, applied recursively to those packages;package.jsondependencies never count, and a name that does not resolve isNOTICES_PACKAGE_UNAVAILABLE. Their license and notice files are collected at every depth of each package (node_modulesexcluded, symbolic links to directories not followed), e.g. argon2'sargon2/LICENSEfor 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.jsonnext to each sidecar binary the tree selected for the target (the filefromnames, or the file below thefromdirectory), in tsbundle's format.@git.zone/tsrust4.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 ascargo:<crate>,native:<lib>,rust:stdorruntime:musl. A missing file isNOTICES_SIDECAR_MISSING, an invalid oneNOTICES_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 withNODE_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--targetones.- 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.
- It writes each executable through a work directory (
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 astssea: <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 };
resolveOutputsruns the complete configuration validation oftssea buildand returns what the build writes:outDirabsolute and resolved against the project directory as the build resolves it,outputsin the order oftargets. It is synchronous and reads nothing but the project'spackage.json, and only for the defaultappName. 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 stringcode) and never returns a partial result:CONFIG_MISSING:rawConfigArgisundefined(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:nodeVersionis not an exact release of Node.js 26 or later.FILE_UNREADABLE: the defaultappNameneedspackage.json, which exists but cannot be read or is not JSON. Without apackage.json, a missingappNameisCONFIG_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 passisVersionDirNameholds the releases; pass every child directory of a version directory toclassifyCacheEntry(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'sTSSEA_CACHE_DIRandXDG_CACHE_HOME. classifyCacheEntryclassifies one directory of anode/v<version>/level.entryTextis the text of itsentry.json(omitted when absent). A.tmp-or.trash-directory isscratchwhilenow - mtimeMs < staleScratchMsandstale-scratchfrom then on, whatever it holds. Any other directory is anentryif itsentry.jsonpassesisNodeEntry, and undefined (not tssea's) otherwise. Files and symlinks are never entries.mtimeMsis the newestlstatmtime 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.isNodeEntryidentifies a record by itsformat. 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:
- Add a tsbundle bundle with
"platform": "node"and"sea": true. - Move
compileTargetstotargets:
| 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.
License and Legal Information
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.