Documentation content

npm Dependencies

Programs can import npm packages the same way they do under Node.js. scriptc compiles each package your program uses to native code along with your own code. The executable doesn't read node_modules at runtime.

Package support is partial. Packages that scriptc can't compile are reported at build time with a diagnostic, so check new dependencies with scriptc coverage before relying on them.

Use a package

Create tool.ts:

tool.ts
import { Command } from "commander";

const program = new Command();

program
  .name("greet")
  .argument("<name>", "who to greet")
  .option("-u, --upper", "use uppercase")
  .action((name: string, opts: { upper?: boolean }) => {
    const text = `hello, ${name}`;
    console.log(opts.upper ? text.toUpperCase() : text);
  });

program.parse();

Install the dependency, then build and run the executable:

$ npm install commander
$ scriptc build tool.ts -o greet >/dev/null
$ ./greet ada --upper
HELLO, ADA

Your callback and the commander implementation both compile to native code. No extra flags are needed.

Package resolution

scriptc finds packages in node_modules with Node.js's resolution rules, including the export conditions in package.json, and compiles their dependencies too. TypeScript workspace packages compile as part of your program, including .js import specifiers that resolve to .ts sources.

Type-only imports follow Node.js's type stripping. import type and export type … from statements are erased, so a package or module named only by them supplies types and isn't compiled or loaded; it can even be a package with no JavaScript, such as type-fest. A clause whose specifiers are all marked type, such as import { type Options } from "./config.ts", still loads its module, as it does under Node.js.

Which packages compile

scriptc compiles a package automatically when it:

  • ships its own type declarations, or is typed by its DefinitelyTyped @types/ package, or is a dependency of another package that has no declarations at all, and
  • ships JavaScript that isn't a webpack bundle. Minified JavaScript compiles like unminified JavaScript.

The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an @types/ package describes another release, fails the build at that use.

A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself.

graceful-fs is the exception: it compiles as node:fs instead of from its JavaScript, with one documented difference.

Explicit package selection

Some packages your program imports directly ship no declarations, so their types can only be inferred from their JavaScript. To try compiling one of them, name it with --npm-static:

$ scriptc build app.ts --npm-static=three -o app >/dev/null

Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing ... to select a package together with every package it depends on, for example --npm-static=semver.... --npm-static=auto uses the default automatic selection. The option accepts a comma-separated list and can be repeated.

Built-in packages

scriptc implements a few packages itself and compiles that implementation in place of the package's published JavaScript. Which implementation compiles depends on the version your program has installed, so an import resolves the same way it does under Node.js: a package that isn't installed is still a missing package, and a version scriptc doesn't implement compiles from its own files as usual.

undici 5.x and 7.x are built in. The implementation covers request(), Dispatcher (including subclasses that override dispatch), Client, Pool with its stats, Agent with per-origin factory functions and connect options, buildConnector, the errors classes and their UND_ERR_* codes, setGlobalDispatcher and getGlobalDispatcher, fetch and Headers, and the request events undici publishes on node:diagnostics_channel. Requests use HTTP/1.1 with keep-alive connection reuse and undici's headers, body and idle timeouts, and each version keeps its own behavior where 5.x and 7.x differ, such as the error an aborted request rejects with. The package's other exports aren't declared, so using one is a type error at build time. Known differences:

  • A response body supports the readable-stream members undici's body mixin uses: on, once, pause, resume, destroy, text(), json(), arrayBuffer(), bytes(), blob() and dump(). It can't be piped or iterated with for await.
  • Diagnostics channel messages carry the request but not the socket or connector.
  • fetch with a dispatcher returns a response whose url is empty and whose redirected is false, offers only gzip, deflate encodings, and sends string and byte bodies. Without a dispatcher it is the built-in fetch.
  • Request bodies are strings and bytes; stream, iterable and form-data bodies, pipelining above 1, CONNECT requests and Unix socket paths aren't supported.

