@epa-wg/trang-native

Copyright (c) 2026 Sasha Firsov https://github.com/sashafirsov

Native (GraalVM-compiled) Trang for RELAX NG / DTD / XSD conversion — no JRE required on consumer machines.

Wraps upstream relaxng/jing-trang and ships per-platform binaries built by GitHub Actions. Used by the cem_ml schema-emit verification fixtures (AC-S-2 RELAX NG compact round-trip) and available to any consumer that wants Trang without installing Java.

Purpose

Trang is the reference RELAX NG converter (RNG ↔ RNC ↔ XSD ↔ DTD), but it ships as a Java tool — invoking it normally requires a JDK or JRE on every developer machine and CI runner. For the cem_ml test suite, Trang is the parity converter: the schema emitter writes *.rng / *.rnc and a round-trip through Trang proves the two forms agree. Forcing every contributor (and every CI job) to install a JDK just to run that one check is heavyweight and fragile.

@epa-wg/trang-native solves that by shipping Trang as a self-contained, ahead-of-time-compiled (GraalVM native-image) executable. Consumers get a single binary that starts in milliseconds and has no Java runtime dependency.

Principles

  1. No JDK on the consumer side. The binary is fully self-contained. Building it from source needs GraalVM + Ant; using it does not.
  2. Only the host platform's binary is installed. The npm package itself contains no native code — at install time, postinstall detects process.platform × process.arch and downloads exactly one archive. No fat package, no multi-platform tarball.
  3. Per-platform binaries live in GitHub Releases, not in npm. Each trang-native-v<version> GitHub Release carries one archive per supported target plus a SHA256SUMS manifest. This keeps the npm tarball tiny and lets us re-cut binaries (e.g. for a new architecture) without republishing to npm.
  4. Integrity is verified. The postinstaller checks the downloaded archive's SHA-256 against the manifest before extracting.
  5. Opt-out is always available. TRANG_NATIVE_SKIP_DOWNLOAD=1, TRANG_NATIVE_BINARY=/abs/path, and pre-staged binaries (for air-gapped builds) all bypass the network step.
  6. Pinned upstream. upstream.json records the exact jing-trang commit each release was built from; rebuilds are reproducible.

Role in cem_ml

cem_ml's schema-emit verification fixtures (AC-S-2) use Trang to validate that the emitter's RELAX NG XML and compact outputs are semantically equivalent to each other under native RELAX NG tooling. The cem-ml test harness depends on @epa-wg/trang-native as a dev-dependency, so the moment yarn install finishes on a developer or CI machine, the platform-correct trang binary is already on disk and the parity tests run with no further setup. Only one binary — the host's — is fetched, so the install cost is a few MB rather than tens of MB.

Supported platforms

PlatformTarget tripleStatus
Linux x86_64linux-x86_64Tier A (CI server + dev)
Linux arm64linux-aarch64Tier A (Graviton / Ampere / RPi server)
Windows x86_64windows-x86_64Tier A (Windows dev workstations)
macOS arm64macos-aarch64Tier A (Apple Silicon dev workstations)

Other platforms (Windows arm64, macOS x86_64, FreeBSD, …) are not shipped today; consumers on unsupported platforms get a clear postinstall warning and can fall back to a system-installed trang.

Install

The postinstall hook reads process.platform + process.arch, downloads the matching archive from the trang-native-v<version> GitHub Release, verifies its SHA-256 checksum, and extracts the binary into node_modules/@epa-wg/trang-native/bin/native/. The package's bin entry points to a tiny Node shim (bin/trang.mjs) so consumers can call npx trang … or any package.json script.

Skipping the download

EnvironmentEffect
TRANG_NATIVE_SKIP_DOWNLOAD=1Postinstall is a no-op. Use when the binary is provided out-of-band.
TRANG_NATIVE_BINARY=/abs/pathShim invokes this path instead of bin/native/. Use for system Trang.
TRANG_NATIVE_REQUIRE_PREBUILT=1nx run @epa-wg/trang-native:build fails instead of compiling from source when no local/release binary is available. Use in CI.
Workspace dev installPostinstall auto-skips when the script is not under node_modules/.

Offline / air-gapped use

Pre-stage the matching archive at bin/native/<target>/trang(.exe) before npm install; the postinstall script's existence check finds it and skips the download.

Usage

CLI:

Programmatic (Node):

The shim simply execFiles the native binary with the arguments forwarded verbatim. Trang's own CLI documentation applies — see upstream docs.

How nx build resolves the binary

nx run @epa-wg/trang-native:build is the smart entry point. A GraalVM native-image compile takes minutes, so it is avoided whenever a prebuilt binary can be obtained. scripts/acquire-binary.mjs resolves a host-platform binary in this order:

  1. Local — if build/native/<triple>/ already holds a binary whose metadata.json records the current package.json version, it is reused as-is. Zero work.
  2. Release — otherwise the matching trang-native-v<version> GitHub Release archive is downloaded, SHA-256 verified against its sidecar, and extracted. This is the same artifact a published-npm consumer's postinstall pulls — "npm release" and "git release" are the one Release.
  3. From source — only if neither of the above yields a binary does it run the full fetch-source → build-jar → build-native chain, which requires GraalVM + Ant on the host. CI should set TRANG_NATIVE_REQUIRE_PREBUILT=1 so this path is never taken by ordinary validation jobs.

A native-image rebuild therefore happens only when:

Nx caching reinforces this: package.json is a build input, so a version bump invalidates the cache; an unchanged version replays the cached output without re-running the resolver at all.

To compile unconditionally regardless of any available binary, use the explicit build-from-source target (see below).

Building from source

You only need this section if you're cutting a new release, hacking on the native-image config, or running on a platform with no prebuilt binary.

Prerequisites:

To target a specific platform explicitly (still requires you to be on that platform's host):

The release workflow at .github/workflows/trang-native-release.yml runs a matrix across the four runs-on targets, uploads all archives

Reflection config

GraalVM's native-image requires reflection metadata for any Java classes loaded dynamically. Trang's reflection surface is small and captured in reflect-config/. To regenerate (run on a host with GraalVM + Ant):

The agent writes JSON config files into reflect-config/.agent-scratch/; review and merge into reflect-config/reflect-config.json before committing.

License

This package's wrappers are BSD-2-Clause (matches upstream Trang). See LICENSE and NOTICE.