---
title: npm Dependencies
url: "https://scriptc.dev/docs/dependencies"
docs_index: /llms.txt
lastUpdated: 2026-11-12
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

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`](/docs/coverage) before relying on them.

## Use a package

Create `tool.ts`:

```ts title="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:

```console
$ 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.

<span id="package-resolution-and-embedding" />

<span id="runtime-behavior" />

<span id="engine-and-builtin-modules" />

## 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.

When scriptc compiles a package, it selects the branch that package publishes under the `scriptc` export condition before the `node`, `import`, `require` and `default` conditions, whatever order the export map lists them in. A `types` branch still leads, so the package's declarations type the code the selected branch supplies. The same rule applies to a TypeScript workspace package, to an import of your project's own package name, and to a `#` import in your project's `imports` map, and the type checker reads the file scriptc compiles. Node.js doesn't match the `scriptc` condition, so `node` runs the branch its own conditions select; a package can use the condition to publish an adapter that only scriptc compiles.

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`. This holds wherever the declarations live: an installed package, a workspace package linked into `node_modules`, an `@types` package, an ambient `declare module`, or a declaration file of your own project. Their interfaces and type aliases type the records your program builds, and data cast to them is checked as for a type declared in a `.ts` file. A value still needs an implementation: a class to construct, an enum object, a constant or a function that only a declaration file declares is refused when you build, or, where Node.js can't find the module either, the program ends at start as it does under Node.js. 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.

A project that tsc compiles follows tsc instead. When the nearest `tsconfig.json` above a TypeScript file doesn't set `verbatimModuleSyntax`, scriptc drops the imports and exports that tsc drops for that file: an import whose bindings are never used as values, a clause of `type` specifiers only, and a re-export of a type. A module imported only in those ways isn't loaded, exactly as in the JavaScript tsc emits, so it doesn't run and doesn't take part in an import cycle. `isolatedModules` and `preserveConstEnums` decide whether a use of a const enum keeps its import, as they do for tsc. An export of a const enum stays a value export even where tsc's output has no value for the name, because a file that imports the export and uses a member as a value keeps that use; the importing file's build warns with `SC6008`. With `verbatimModuleSyntax: true`, or with no `tsconfig.json`, the type-stripping rule applies; a plain import of a type then fails when the program starts, as it does under Node.js, and the build warns about it with `SC6201`.

## 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](/docs/limitations#graceful-fs).

<span id="value-conversion" />

<span id="static-package-compilation-experimental" />

<span id="type-declarations-and-boundary-restrictions" />

## 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`:

