ende compile

Flags

As with ende install, the runtime flags used to execute the script must be specified at compilation time. This includes permission flags.

ende compile --allow-read --allow-net jsr:@ende/http/file-server

Script arguments can be partially embedded.

ende compile --allow-read --allow-net jsr:@ende/http/file-server -p 8080

./file_server --help

Watch mode

Pass --watch to rebuild the executable whenever a file in the compile graph changes:

ende compile --watch main.ts

Use --watch-exclude to keep specific paths from triggering a rebuild, and --no-clear-screen to preserve the terminal output between rebuilds:

ende compile --watch --watch-exclude=./dist --no-clear-screen main.ts

Cross Compilation

You can cross-compile binaries for other platforms by using the --target flag.

# Cross compile for Apple Silicon
ende compile --target aarch64-apple-darwin main.ts

# Cross compile for Windows with an icon
ende compile --target x86_64-pc-windows-msvc --icon ./icon.ico main.ts

Supported Targets

Ende supports cross compiling to all targets regardless of the host platform.

OS Architecture Target
Windows x86_64 x86_64-pc-windows-msvc
Windows ARM64 aarch64-pc-windows-msvc
macOS x86_64 x86_64-apple-darwin
macOS ARM64 aarch64-apple-darwin
Linux x86_64 x86_64-unknown-linux-gnu
Linux ARM64 aarch64-unknown-linux-gnu

aarch64-pc-windows-msvc (Windows on ARM) is supported .

The endert binary

ende compile embeds your program into endert ("Ende runtime"): a stripped build of Ende that contains only what's needed to run a compiled program, with none of the tooling subcommands. Using endert as the base instead of the full ende binary is what keeps compiled executables smaller.

The first time you compile for a given Ende version and target, Ende downloads the matching endert-<target>.zip from dl.ende.sh and caches it in ENDE_DIR. This is also how cross-compilation works: compiling with --target fetches that platform's endert. Subsequent compiles reuse the cached binary and work offline.

To use a custom or locally built runtime as the base, set the ENDERT_BIN environment variable to its path. Ende also picks up a endert binary placed next to the ende executable.

Choosing a JavaScript engine

Caution

--engine quickjs is experimental and subject to change.

Compiled binaries run on V8 by default. The --engine flag lets you build against QuickJS instead, which produces a smaller executable:

# Default, same as omitting the flag
ende compile --engine v8 main.ts

# Build a binary that runs on QuickJS
ende compile --engine quickjs main.ts

The same flag is available on ende desktop, where it selects the engine embedded in the app's runtime library.

Trade-offs to weigh before shipping a QuickJS binary:

  • Security updates. The QuickJS backend does not receive the same security updates as V8. Don't use it for programs that run untrusted input.
  • Performance. QuickJS is an interpreter with no JIT, so compute-heavy code runs slower than on V8. Startup and memory use are lower.
  • Maturity. The backend is experimental; behavior differences from V8 are possible, so test your program with the engine you intend to ship.

Each engine has its own prebuilt runtime: with --engine quickjs, Ende downloads endert-quickjs-<target>.zip (or libendert-quickjs-<target>.zip for ende desktop) instead of the default endert-<target>.zip. Both variants are published for every supported target, so cross-compilation works the same way. If you override the base runtime with ENDERT_BIN (or a endert binary next to ende), that binary is used as-is: --engine is then ignored and Ende prints a warning.

Icons

It is possible to add an icon to the executable by using the --icon flag when targeting Windows. The icon must be in the .ico format.

ende compile --icon icon.ico main.ts

# Cross compilation with icon
ende compile --target x86_64-pc-windows-msvc --icon ./icon.ico main.ts

Dynamic Imports

By default, statically analyzable dynamic imports (imports that have the string literal within the import("...") call expression) will be included in the output.

// calculator.ts and its dependencies will be included in the binary
const calculator = await import("./calculator.ts");

But non-statically analyzable dynamic imports won't:

const specifier = condition ? "./calc.ts" : "./better_calc.ts";
const calculator = await import(specifier);

To include non-statically analyzable dynamic imports, specify an --include <path> flag.

