Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .claude/settings.json

This file was deleted.

65 changes: 65 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/bin/sh
# Blocks a commit unless the whole codebase, exactly as it will be committed, is formatted and
# passes lint.
#
# oxfmt is the formatter (.oxfmtrc.json); oxlint is the linter (.oxlintrc.json). Wired up by
# the "prepare" script in package.json (git config core.hooksPath .githooks) on npm install.
# Emergency bypass: git commit --no-verify
#
# This file is shared verbatim by jetstreamapp/sf-formula-parser, jetstreamapp/soql-parser-js,
# jetstreamapp/simple-excel and jetstreamapp/simple-xml - keep every copy in sync when changing it.

root=$(git rev-parse --show-toplevel) || exit 1
OXFMT="$root/node_modules/.bin/oxfmt"
OXLINT="$root/node_modules/.bin/oxlint"

for bin in "$OXFMT" "$OXLINT"; do
if [ ! -x "$bin" ]; then
echo "pre-commit: $bin not found - run 'npm install' first." >&2
exit 1
fi
done

# The checks run against a snapshot of the index rather than the working tree. The index is
# exactly what the commit will contain, so a file formatted on disk but never re-staged cannot
# slip through, and unstaged edits cannot block a clean commit. git points GIT_INDEX_FILE at the
# right index for `commit -a` and `commit <path>` too, and checkout-index honours it. Every
# tracked file is exported, so the whole codebase is checked, not only the files being changed.
snapshot=$(mktemp -d "${TMPDIR:-/tmp}/pre-commit.XXXXXX") || exit 1
trap 'rm -rf "$snapshot"' EXIT
trap 'exit 1' HUP INT TERM

git -C "$root" checkout-index --all --prefix="$snapshot/" || exit 1

# Some lint rules resolve imports, so the snapshot shares the installed dependencies.
ln -s "$root/node_modules" "$snapshot/node_modules" || exit 1

cd "$snapshot" || exit 1

# Both run even when the first fails, so one commit attempt reports everything to fix.
status=0

if ! "$OXFMT" --check; then
cat >&2 <<'MSG'

Commit blocked: the files listed above are not formatted.

npm run format # format the repo
git add -u # stage the result
MSG
status=1
fi

if ! "$OXLINT"; then
cat >&2 <<'MSG'

Commit blocked: the files listed above have lint errors.

npm run lint # review the full report
npm run lint:fix # apply the fixes oxlint can make automatically
git add -u # stage the result
MSG
status=1
fi

exit $status
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
cache: npm

- run: npm ci
# The pre-commit hook runs these too, but it can be bypassed with --no-verify
- run: npm run format:check
- run: npm run lint
- run: npm run typecheck
# build before test, matching sf-formula-parser and soql-parser-js: tests run against a
# build that is known to have succeeded.
Expand Down
4 changes: 3 additions & 1 deletion .prettierrc → .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 140,
"semi": true,
"tabWidth": 2,
Expand All @@ -7,5 +8,6 @@
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"insertPragma": false
"sortPackageJson": false,
"ignorePatterns": []
}
33 changes: 33 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
// typescript, unicorn, oxc and import apply everywhere. vitest is enabled only in the test
// override below, so its rules cannot fire on source files.
"plugins": ["typescript", "unicorn", "oxc", "import"],
// correctness, suspicious and perf catch defects, so they are errors. style and pedantic stay
// off because oxfmt owns layout and the rest is opinion rather than defect detection.
// restriction and nursery are off by default, listed so the whole policy reads in one place.
"categories": {
"correctness": "error",
"suspicious": "error",
"perf": "error",
"pedantic": "off",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"overrides": [
{
// The release script polls external state; each iteration must observe the result of the
// previous one, so the sequential awaits are deliberate.
"files": ["scripts/release.mjs"],
"rules": {
"no-await-in-loop": "off"
}
},
{
"files": ["src/__tests__/**"],
"plugins": ["vitest"]
}
],
"ignorePatterns": ["dist/**"]
}
43 changes: 42 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,46 @@

Use conventional commit style messages.

## Formatting

**ALWAYS run `npm run format` after making any change.** This is not optional and applies to every
file you touch — source, tests, docs, config, JSON, YAML and Markdown alike.

oxfmt is the formatter; its settings live in `.oxfmtrc.json`. It reads `.gitignore`, so build output
is skipped automatically.

- `npm run format` — format the repo in place
- `npm run format:check` — verify without writing

## Linting

**ALWAYS run `npm run lint` after making any change** and fix what it reports.

oxlint is the linter; its settings live in `.oxlintrc.json`. It lints `src/` and `scripts/` — `dist/`
is excluded via `ignorePatterns`.

- `npm run lint` — report violations
- `npm run lint:fix` — apply the fixes oxlint can make automatically

The `correctness`, `suspicious` and `perf` categories are errors. `style` and `pedantic` are
deliberately off: oxfmt already owns layout, and the remainder is opinion rather than defect
detection.

Prefer fixing the code over silencing the rule. When a rule genuinely misfires, disable it at the
narrowest scope that works — an `oxlint-disable-next-line` comment or a file-glob entry in
`overrides` rather than repo-wide — and record why inline. The existing exceptions cover the
sequential polling loop in `scripts/release.mjs` and the in-place `reverse()` in `src/parse.ts`,
where the suggested `toReversed()` is above the ES2022 build target.