fs-extra 8.x through 11.x is built in, and its declarations replace @types/fs-extra. The default import, a namespace import and named imports all read the same exports. fs's members are re-exported, and fs-extra's wrapped methods follow its rule: with a callback as the last argument they call back, otherwise they return a promise. The extra methods cover copy, copySync, move, moveSync, remove, removeSync, emptyDir, ensureDir, mkdirs, mkdirp, ensureFile, pathExists, outputFile, readJson, readJsonSync, writeJson, writeJsonSync, outputJson and their aliases, with fs-extra's error messages. Known differences:

  • ensureLink, ensureSymlink, createLink, createSymlink, gracefulify, the promise forms of the descriptor-based methods, and the callback forms of stat, lstat, readdir, mkdtemp, realpath and readJson aren't declared, so using one is a type error at build time. Paths are strings, text is UTF-8, and write flags, string modes and copyFile mode flags aren't accepted.
  • graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first EMFILE error instead of retrying.
  • An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like readFile throws a SyntaxError there.

@hono/node-server 2.x is built in. serve, createAdaptorServer, getRequestListener and RequestError serve a fetch handler such as Hono's app.fetch over node:http with native Request and Response objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies stream to the handler as it reads them, and a body the handler leaves unread is drained after the response, up to 64 MiB or 500 ms, as the package does. Known differences:

  • Only HTTP/1.1 servers are declared: the createServer and serverOptions options for node:https and node:http2, the websocket option and upgradeWebSocket aren't declared, so using one is a type error at build time. Subpath modules such as @hono/node-server/serve-static compile from the package's own files.
  • The fetch handler must return a Response or a promise of one; the handler's second argument holds incoming and outgoing.
  • The global Request and Response stay the built-in classes, so overrideGlobalObjects has no effect, and a Response reads its default text content type as text/plain;charset=UTF-8 where the package's replacement reads text/plain; charset=UTF-8. Written responses carry the package's spelling.
  • A streamed response is written without waiting for drain, and a failed handler answers 504 only for an error whose name is TimeoutError.

Small polyfill packages whose module is exactly a Node.js built-in on Node.js 24 compile as that built-in, at the require() that loads them, from your program's CommonJS files, from compiled packages, and from esbuild bundles whose module paths name the package and its version (as pnpm installs do). The package's own files aren't compiled.

PackageVersionsCompiles as
isarray0.x, 1.x, 2.xArray.isArray
safe-buffer5.xrequire("buffer")
process-nextick-args2.xprocess
inherits2.xrequire("util").inherits
util-deprecate1.xrequire("util").deprecate
readable-stream2.x, 3.x, 4.xrequire("stream") and its classes

readable-stream compiles as Node.js's built-in streams, which is its own READABLE_STREAM=disable mode for 2.x and 4.x. That can differ from its default behavior under Node.js; see limitations.

Optional dependencies that never load

Some packages load an optional dependency inside try/catch and work without it, such as node-fetch with encoding. To keep such a dependency out of the native build even when it is installed, mark it unloaded in your entry's package.json:

package.json
{
  "scriptc": {
    "packages": {
      "encoding": "unloaded"
    }
  }
}

A require() of an unloaded package throws the same MODULE_NOT_FOUND error Node.js throws when the package isn't installed, and the package isn't compiled.

CommonJS packages

CommonJS packages compile with Node.js's module semantics. Each require() evaluates its module at the call, once, and later calls return the cached exports. A package can assign module.exports and run code before it requires its dependencies. If that code reads a const or let require binding before its require runs, the read throws the same ReferenceError as Node.js. A var binding that earlier code can reach is reported at build time. A require() of a JSON file, such as a package's own package.json, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes and esbuild's per-file output do, and uses the other modules only inside functions that its top-level code can't call. Bundles built by esbuild compile with their bundled CommonJS modules and lazily initialized ES modules, each evaluated once, at its first use, as under Node.js. Other require cycles are reported at build time. The limitations page lists the supported require forms.

