Deprecated. This package is no longer maintained.
Use the official Oxc Cloudflare Workers bindings instead:
@oxc-parser/binding-wasm32-wasip1@oxc-transform/binding-wasm32-wasip1- demo: Boshen/oxc-wasip1-workers
experimentalAnalyzehas no drop-in replacement. If you need semantic facts, build a thin project-owned analyzer over the official parser for the facts your product actually needs.
This package began as a way to run Oxc inside Cloudflare Workers before
upstream shipped WASIp1 /workerd loaders. That path is now official, and the
real consumer of this package migrated off it onto a domain-specific analyzer.
Keeping a generic adapter here no longer pays for itself.
The final published surface still wraps the official parser/transform bindings and retains the experimental custom analyzer for historical consumers, but new code should not depend on it.
Replace workerd-oxc with the official packages:
import parserWasm from "@oxc-parser/binding-wasm32-wasip1/wasm.wasm";
import { instantiate as instantiateParser } from "@oxc-parser/binding-wasm32-wasip1/workerd";
import transformWasm from "@oxc-transform/binding-wasm32-wasip1/wasm.wasm";
import { instantiate as instantiateTransform } from "@oxc-transform/binding-wasm32-wasip1/workerd";
const parser = await instantiateParser(parserWasm);
const transformer = await instantiateTransform(transformWasm);
const parsed = parser.parseSync("app.tsx", source);
const transformed = transformer.transformSync("app.tsx", source, {
lang: "tsx",
sourceType: "module",
target: "es2022",
jsx: { runtime: "automatic", importSource: "react" },
});Worker config needs Node.js compatibility and compiled Wasm module rules:
There is no upstream equivalent of this package's fact schema. Do not look for another generic package. Prefer:
- parse with
@oxc-parser/binding-wasm32-wasip1 - extract only the facts your product needs
- keep that analyzer next to the product that owns the source language
A real migration took that path successfully: lean bindings / imports / JSX
facts over the official parser, with no remaining workerd-oxc dependency.
The last supported API remains available on already-published versions for existing lockfiles, but it should not be adopted:
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;
}npm install workerd-oxcPrefer installing the official Oxc packages directly. This package is frozen and deprecated.
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);
}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.
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.
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
}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.
importsandexportsare recorded as written; specifiers are not resolved. - Spans are JavaScript UTF-16 string offsets into
source. id,scopeId, andbindingIdare stable only within a single result.BindingFact.kindreports the declaration category when Oxc exposes one, including"param","type","interface","enum", and"enum-member".ImportFact.bindingIdis the semantic binding created by the import specifier.ImportFact.specifierKindreports the import form ("named","default", or"namespace"). Only named imports includeimported; default and namespace imports do not use sentinel strings.ExportFact.kindis a discriminant for the export form ("named","default", or"all"). Named exports includelocalandexported; all-exports includesourceand includeexportedonly for namespace re-exports such asexport * as ns from "./mod"; default exports useexported: "default".ExportFact.exportKindreports"value"or"type"; for export specifiers this is the syntactictypemarker as written, because specifiers are not resolved.ExportFact.declarationKindreports the declaration category for direct declaration exports.JsxTagFact.spanis the opening tag span.nameSpanis the exact tag-name span.elementSpancovers the whole JSX element. Non-self-closing elements also includeclosingSpanandclosingNameSpan.- JSX tag facts include source-order
attributesandchildren.parentIdfollows 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. ALiteralValueFactis 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 -- leavesliteralabsent, and consumers can fall back to theexpressionSpan. 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
bindingIdonly 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.
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.
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.
your Worker
└─ workerd-oxc
├─ @oxc-parser/binding-wasm32-wasip1
├─ @oxc-transform/binding-wasm32-wasip1
└─ dist/wasm/analyze.wasm (custom, 0 imports)
Workers can't compile WebAssembly at runtime — no WebAssembly.compile, no
fetching .wasm over the network, no threads. The official Oxc packages expose
deferred /workerd loaders that accept statically imported, precompiled
WebAssembly.Module values. workerd-oxc maps those bindings onto its existing
result and diagnostic API.
The experimental analyzer remains a small custom Rust module
(native/analyze) only for historical consumers. New projects
should not depend on it. Prefer a project-owned fact extractor over the
official parser.
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.jsonexports - 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.
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 the custom analyzer 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/analyze.wasm
npm run build # builds the adapter and copies the analyzer into distThe analyzer is shipped in the npm package because Workers load it as a static
WebAssembly.Module import; runtime consumers never build it. Inspect or verify
it with:
npm run wasm:info # size / hash / imports / exports
npm run wasm:check # asserts zero imports and the expected ABI exportsThe canonical check runs everything CI does:
npm run checkThat 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:workersIf you use just, a thin command facade
wraps the common tasks:
just check
just fmt
just build-wasmWorker tests run under
@cloudflare/vitest-pool-workers.
{ "compatibility_flags": ["nodejs_compat"], "rules": [ { "type": "CompiledWasm", "globs": [ "**/parser.wasm32-wasip1.wasm", "**/transform.wasm32-wasip1.wasm", ], "fallthrough": true, }, ], }