```console
$ 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`. With 5.x, `ProxyAgent` is built in too: every request travels through an HTTP CONNECT tunnel that an `http:` or `https:` proxy opens, with the `uri`, `token`, `auth`, `headers`, `requestTls` and `proxyTls` options. 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`, and offers only `gzip, deflate` encodings. Without a dispatcher it is the built-in `fetch`. Its init can be undici's `RequestInit` or the global one, and it sends string and byte bodies (a string, an `ArrayBuffer` or one of its views); a stream, `Blob`, `FormData` or `URLSearchParams` body rejects with a `NotSupportedError`. The init members undici acts on and this implementation doesn't (`integrity`, `cache`, `mode`, `referrer`, `referrerPolicy`, `credentials`, `duplex`, `keepalive`, `priority`, `window`) are taken only with a value that asks for nothing, such as an empty `integrity` or `mode: "cors"`; any other value rejects with a `NotSupportedError` that names the member, so a digest is never accepted and then not checked.
- The global `fetch` called with an init whose `dispatcher` is an object of this package (an `Agent`, a `ProxyAgent`, an instance of a class that extends `Dispatcher`) sends the request through that dispatcher, with the same limits as the package's own `fetch`. An object that implements `dispatch()` itself is refused: when the program is built if it is written in the init, otherwise where the call runs, with a `NotSupportedError` that says so.
- `ProxyAgent` dials the proxy once for each tunnel instead of through a pooled client, doesn't take `clientFactory`, and isn't declared for 7.x. Of `requestTls` it applies `servername`, `rejectUnauthorized` and `ca`, of `proxyTls` those and `timeout`, `maxCachedSessions` and `lookup`; another member of either, and a `host`, `content-length` or `connection` header for the tunnel, throws a `NotSupportedError` when the agent is made. It doesn't read `HTTP_PROXY` or its relatives, as in undici: the program passes the proxy.
- 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.

<table>
  <thead>
    <tr>
      <th>Package</th>

      <th>Versions</th>

      <th>Compiles as</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><code>isarray</code></td>

      <td>0.x, 1.x, 2.x</td>

      <td><code>Array.isArray</code></td>
    </tr>

    <tr>
      <td><code>safe-buffer</code></td>

      <td>5.x</td>

      <td><code>require("buffer")</code></td>
    </tr>

    <tr>
      <td><code>process-nextick-args</code></td>

      <td>2.x</td>

      <td><code>process</code></td>
    </tr>

    <tr>
      <td><code>inherits</code></td>

      <td>2.x</td>

      <td><code>require("util").inherits</code></td>
    </tr>

    <tr>
      <td><code>util-deprecate</code></td>

      <td>1.x</td>

      <td><code>require("util").deprecate</code></td>
    </tr>

    <tr>
      <td><code>readable-stream</code></td>

      <td>2.x, 3.x, 4.x</td>

      <td><code>require("stream")</code> and its classes</td>
    </tr>
  </tbody>
</table>

`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](/docs/limitations#readable-stream).

## 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`:

```json title="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](/docs/limitations) page lists the supported `require` forms.

Some package entries choose a module by an environment variable, as React's `index.js` requires its production or development build by `NODE_ENV`. Build such a program with [`--define`](/docs/cli), for example `--define='process.env.NODE_ENV="production"'`: the entry compiles only the module the value selects. Without the flag, that conditional `module.exports` assignment is unsupported.

## 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.

When a deferred operation runs, the error it throws can be caught by your code or by a framework's error handler. So that a caught error doesn't look like an application bug, the program writes to stderr the first time each such site runs, naming the operation, its diagnostic code, and its source position when the site has one. A program that calls `v5` from `uuid` 14.0.3 prints:

```text
scriptc: this program reached code that scriptc could not compile; each such operation throws where it runs and is reported here once ('scriptc coverage' lists them, SCRIPTC_DEFERRED_REPORT=0 silences these lines)
scriptc: not compiled: 'unescape' is part of the standard library types but has no scriptc lowering yet [SC2020]
```

Set `SCRIPTC_DEFERRED_REPORT=0` in the program's environment to silence these lines. A program with no deferred operations never prints them.

A deferred operation that sits in the `try` block of a `try`/`catch` in the same function is different: the program wrote a handler for exactly that statement, as in `try { optional() } catch {}`. The program doesn't print anything for such a site when it runs, so its stderr stays what it is under Node.js. The build names the site instead, with warning `SC6012` at the statement: the `catch` clause runs even where the statement would have completed under Node.js, so the program's output can differ from there on. Set `SCRIPTC_DEFERRED_REPORT=all` to have the program report these sites at run time too. A site in a function that the `try` block calls, in a `catch` or `finally` block, or in a `try` that has only a `finally`, is reported at run time like any other. A library built with `scriptc build --lib` doesn't print them either: a deferred operation in a library throws its error, and nothing is written to the host process's stderr.

## 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:

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

`scriptc coverage` lists the declarations your program trusts before it runs, each with the command that patches it; see [type claims](/docs/coverage#type-claims).

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`:

```console
$ 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:

```json title="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:

<dl>
  <dt>
    `"| undefined"`
  </dt>

  <dd>
    A type that starts with

    `|`

    widens the declared type, here to

    `number | undefined`

    .
  </dd>

  <dt>
    `"string | null"`
  </dt>

  <dd>
    Any other type replaces the declared type.
  </dd>
</dl>

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:

```json title="package.json"
{
  "scriptc": {
    "typePatches": {
      "some-client@^3.2.0": {
        "files": { "dist/types/options.d.ts": "./type-patches/options.d.ts" }
      },
      "untyped-helper@1.x": {
        "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](/docs/native-objects) into your own builds, use the runtime pack that matches the 0.3 compiler.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)