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:
Install the dependency, then build and run the executable:
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:
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()anddump(). It can't be piped or iterated withfor await. - Diagnostics channel messages carry the request but not the socket or connector.
fetchwith a dispatcher returns a response whoseurlis empty and whoseredirectedisfalse, offers onlygzip, deflateencodings, and sends string and byte bodies. Without a dispatcher it is the built-infetch.- Request bodies are strings and bytes; stream, iterable and form-data bodies,
pipeliningabove 1,CONNECTrequests 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 ofstat,lstat,readdir,mkdtemp,realpathandreadJsonaren't declared, so using one is a type error at build time. Paths are strings, text is UTF-8, and write flags, string modes andcopyFilemode 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
EMFILEerror 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
readFilethrows aSyntaxErrorthere.
@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
createServerandserverOptionsoptions fornode:httpsandnode:http2, thewebsocketoption andupgradeWebSocketaren't declared, so using one is a type error at build time. Subpath modules such as@hono/node-server/serve-staticcompile from the package's own files. - The fetch handler must return a
Responseor a promise of one; the handler's second argument holdsincomingandoutgoing. - The global
RequestandResponsestay the built-in classes, sooverrideGlobalObjectshas no effect, and aResponsereads its default text content type astext/plain;charset=UTF-8where the package's replacement readstext/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 whosenameisTimeoutError.
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.
| Package | Versions | Compiles as |
|---|---|---|
isarray | 0.x, 1.x, 2.x | Array.isArray |
safe-buffer | 5.x | require("buffer") |
process-nextick-args | 2.x | process |
inherits | 2.x | require("util").inherits |
util-deprecate | 1.x | require("util").deprecate |
readable-stream | 2.x, 3.x, 4.x | require("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:
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:
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:
The patch it writes looks like this:
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 tonumber | 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:
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.