Skip to content
oxc-projectPublic

About

Run TypeScript directly in Node.js, powered by Oxc.

Resources

Security policy

Stars

246 stars

Watchers

6 watching

Forks

Repository files navigation

oxc-node

Build Status npmjs.com

Run TypeScript directly in Node.js, powered by Oxc.

oxc-node adds module resolution and transformation hooks to Node. When Node loads a file, oxc-node resolves the import with oxc-resolver, transforms the source with Oxc, and returns JavaScript with an inline source map for Node to execute.

Node remains the runtime. oxc-node is not a JavaScript runtime, bundler, or type checker, and it does not write build output to disk.

What it supports

  • TypeScript and TSX without a separate build step
  • ESM and CommonJS, including .mts, .cts, .mjs, and .cjs
  • JSX and selected TypeScript compiler options
  • tsconfig.json path aliases and TypeScript-aware import resolution
  • Imports such as ./file.js resolving to ./file.ts or ./file.tsx
  • Source-mapped stack traces
  • Node features and flags such as watch mode, the test runner, and the inspector

Type annotations are removed at runtime, not checked. Run a type checker separately when you need type safety.

CLI

Install the CLI in a project:

npm install --save-dev @oxc-node/cli

Run a TypeScript entry point:

npx oxnode ./src/index.ts

oxnode starts Node with the @oxc-node/core hooks and source maps enabled. Arguments are passed through to Node, so normal Node workflows continue to work:

npx oxnode --watch ./src/server.ts
npx oxnode --test
npx oxnode --inspect ./src/index.ts

Running oxnode without arguments starts the Node REPL. Use oxnode --node-help to print Node's help; oxnode --help prints help for the wrapper.

Node hook

If you do not need the wrapper CLI, install the core package:

npm install --save-dev @oxc-node/core

Register it with any Node command:

node --import @oxc-node/core/register ./path/to/entry.ts
node --import @oxc-node/core/register --test

The register entry point installs both the ESM loader hooks and a CommonJS transform hook.

Configuration

By default, each file is governed by the nearest tsconfig.json in its own ancestor directories that claims it through files, include, exclude or a project reference — the same rule tsc follows. A file in a sub-project without its own tsconfig.json therefore inherits the workspace root one, and a file that no config claims is compiled with no options at all.

include only covers .js, .jsx, .mjs and .cjs files when allowJs is enabled, so by default no config claims them and they are compiled with no options. For module resolution they are matched as if they were TypeScript, which keeps path aliases working from JavaScript without allowJs.

A tsconfig.json that fails to parse stops the search: that file and every directory above it are skipped, and the file is compiled with no options. Comments and trailing commas are fine — only genuinely invalid JSON does this. Run with OXC_LOG=debug to see which config was picked for each file.

Set OXC_TSCONFIG_PATH to pin one config for every file instead. TS_NODE_PROJECT is also supported and takes precedence when both variables are set; an empty value counts as unset. Naming a file that does not exist disables tsconfig.json handling entirely rather than falling back to the search above.

A tool that wraps the loader can name the config through the API instead, so the choice does not leak into process.env and from there into the user's script and the processes it spawns:

import { setTsconfigPath } from "@oxc-node/core";

setTsconfigPath("./tsconfig.app.json");
await import("@oxc-node/core/register");

A path set this way takes precedence over both environment variables, and a relative path resolves against the working directory, just as theirs does. The config is read once per process, so call setTsconfigPath before the first transform or resolve; calling it later with a different path throws.

The supported tsconfig.json options are used for resolution and transformation. These include path aliases, module and JSX settings, legacy decorators and decorator metadata, class-field semantics, and relative import extension rewriting.

The loader does not transform files in node_modules by default. Set OXC_TRANSFORM_ALL=typescript to transform only the TypeScript dependencies — .ts, .mts, .cts and .tsx; JavaScript dependencies are still left to Node.js — or OXC_TRANSFORM_ALL=1 if every dependency needs transformation. Absent, empty, 0 and false all keep dependencies untouched.

A tool that wraps the loader can choose through the API instead, so the choice does not leak into process.env and from there into the user's script and the processes it spawns:

import { setTransformDeps } from "@oxc-node/core";

setTransformDeps("typescript");
await import("@oxc-node/core/register");

The setter accepts "typescript", "all" and "off" — the last keeps dependencies untouched even when the variable asks for more. A mode set this way takes precedence over OXC_TRANSFORM_ALL, and null clears the override. The mode is read once per process, so call setTransformDeps before the first transform or resolve; on the module.register() path the deadline is the register import itself, which fixes the mode for the hook thread. Calling it later with a different mode throws. On WASI the hook thread instantiates its own copy of the binding, so the mode in effect when @oxc-node/core/register is imported is the one the ESM hooks see.

OXC_TRANSFORM_ALL=typescript predates no older release: a version before this one reads any non-empty, non-0/false value as transform-all, so if an older copy is registered alongside (NODE_OPTIONS preloading a global install is the common case) it transforms JavaScript dependencies too. The setTransformDeps API is invisible to older copies and avoids that entirely.

Programmatic transformation

@oxc-node/core also exposes the native Oxc transformer:

import { OxcTransformer } from "@oxc-node/core";

const transformer = new OxcTransformer(process.cwd());
const output = await transformer.transformAsync("example.ts", "const answer: number = 42;");

console.log(output.source());
console.log(output.sourceMap());

The standalone transformer emits CommonJS. The registered loader preserves the module format selected for ESM files.

A .cts file is CommonJS, so it cannot use ES module syntax (import, export, or import.meta): oxc does not compile it to CommonJS the way tsc and tsx do, and the loader reports an error instead. Rename such a file to .mts, or write it with import x = require() and export =. Type-only imports and exports are fine.

Our sponsors

About

Run TypeScript directly in Node.js, powered by Oxc.

Resources

Security policy

Stars

246 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages