npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

workerd-oxc

v0.6.0

Published

Oxc parser and transform adapter for Cloudflare workerd using direct WebAssembly modules.

Readme

workerd-oxc

Run the Oxc parser and transformer inside Cloudflare Workers. An experimental per-file semantic analyzer is also included.

import { transform } from "workerd-oxc";

const result = await transform({
  filename: "component.tsx",
  source: "export const view = <main>Hello</main>;",
  jsx: { runtime: "automatic", importSource: "react" },
});

if (result.ok) {
  result.value.code; // '...jsx("main", { children: "Hello" })...'
}

Install

npm install workerd-oxc

The npm package ships prebuilt .wasm artifacts, so consumers do not need a Rust toolchain. Building from a source checkout does; see Building from source.

Getting started

There are two ways in; both do the same work.

Call the one-shot functions when you parse or transform occasionally. They lazily initialize a shared instance on first call and return a promise:

import { parse, transform } from "workerd-oxc";

const parsed = await parse({ filename: "app.tsx", source });
const transformed = await transform({ filename: "app.tsx", source });

Create an explicit instance when you work in a hot path. Operation runtimes are initialized lazily, so using only parse does not instantiate transform or analyze Wasm:

import { createOxc } from "workerd-oxc";

const oxc = await createOxc();

await oxc.parse({ filename: "app.tsx", source });
await oxc.transform({ filename: "app.tsx", source });

Every call returns a result object rather than throwing on expected failures. Gate on result.ok:

const result = await transform({ filename: "app.tsx", source });

if (result.ok) {
  deploy(result.value.code);
} else {
  report(result.diagnostics);
}

API

createOxc(): Promise<Oxc>

Returns an instance with async parse, transform, and experimentalAnalyze methods. Takes no arguments.

The instance lazily initializes one Wasm runtime per operation. A parser-only caller does not pay to instantiate transform or analyzer Wasm.

parse(input): Promise<OxcResult<ParseOutput>> / oxc.parse(input)

Parses a source file into a full Oxc ESTree-shaped AST.

interface ParseInput {
  filename: string;
  source: string;
  lang?: "js" | "jsx" | "ts" | "tsx"; // default: inferred from filename
  sourceType?: "module" | "script"; // default: "module"
  astType?: "js" | "ts"; // default: "ts" for .ts/.tsx/.mts/.cts
  range?: boolean; // include byte ranges (default: false)
  preserveParens?: boolean; // default: false
}

interface ParseOutput {
  ast: OxcProgramAst; // { type: "Program", body: [...], ... }
  rawProgramLength: number;
}

BigInt and RegExp literals are materialized to real JS values, matching Oxc's own JS wrapper.

transform(input): Promise<OxcResult<TransformOutput>> / oxc.transform(input)

Strips TypeScript types and lowers JSX for a single file. Import specifiers are left untouched.

interface TransformInput {
  filename: string;
  source: string;
  lang?: "js" | "jsx" | "ts" | "tsx"; // default: inferred from filename
  sourceType?: "module" | "script"; // default: "module"
  target?: string; // e.g. "es2022" (default: "es2022")
  sourcemap?: boolean; // default: false
  jsx?:
    | "preserve"
    | {
        runtime?: "automatic" | "classic"; // default: "automatic"
        importSource?: string; // default: "react"
        development?: boolean; // default: false
      };
}

interface TransformOutput {
  code: string;
  map?: SourceMapV3; // present only when sourcemap: true
}

experimentalAnalyze(input): Promise<OxcResult<AnalyzeOutput>> / oxc.experimentalAnalyze(input)

Returns semantic facts for a single source file: scopes, bindings, references, unresolved references, imports, exports, and constrained JSX facts.

interface AnalyzeInput {
  filename: string;
  source: string;
  lang?: "js" | "jsx" | "ts" | "tsx"; // default: inferred from filename
  sourceType?: "module" | "script"; // default: "module"
}

