@endevops/parser
v0.0.1
Published
XML parser in pure JavaScript with pluggable output builders and composable value parsers. Fork of @nodable/flexible-xml-parser, with static types across the source tree and a fully typed test suite.
Maintainers
Readme
@endevops/parser
A fork of @nodable/flexible-xml-parser, a high-performance XML parser in pure JavaScript with pluggable output builders, composable value parsers, and string, buffer, stream, and incremental feed input modes.
This is a fork
This project is not the original parser. It started as a copy of @nodable/flexible-xml-parser at commit f51ecad5 and is maintained separately by Endevops. The package name changed and the code is edited, so this repository is the place to file issues against the fork, not the upstream one.
Upstream released @nodable/flexible-xml-parser as the scoped successor to the unscoped fast-xml-parser, so the credit chain runs fast-xml-parser (Amit Gupta) to @nodable/flexible-xml-parser to this fork. The flexible-xml-parser name in this package name is inherited from upstream, not chosen here.
What this fork changes
The parsing behaviour is the same. The changes are in how the code is written, built, and called.
| Change | Why |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Every file and directory renamed to dash-case | The upstream names were PascalCase and SCREAMING_CASE in the same tree |
| Static types across all of src/ | Upstream shipped types only on the public entry points |
| Every entry point returns an Effect | Construction and parsing can fail, so they report failure on a typed channel instead of throwing |
| One error class per parse failure | A limit, a broken tag and a refused option are three different decisions, so each gets a class |
| Test suite and benchmark fully typed | The specs are now checked by the compiler, which surfaced real bugs |
| Built with Vite+ (vp pack, vp test, vp check) | Replaces the previous ad-hoc build setup |
| Latent bugs fixed in specs and entity handling | Found while typing, listed in the commit history |
| Path matching and name validation are workspace pkgs | The path-expression-matcher type augmentations are gone; see below |
Two known differences worth calling out: test/compact-builder-force.spec.ts is fork-local.
bench/parse.bench.ts is a Vitest benchmark comparing whole-shot parse() against chunked feed()/end(). Run it with vp run bench from the workspace root, or vp test bench packages/parser for this package alone. vp test skips it.
The path-expression-matcher augmentations are gone
The parser used to carry src/path-expression-matcher.d.ts, a module augmentation patching three things the published index.d.ts got wrong: the third data constructor argument it never declared, findMatch()'s non-nullable return, and the stale doc comments. It also exported a ConfigurableExpressionCtor alias, because the two-argument declaration made the parser's three-argument construction a TS2554 at every call site.
All four are now real. Expression and ExpressionSet — which ship in @endevops/common-xml — are generic over the expression payload, so ExpressionSet<TagExpressionConfig> carries the config type from construction through to findMatch().data with no cast and no augmentation, which is what let the file be deleted rather than trimmed.
The output builders are in the workspace too, as @endevops/builder. Upstream @nodable/base-output-builder and @nodable/compact-builder ship declarations that name the upstream path-expression-matcher from npm, so a pnpm overrides entry used to point that transitive dependency at the workspace package — one copy of the types rather than two structurally-identical-but-distinct ones, without which every builder factory failed to satisfy the parser's structural contract. Merging the builders into this workspace removed the need for that override, and with it the remaining src/nodable-builders.d.ts augmentations.
Installation
The package is not published to npm. Clone the repository and link it into a consuming project, or point a pnpm catalog: entry at the local path.
pnpm install
pnpm buildIts runtime dependencies are effect and the two workspace packages @endevops/common-xml and @endevops/builder.
Quick start
Every entry point returns an Effect, so these examples run one to get a value back. In real code
prefer Effect.runPromise or a runtime.
import { Effect } from 'effect';
import XMLParser from '@endevops/parser';
const parser = Effect.runSync(XMLParser.make());
Effect.runSync(parser.parse('<root><count>3</count><active>true</active></root>'));
// { root: { count: 3, active: true } }Attributes are skipped by default. Turn them on to see them:
const parser = Effect.runSync(XMLParser.make({ skip: { attributes: false } }));
Effect.runSync(parser.parse('<item id="1">hello</item>'));
// { item: { '@_id': 1, '#text': 'hello' } }A ParseError — a union of 33 classes, one per cause, each tagged with its ErrorCode value, and
each carrying the numbers and names a handler needs — is the only thing that can fail. Recover from a
code you can handle with Effect.catchTag; a code you do not handle re-fails unchanged, and the
handler is handed that class rather than a payload in a wrapper.
Input modes
const parser = Effect.runSync(XMLParser.make());
Effect.runSync(parser.parse('<root/>')); // string
Effect.runSync(parser.parse(Buffer.from('<root/>'))); // buffer
Effect.runSync(parser.parseBytesArr(new Uint8Array([...]))); // typed array
await Effect.runPromise(parser.parseStream(fs.createReadStream('big.xml'))); // Node.js readable
// Incremental feed — feed() yields the parser back, end() produces the result
const streamed = Effect.runSync(XMLParser.make());
Effect.runSync(streamed.feed('<root>'));
Effect.runSync(streamed.feed('<item>1</item>'));
Effect.runSync(streamed.feed('</root>'));
const result = Effect.runSync(streamed.end());
// { root: { item: 1 } }Options
Everything is optional. XMLParser.make resolves and validates them, so this object literal is its
argument:
XMLParser.make({
skip: {
// What to leave out of the output
attributes: true, // Skip all attributes
declaration: false, // Skip <?xml ...?>
pi: false, // Skip processing instructions
cdata: false, // Leave CDATA out of the output
comment: false, // Leave comments out of the output
nsPrefix: false, // Strip namespace prefixes
tags: [], // Tag paths to drop from the output
},
nameFor: {
// Property names for special nodes
text: '#text', // Mixed-content text property
cdata: '', // '' merges into text, '#cdata' gets its own key
comment: '', // '' omits, '#comment' captures
},
attributes: {
// Attribute representation
prefix: '@_',
suffix: '',
groupBy: '', // Group attributes under one key, '' keeps them inline
booleanType: 'allow', // 'allow' reads valueless attributes as true, 'ignore' drops them, 'throw' rejects
},
tags: {
unpaired: [], // Self-closing tags written without a slash
stopNodes: [], // Paths whose content is captured raw
},
limits: { maxNestedTags: null, maxAttributesPerTag: null },
doctypeOptions: { enabled: false, maxEntityCount: 100, maxEntitySize: 10000 },
strictReservedNames: false,
exitIf: null,
feedable: { maxBufferSize: 10 * 1024 * 1024, autoFlush: true, flushThreshold: 1024 },
autoClose: null, // null is strict, 'html' recovers and collects errors
// OutputBuilder is omitted, not set to null — omit it to get CompactBuilder
});Value parsers
Value parsing belongs to the output builder, so tag text and attribute values get independent chains.
import { Effect } from 'effect';
import { CompactBuilderFactory } from '@endevops/builder';
const builder = Effect.runSync(
CompactBuilderFactory.make({
tags: { valueParsers: ['entity', 'boolean', 'number'] },
attributes: { valueParsers: ['entity', 'number', 'boolean'] },
})
);
const parser = Effect.runSync(XMLParser.make({ OutputBuilder: builder }));Documentation
The docs are inherited from upstream. Their install and import snippets name this package; the option reference and the internals notes still describe upstream behaviour in upstream's terms, so check a snippet against 10 — TypeScript if it disagrees with your editor.
| File | Topic |
| -------------------------------------------------------------- | ------------------------------------------------ |
| docs/01-getting-started.md | Installation, first parse, common patterns |
| docs/02-options.md | Full options reference |
| docs/03-value-parsers.md | Value parser pipeline, built-ins, custom parsers |
| docs/04-stop-nodes.md | Stop nodes and skip tags |
| docs/05-output-builders.md | Built-in and custom output builders |
| docs/06-streaming.md | Stream, feed and end, memory behaviour |
| docs/07-auto-close.md | Lenient HTML parsing and error collection |
| docs/08-security.md | DoS limits and prototype pollution |
| docs/09-path-expressions.md | Path expression syntax |
| docs/10-typescript.md | TypeScript usage and type definitions |
| docs/16-encoding.md | Encoding detection and decoding |
Thanks
This parser exists because Amit Gupta wrote fast-xml-parser and then @nodable/flexible-xml-parser. The tag scanning, attribute handling, value coercion, stop nodes, streaming design, and the output builder split that makes this parser configurable are all his work. The MIT license he chose for both packages is what makes this fork possible.
Thanks also to everyone who has reported a bug, sent a pull request, or answered an issue on either repository. A fork only stays useful when the original keeps moving, and that is mostly thanks to the people who keep sending it fixes.
This fork exists because of that work, and the same MIT terms apply to it.
License
MIT, the same as upstream. See LICENSE for the full text. The copyright notices for Amit Gupta (2026, @nodable/flexible-xml-parser) and Amit Kumar Gupta (2017, fast-xml-parser) are retained there alongside the fork's own, as the MIT terms require.