ende compile --include calc.ts --include better_calc.ts main.ts

Including Data Files or Directories

You can include files or directories in the executable by specifying them via the --include <path> flag.

ende compile --include names.csv --include data main.ts

Then read the file relative to the directory path of the current module via import.meta.dirname:

// main.ts
const names = Ende.readTextFileSync(import.meta.dirname + "/names.csv");
const dataFiles = Ende.readDirSync(import.meta.dirname + "/data");

// use names and dataFiles here

Note this currently only works for files on the file system and not remote files.

--include treats embedded .js and .ts files as module-graph roots, so it resolves and transpiles them. To embed files exactly as they are, without any module resolution, use --include-as-is instead. This is the right choice for pre-built frontend bundles (for example Vite or webpack output) that are already processed and would fail to resolve as Ende modules:

ende compile --include-as-is ./dist main.ts

Configuring include / exclude in ende.json

The --include and --exclude paths can be set declaratively in ende.json so you don't have to repeat them on every ende compile invocation:

ende.json
{
  "compile": {
    "include": ["names.csv", "data", "worker.ts"],
    "exclude": ["data/secrets", "**/*.test.ts"]
  }
}

CLI flags are merged with the config: --include and --exclude add to the lists in ende.json rather than replacing them. See the Compile config section in the configuration guide for more details, including how to declare permissions on the same block.

Workers

Similarly to non-statically analyzable dynamic imports, code for workers is not included in the compiled executable by default. There are two ways to include workers:

  1. Use the --include <path> flag to include the worker code.
ende compile --include worker.ts main.ts
  1. Import worker module using a statically analyzable import.
// main.ts
import "./worker.ts";
ende compile main.ts

Self-Extracting Executables

By default, compiled executables serve embedded files from an in-memory virtual file system. The --self-extracting flag changes this behavior so that the binary extracts all embedded files to disk on first run and uses real file system operations at runtime.

ende compile --self-extracting main.ts

This is useful for scenarios where code needs real files on disk, such as native addons or native code that reads relative files.

The extraction directory is chosen in order of preference:

  1. <exe_dir>/.<exe_name>/<hash>/ (next to the compiled binary)
  2. Platform data directory fallback:
    • Linux: $XDG_DATA_HOME/<exe_name>/<hash> or ~/.local/share/<exe_name>/<hash>
    • macOS: ~/Library/Application Support/<exe_name>/<hash>
    • Windows: %LOCALAPPDATA%\<exe_name>\<hash>

Files are only extracted once — subsequent runs reuse the extracted directory if it already exists and the hash matches.

Trade-offs

Self-extracting mode enables broader compatibility, but comes with some trade-offs:

  • Initial startup cost: The first run takes longer due to file extraction.
  • Disk usage: Extracted files take up additional space on disk.
  • Memory usage: Higher memory usage since embedded content can no longer be referenced as static data.
  • Tamper risk: Users or other code can modify the extracted files on disk.

Code Signing

macOS

By default, on macOS, the compiled executable will be signed using an ad-hoc signature which is the equivalent of running codesign -s -:

ende compile -o main main.ts
codesign --verify -vv ./main

./main: valid on disk
./main: satisfies its Designated Requirement

You can specify a signing identity when code signing the executable just like you would do with any other macOS executable:

codesign -s "Developer ID Application: Your Name" ./main

Refer to the official documentation for more information on codesigning and notarization on macOS.

Windows

On Windows, the compiled executable can be signed using the SignTool.exe utility.

ende compile -o main.exe main.ts
signtool sign /fd SHA256 main.exe

Persistent storage in executables

A compiled binary is treated as a standalone application, so origin-bound storage persists across runs in the platform's application data directory (%LOCALAPPDATA% on Windows, ~/Library/Application Support on macOS, $XDG_DATA_HOME on Linux):

localStorage and the Web Cache API read and write to that directory.

Each compiled app gets its own location derived from its identity, so separate apps do not share storage. That identity comes from the --app-name flag, which is baked in at compile time and falls back to the output file name when omitted:

ende compile --app-name my-app main.ts

