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

batis-xml

v0.1.1

Published

WebAssembly bindings for batis-xml (MyBatis/iBatis mapper XML parser)

Readme

batis-xml

WebAssembly bindings for batis-xml — a parser and dynamic-SQL flattener for MyBatis and iBatis mapper XML.

const batisXml = require("batis-xml");

const bytes = fs.readFileSync("OrderMapper.xml"); // Buffer, NOT a decoded string
const result = JSON.parse(batisXml.parse(bytes)); // schema v1, see schema.d.ts
const dialect = batisXml.detect(bytes); // "mybatis" | "ibatis" | "unknown" -- cheap pre-check

TypeScript consumers: schema.d.ts ships in this package (generated from schema/batis-xml.v1.json, drift-checked in CI) — import type { ParseResult } from "batis-xml/schema".

Node.js target only — no browser/bundler build yet. This package is built with wasm-pack --target nodejs (CommonJS, loads the .wasm via fs.readFileSync at require time). It will not work as-is in a browser or with a bundler expecting --target web/--target bundler output (fetch-based instantiation, ESM). That's a separate build target to add later, not a difference in the Rust source.

Four things that will bite you

(a) Feed raw bytes — never a host-pre-decoded string. Always pass the file's original Buffer/Uint8Array to parse/detect, not a string you already decoded (e.g. fs.readFileSync(path, "utf-8") then re-encoded). batis-xml detects the encoding itself: a BOM sniff first (UTF-16 LE/BE select directly; a UTF-8 BOM is stripped), then a UTF-8 attempt, then the XML declaration's own encoding= label (BOM/declared-label-driven, covering every WHATWG encoding via encoding_rs — Shift_JIS, GB18030, Big5, UTF-16, …), then an EUC-KR heuristic for declaration-less legacy files; anything else decodes lossily with a diagnostic. result.encoding reports which of these actually won (see (b)). Feeding it bytes that already went through a host UTF-8 decoder defeats all of that, since a genuinely non-UTF-8 file would already have been mangled (replacement characters) before batis-xml ever sees it. Read files as bytes and stay in bytes until you call in.

(b) Spans are byte offsets into the UTF-8 text batis-xml itself decoded — never JS string indices, and never the original file's raw bytes for anything but a UTF-8 source. Every ByteSpan { start, end } in the JSON indexes into the UTF-8 bytes of the decoded text (see ByteSpan's own doc in schema.d.ts), while a JS string is indexed by UTF-16 code units — these diverge the moment a multi-byte character appears before the offset you care about. Worse, for anything decoded from a non-UTF-8 encoding, the original file's raw bytes aren't even the same length as the UTF-8 re-encoding the spans are offsets into — do not slice the original Buffer/Uint8Array directly with these offsets. result.encoding (the WHATWG name TextDecoder accepts directly) is what makes this reproducible:

// bytes is the same Buffer/Uint8Array you fed to parse()
const decodedText = new TextDecoder(result.encoding).decode(bytes);
const utf8Bytes = new TextEncoder().encode(decodedText); // byte-identical to batis-xml's own internal String
const text = new TextDecoder("utf-8").decode(
  utf8Bytes.subarray(span.start, span.end)
);

If the input was plain UTF-8, bytes and utf8Bytes are already byte-identical (decoding then re-encoding UTF-8 is a no-op), so slicing bytes directly happens to work in that one case — but relying on that silently breaks the moment a file turns out to be Shift_JIS/EUC-KR/ UTF-16/etc., which is exactly the failure mode result.encoding exists to prevent. Always go through the TextDecoder/TextEncoder round trip above regardless of what encoding you expect.

(c) Build qualified names as ns.id, suffixed @databaseId when present. Statement.database_id is deliberately not folded into id — that's the consumer's call. If you build a "qualified name" key (e.g. namespace.statementId) and drop database_id, two dual-dialect variants of the same statement (databaseId="oracle" / "mysql") collide onto one key. Recommended recipe:

// Both mapper.namespace (Option<Spanned<String>> — a namespace-less iBatis
// sqlMap has none) and statement.id (Option<..>, absent + a
// MissingStatementId diagnostic when the tag is missing an id attribute)
// are nullable in the schema -- `statement.id` in particular is easy to
// miss, since template literals silently accept `undefined` (stringified
// as "undefined") instead of a type error, so `tsc --strict` alone won't
// catch skipping this guard.
if (statement.id == null) {
  // No stable identifier for this statement at all -- MissingStatementId
  // is already in result.diagnostics; skip it rather than fabricate a key.
} else {
  const namespace = mapper.namespace?.value ?? "";
  const qualifiedName = statement.database_id
    ? `${namespace}.${statement.id.value}@${statement.database_id.value}`
    : `${namespace}.${statement.id.value}`;
}

(d) <include> expands before <where>/<set>/<trim> in MyBatis/ iBatis — this crate doesn't expand it at all. IncludeRef gives you the raw refid plus a best-effort IncludeTarget; substituting the referenced <sql> fragment's text in is on you.

The marker's textual form is a stable v1 contract: it renders in the flattened SQL as the literal, fixed-prefix comment token /* batis:include(<raw>) */, where <raw> is IncludeRef.raw verbatim (any literal */ inside it is replaced with *_/ so it can't terminate the comment early) — the same whether the target is Local, Qualified (rendered with its original dot, e.g. otherNs.frag), or Dynamic (the unresolved ${...} text rendered as-is). Since the prefix is fixed, sqlText.indexOf("/* batis:include(") (or a global regex) finds every token directly; match each one to its Statement.includes/ SqlFragment.includes entry by reconstructing the exact token string from that entry's raw field:

for (const inc of statement.includes) {
  const token = `/* batis:include(${inc.value.raw.replaceAll("*/", "*_/")}) */`;
  sqlText = sqlText.replace(token, resolveFragmentText(inc.value)); // your own lookup
}

If the fragment you're substituting is itself multi-variant (sql.variants on the fragment's own flattened output has more than one entry), there's no single deterministic substitution — pick the fragment's variant whose conditions match the same parameter state as the enclosing statement's variant you're substituting into, not variants[0] arbitrarily.

If you splice fragment text in after flattening (rather than before, like the real engines do), redo the wrapper's own cleanup against that substituted text: re-apply the leading-AND/OR strip or trailing-comma strip when the include token was first/last inside the wrapper, and treat a wrapper whose only content is an include token as conditional (the fragment might expand to nothing). result.diagnostics carries include_at_wrapper_boundary for every spot this applies to — each diagnostic's span is the same original-XML span as the matching includes[] entry's span (not a position in the flattened text) — see the core crate's README ("Include expansion order") for the full contract. As with any diagnostic, match on code ("include_at_wrapper_boundary"), never on message text — messages may be reworded between versions without that being a breaking change.

One more ordering guarantee worth relying on: result.mapper.statements (and fragments/result_maps) preserve the source document's order.

One more thing

IncludeRef.raw (the raw, unparsed refid text) is kept even when IncludeTarget is Dynamic (a ${}-driven refid batis-xml can't resolve statically) — inspect it for a static prefix or pattern to attempt your own best-effort match, rather than treating every dynamic include as a dead end.