interface AnalyzeOutput {
  scopes: ScopeFact[];
  bindings: BindingFact[];
  references: ReferenceFact[];
  unresolved: ReferenceFact[];
  imports: ImportFact[];
  exports: ExportFact[];
  jsxTags: JsxTagFact[];
}

See src/types.ts for each fact's fields.

  • Facts describe one file. imports and exports are recorded as written; specifiers are not resolved.
  • Spans are JavaScript UTF-16 string offsets into source.
  • id, scopeId, and bindingId are stable only within a single result.
  • BindingFact.kind reports the declaration category when Oxc exposes one, including "param", "type", "interface", "enum", and "enum-member".
  • ImportFact.bindingId is the semantic binding created by the import specifier. ImportFact.specifierKind reports the import form ("named", "default", or "namespace"). Only named imports include imported; default and namespace imports do not use sentinel strings.
  • ExportFact.kind is a discriminant for the export form ("named", "default", or "all"). Named exports include local and exported; all-exports include source and include exported only for namespace re-exports such as export * as ns from "./mod"; default exports use exported: "default". ExportFact.exportKind reports "value" or "type"; for export specifiers this is the syntactic type marker as written, because specifiers are not resolved. ExportFact.declarationKind reports the declaration category for direct declaration exports.
  • JsxTagFact.span is the opening tag span. nameSpan is the exact tag-name span. elementSpan covers the whole JSX element. Non-self-closing elements also include closingSpan and closingNameSpan.
  • JSX tag facts include source-order attributes and children. parentId follows JSX child hierarchy, not broad lexical containment; JSX elements in attribute expressions are emitted as separate root tags. JSX text facts expose syntax text, not React-rendered whitespace semantics.
  • Expression attribute values and expression children expose their span and, when the expression is already a static literal, a literal: LiteralValueFact. A LiteralValueFact is a JSON-shaped, tagged value: { type: "string" | "number" | "boolean" | "null" }, { type: "array"; elements }, or { type: "object"; properties: { key, value }[] } (properties keep source order, including duplicates). This is a purely structural read of already-literal syntax, not a constant evaluator: it materializes string, finite number, boolean, null, no-substitution template strings, unary +/- on a numeric literal, and arrays/objects composed of those. Anything else -- identifiers, calls, member access, bigint, regexp, non-finite numbers, negative zero, spreads, array holes, computed keys, getters/setters/methods, or templates with substitutions -- leaves literal absent, and consumers can fall back to the expressionSpan. Numeric object keys materialize only for finite safe integers; other numeric keys make the object opaque. Note the two literal sources: JSX string-attribute syntax (prop="x") is { kind: "string", value }, while an expression container (prop={"x"}, prop={5}) is { kind: "expression", literal }.
  • Intrinsic (lowercase) JSX tags are not bound to lexical variables. Component tags carry a bindingId only when Oxc semantic resolution resolves the tag; unresolved or type-only JSX names omit it.
  • Absent optional fields are omitted, not set to null.
  • This API is experimental; the fact shape may change in a minor version before it stabilizes. For why the analyzer stops at per-file facts, see Scope and non-goals.

OxcResult<T>

Every call returns a discriminated result. Expected failures — syntax errors, invalid transforms — are diagnostics, not thrown exceptions.

type OxcResult<T> =
  | { ok: true; value: T; diagnostics: OxcDiagnostic[] }
  | { ok: false; diagnostics: OxcDiagnostic[] };

A successful result can still carry warnings, so gate on ok, not on diagnostics.length.

OxcDiagnostic

interface OxcDiagnostic {
  phase: "parse" | "transform" | "analyze" | "runtime";
  severity: "error" | "warning";
  message: string;
  filename?: string;
  location?: { line: number; column: number }; // both 1-based
  span?: { start: number; end: number };
  cause?: string;
}

span offsets are JavaScript string offsets (UTF-16 code units), converted from Oxc's native UTF-8 byte offsets. They index into the source string you passed in.

How it works