Unsupported code inside a package

A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. Sites in a module's top-level code are listed under runs at startup: that code runs when the module loads, at startup for a module the program imports.

Fix a package's types

scriptc trusts declared types: a package's .d.ts files, its @types/ package, and the JSDoc types in its JavaScript. A value that contradicts its declared type throws a TypeError instead of flowing on, so a package whose declarations are wrong can stop a program that runs under Node.js. A common case is a member declared string or number that is sometimes undefined.

When the type comes from a package's declarations, the error names the declaration and the patch that would admit the value:

Uncaught TypeError: expected string at $, got undefined
    [email protected] declares Profile.email as string (index.d.ts:7)
    if that declaration is wrong, patch it: scriptc types patch profilekit "Profile.email" "| undefined"

Fix such a declaration in your project with a type patch. scriptc types patch shows what a declaration path matches in the installed package and adds the patch to your package.json:

$ scriptc types patch commander "HelpContext.helpWidth" "| undefined"
commander 15.0.0 (node_modules/commander)
  lib/command.js:2481  number  ->  number | undefined
wrote package.json scriptc.typePatches["commander@^15.0.0"].declarations["HelpContext.helpWidth"]

The patch it writes looks like this:

package.json
{
  "scriptc": {
    "typePatches": {
      "commander@^15.0.0": {
        "declarations": {
          "HelpContext.helpWidth": "| undefined"
        }
      }
    }
  }
}

Each key names a package and a semver range of the installed versions the patch describes. declarations maps a declaration to a type:

"| undefined"
A type that starts with | widens the declared type, here to number | undefined.
"string | null"
Any other type replaces the declared type.

A declaration is named by its path: Options.name for a member of a type, parse() for a function's return type, parse(text) for a parameter, list()[] for an array's elements, and Env[string] for an index signature. export=() names the return type of the function the module exports, default its default export, and a file prefix such as index.js:parse() names a declaration inside one of the package's files. One patch applies to the package's own declarations, its JSDoc, and its @types/ package. The patched type then holds everywhere, so your code has to handle the undefined that a widened declaration allows. An optional "note" string can link the upstream issue.

For larger fixes, replace a whole declaration file, or give a package without declarations its own:

package.json
{
  "scriptc": {
    "typePatches": {
      "some-client@^3.2.0": {
        "files": { "dist/types/options.d.ts": "./type-patches/options.d.ts" }
      },
      "[email protected]": {
        "types": "./type-patches/untyped-helper.d.ts"
      }
    }
  }
}

files maps a declaration file inside the package, or inside its @types/ package written as @types/<name>/<path>, to a local .d.ts file. types supplies the package's declaration entry. Paths of local files are relative to the package.json, and a replaced file can import the package's other declaration files with the same relative paths as the file it replaces.

Patches change only declarations, never the package's code, and values are still checked against the patched types. A build warns when a patch doesn't apply as written: when no range covers the installed version, when a declaration path matches nothing, when the declaration already has the type, or when a path is ambiguous. Report wrong declarations to the package too, so the patch can be removed later.

Provenance sources (experimental)

--provenance-sources compiles a package from the source at the commit its npm provenance attestation names, instead of from its published JavaScript. This lets packages written in TypeScript compile from their original TypeScript. Packages without a usable attestation compile from their installed files as usual.

Upgrading from scriptc 0.2

scriptc 0.2 had a --dynamic mode that embedded a JavaScript engine in the executable to run code that didn't compile, including packages that failed native compilation. scriptc 0.3 removed --dynamic and the embedded engine, so all application and package code now compiles to native code.

If you built with --dynamic, remove the flag and run scriptc coverage on your entry file. The report lists the operations and packages that need a change; rewrite those sites or replace the package. If you link native program objects into your own builds, use the runtime pack that matches the 0.3 compiler.