## Commit hook

A `pre-commit` hook in `.githooks/` runs both the format check and oxlint against the whole codebase
and **blocks the commit** if either fails, so skipping those steps will stop the commit rather than
slip through. It checks a snapshot of the index, not the working tree, so it sees exactly what the
commit will contain: a file formatted on disk but not re-staged still blocks, and unstaged edits
never do. The hook is wired up by the `prepare` script (`git config core.hooksPath .githooks`) on
`npm install`. Emergency bypass: `git commit --no-verify`.

## Releasing

Releases are cut from `main` by CI, kicked off from your terminal:
Expand Down Expand Up @@ -48,4 +88,5 @@ hook failing mid-release would abort after the npm publish.
`changelog.yml` workflows are shared verbatim with
[sf-formula-parser](https://github.com/jetstreamapp/sf-formula-parser) and
[soql-parser-js](https://github.com/jetstreamapp/soql-parser-js) — keep the copies in sync when
changing them.
changing them. `.githooks/pre-commit` and `.oxfmtrc.json` are shared the same way, with
[simple-excel](https://github.com/jetstreamapp/simple-excel) as well.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Changed

- **Replaced Prettier with [oxfmt](https://oxc.rs/docs/guide/usage/formatter.html) and added
[oxlint](https://oxc.rs/docs/guide/usage/linter) as the project linter**, matching
sf-formula-parser, soql-parser-js and simple-excel. Settings live in `.oxfmtrc.json` and
`.oxlintrc.json`, with `npm run format`, `format:check`, `lint` and `lint:fix`. oxlint runs the
`correctness`, `suspicious` and `perf` categories as errors
- **The `pre-commit` hook blocks a commit unless the whole codebase, as staged, is formatted and
passes lint.** `npm install` wires it up by pointing `core.hooksPath` at `.githooks/`. Emergency
bypass: `git commit --no-verify`
- **CI now gates on `format:check` and `lint`**, since the pre-commit hook can be bypassed
- Internal cleanups surfaced by oxlint, with no behaviour change: `addChild` moved to module scope
so it is no longer recreated on every `parse()` call, and `parseAttributes` dropped a `coerce`
parameter left unused since attribute values stopped being coerced in 1.1.0

## [1.2.0] - 2026-09-30

### Added
Expand Down
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ const result = parse(soapResponse, {

#### ParseOptions

| Option | Type | Default | Description |
| --------------------- | --------- | ------- | -------------------------------------------------------- |
| `trimValues` | `boolean` | `true` | Trim whitespace from text content |
| `ignoreAttributes` | `boolean` | `false` | Skip attributes entirely |
| `removeNSPrefix` | `boolean` | `false` | Strip namespace prefixes (`soap:Body` → `Body`) |
| `attributeNamePrefix` | `string` | `'@_'` | Prefix prepended to attribute names in the output object |
| `parseTagValue` | `boolean` | `true` | Coerce numeric/boolean text values to their JS types |
| `processEntities` | `boolean` | `true` | Decode XML entities (`&amp;` → `&`, `&lt;` → `<`, etc.) |
| `maxDepth` | `number` | `256` | Maximum nesting depth allowed. Throws if exceeded |
| Option | Type | Default | Description |
| --------------------- | --------- | ------- | -------------------------------------------------------------------- |
| `trimValues` | `boolean` | `true` | Trim whitespace from text content |
| `ignoreAttributes` | `boolean` | `false` | Skip attributes entirely |
| `removeNSPrefix` | `boolean` | `false` | Strip namespace prefixes (`soap:Body` → `Body`) |
| `attributeNamePrefix` | `string` | `'@_'` | Prefix prepended to attribute names in the output object |
| `parseTagValue` | `boolean` | `true` | Coerce numeric/boolean text values to their JS types |
| `processEntities` | `boolean` | `true` | Decode XML entities (`&amp;` → `&`, `&lt;` → `<`, etc.) |
| `maxDepth` | `number` | `256` | Maximum nesting depth allowed. Throws if exceeded |
| `strict` | `boolean` | `false` | Throw on malformed XML (unclosed, mismatched, or extra closing tags) |

### `build(obj: Record<string, unknown>, options?: BuildOptions): string`
Expand All @@ -81,12 +81,12 @@ const xml = build(

#### BuildOptions

| Option | Type | Default | Description |
| --------------------- | --------- | ------- | ---------------------------------------------------------- |
| `format` | `boolean` | `false` | Pretty-print with indentation |
| `ignoreAttributes` | `boolean` | `false` | Skip attributes when building XML |
| `attributeNamePrefix` | `string` | `'@_'` | Prefix used to identify attribute keys in the input object |
| `indentBy` | `string` | `' '` | Indentation string used when `format` is `true` |
| Option | Type | Default | Description |
| --------------------- | --------- | ------- | -------------------------------------------------------------- |
| `format` | `boolean` | `false` | Pretty-print with indentation |
| `ignoreAttributes` | `boolean` | `false` | Skip attributes when building XML |
| `attributeNamePrefix` | `string` | `'@_'` | Prefix used to identify attribute keys in the input object |
| `indentBy` | `string` | `' '` | Indentation string used when `format` is `true` |
| `declaration` | `boolean` | `false` | Prepend `<?xml version="1.0" encoding="UTF-8"?>` and a newline |

## Output Shape
Expand Down
Loading
Loading