your Worker
  └─ workerd-oxc
       ├─ dist/wasm/parser.wasm      (wasm32-unknown-unknown, 0 imports)
       ├─ dist/wasm/transform.wasm   (wasm32-unknown-unknown, 0 imports)
       ├─ dist/wasm/analyze.wasm     (wasm32-unknown-unknown, 0 imports)
       └─ a shared pointer/length/result ABI in JavaScript and Rust

Workers can't compile WebAssembly at runtime — no WebAssembly.compile, no fetching .wasm over the network, no threads. That rules out the stock Oxc WASI and browser builds, which expect one or more of those. workerd-oxc sidesteps the problem by shipping Oxc as static, zero-import WebAssembly.Module artifacts.

The .wasm files are Rust crates (native/) that wrap the Oxc parser, transformer, and semantic analyzer and expose a handful of C-ABI functions (alloc, parse/transform/analyze, result_ptr, free_result, …). The TypeScript host writes UTF-8 into Wasm memory, calls in, and reads a JSON result back out.

There is no N-API, no emnapi, no WASI, no runtime WebAssembly.compile, no browser Worker, and no shared memory. The modules import nothing — WebAssembly.Module.imports(module) is [] — which is what makes them loadable in workerd.

Scope and non-goals

workerd-oxc works on one file at a time. It parses a file, transforms a file, and reports semantic facts about a file. That single-file boundary is deliberate.

It is not a bundler, package manager, npm resolver, framework analyzer, or a drop-in for Vite, esbuild, or Rolldown, and it does not:

  • resolve modules or bundle
  • resolve npm / package.json exports
  • shim CJS/ESM interop
  • handle CSS, assets, or import.meta.url
  • rewrite dynamic import() / require()
  • check types or reason across files
  • evaluate or fold JSX expressions, or validate prop schemas (it materializes already-literal values structurally, but does not compute 1 + 2, resolve identifiers, or run calls)
  • decide application-specific component, route, deck, or document semantics

These are much larger problems, each with its own correctness burden. Folding them into a per-file call tends to make that call mean less, not more: callers can no longer tell whether a reported import was resolved, whether a type was checked, or whether a fact holds for the file or the whole project. Keeping the boundary sharp keeps the results trustworthy.

None of this is ruled out forever. Project-level or type-aware analysis could be worth adding later — but as a separate API with its own contract, not as hidden behaviour that experimentalAnalyze grows into. If you need resolution or bundling today, reach for a real bundler; if you need cross-file analysis today, this package is not it yet.

Cloudflare Worker Loader

Because transform emits plain module source, its output can be loaded as a Dynamic Worker. See examples/worker-loader for an end-to-end proof. Loader wiring is left to you — it is not part of this package's API.

Building from source

Requires Rust 1.95.0 with the wasm32-unknown-unknown target. The repo includes rust-toolchain.toml, so rustup installs the right toolchain and target automatically.

npm run build:wasm   # generates src/wasm/{parser,transform,analyze}.wasm
npm run build        # build:wasm, then copies artifacts into dist/wasm/

The artifacts are shipped in the npm package because Workers load them as static WebAssembly.Module imports; runtime consumers never build them. Inspect or verify them with:

npm run wasm:info    # size / hash / imports / exports per artifact
npm run wasm:check   # asserts zero imports and the expected ABI exports

Contributing

The canonical check runs everything CI does:

npm run check

That is Oxlint, Oxfmt, Rust formatting, Wasm artifact checks, package-shape checks, TypeScript, and the node and workerd test suites. Individual steps:

npm run lint          # Oxlint
npm run fmt           # Oxfmt + rustfmt (write)
npm run fmt:check     # Oxfmt + rustfmt (check)
npm test              # typecheck + node + workerd tests
npm run test:node
npm run test:workers

If you use just, a thin command facade wraps the common tasks:

just check
just fmt
just build-wasm

Worker tests run under @cloudflare/vitest-pool-workers.

License

MIT