Because the name (not the module path) drives the directory, the store stays stable across runs even if you rename the binary, two binaries built with the same --app-name share a store, and differently named apps stay isolated. Recompiling with a different --app-name starts a fresh store.

Command line usage:

compile

Compile the script into a self contained executable

Options

--app-name

Stable identity for the compiled app. Determines where origin-bound storage such as localStorage and caches is persisted (under the platform's app data directory). Defaults to the output file name. Set this to keep storage stable across renames.

--cached-only

Require that remote dependencies are already cached.

--cert

Load certificate authority from PEM encoded file.

--check

Set type-checking behavior. This subcommand type-checks local modules by default, so passing --check is redundant; pass --check=all to also type-check remote modules. Alternatively, use the 'ende check' subcommand.

--config, -c

Configure different aspects of ende including TypeScript, linting, and code formatting. Typically the configuration file will be called ende.json or ende.jsonc and automatically detected; in that case this flag is not necessary.

--engine

JS engine the compiled binary runs on (quickjs is smaller and experimental, and does not receive the same security updates as v8).

--env-file

Load environment variables from local file Only the first environment variable with a given key is used. Existing process environment variables are not overwritten, so if variables with the same names already exist in the environment, their values will be preserved. Where multiple declarations for the same environment variable exist in your .env file, the first one encountered is applied. This is determined by the order of the files you pass as arguments.

--exclude

Excludes a file/directory in the compiled executable. Use this flag to exclude a specific file or directory within the included files. For example, to exclude a certain folder in an included directory.

--ext

Set content type of the supplied file.

--frozen-lockfile

Error out if lockfile is out of date.

--icon

Set the icon of the executable on Windows (.ico).

--import-map

Load import map file from local file or remote URL.

--include

Includes an additional module or file/directory in the compiled executable. Use this flag if a dynamically imported module or a web worker main module fails to load in the executable or to embed a file or directory in the executable. This flag can be passed multiple times, to include multiple additional modules.

--inspect

Activate inspector on host:port [default: 127.0.0.1:9229]. Host and port are optional. Using port 0 will assign a random free port.

--inspect-brk

Activate inspector on host:port, wait for debugger to connect and break at the start of user script.

--inspect-publish-uid

--inspect-wait

Activate inspector on host:port and wait for debugger to connect before running user code.

--location

Value of globalThis.location used by some web APIs.

--lock

Check the specified lock file. (If value is not provided, defaults to "./ende.lock").

--min-dep-age

(Unstable) The age in minutes, ISO-8601 duration or RFC3339 absolute timestamp (e.g. '120' for two hours, 'P2D' for two days, '2025-09-16' for cutoff date, '2025-09-16T12:00:00+00:00' for cutoff time, '0' to disable).

--no-check

Skip type-checking. If the value of "remote" is supplied, diagnostic errors from remote modules will be ignored.

--no-clear-screen

Do not clear terminal screen when under watch mode.

--no-code-cache

Disable V8 code cache feature.

--no-config

Disable automatic loading of the configuration file.

--no-lock

Disable auto discovery of the lock file.

--no-remote

Do not resolve remote modules.

--no-terminal

Hide terminal on Windows.

--output, -o

Output file (defaults to $PWD/).

--preload

A list of files that will be executed before the main module.

--reload, -r

Reload source code cache (recompile TypeScript). With no value, reloads everything. Pass a comma-separated list of specifiers to reload only those modules; jsr:@ende/http/file-server,jsr:@ende/assert/equals reloads specific modules.

--seed

Set the random number generator seed.

--self-extracting

Create a self-extracting binary that extracts the embedded file system to disk on first run and then runs from there.

--target

Target OS architecture.

--unsafely-ignore-certificate-errors

DANGER: Disables verification of TLS certificates.

--v8-flags

To see a list of all available flags use --v8-flags=--help Flags can also be set via the ENDE_V8_FLAGS environment variable. Any flags set with this flag are appended after the ENDE_V8_FLAGS environment variable.

--vendor

Toggles local vendor folder usage for remote modules.

--watch

Watch for file changes and restart process automatically. Only local files from entry point module graph are watched.

--watch-exclude

Exclude provided files/patterns from watch mode.