From c2d9d4841bedcdc7c232c3184033c06995eecf55 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Fri, 2 Oct 2026 13:51:35 -0500 Subject: [PATCH 1/8] chore: replace prettier with oxfmt and add oxlint Adopt the oxc toolchain already used by sf-formula-parser, soql-parser-js and simple-excel. .prettierrc becomes .oxfmtrc.json with the same settings those repositories share, and .oxlintrc.json adds a linter, which the repository did not have. Both are exposed as format, format:check, lint and lint:fix. oxlint runs correctness, suspicious and perf as errors; style and pedantic stay off because oxfmt owns layout. The only overrides are the ones this codebase needs: __dirname in the parse tests and the sequential polling loop in the shared release script. oxlint is locked at 1.85.0 to match the sibling repositories, so all four lint with the same rule set. The reformat touches only lines Prettier had also left unformatted: README table padding and a few test assertions that fit within 140 columns. .claude/settings.json now matches the copy the sibling repositories share. --- .claude/settings.json | 4 +- .prettierrc => .oxfmtrc.json | 4 +- .oxlintrc.json | 37 ++ README.md | 30 +- package-lock.json | 824 ++++++++++++++++++++++++++++++++++- package.json | 7 +- src/__tests__/build.test.ts | 10 +- src/__tests__/parse.test.ts | 8 +- 8 files changed, 873 insertions(+), 51 deletions(-) rename .prettierrc => .oxfmtrc.json (61%) create mode 100644 .oxlintrc.json diff --git a/.claude/settings.json b/.claude/settings.json index 1d9dc48..36be7a1 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,7 +1,5 @@ { "permissions": { - "allow": [ - "Bash(npx vitest:*)" - ] + "allow": ["Bash(npx vitest:*)", "Bash(npx tsc:*)", "Bash(npx oxfmt:*)", "Bash(npx oxlint:*)", "Bash(npm test:*)", "Bash(npm run:*)"] } } diff --git a/.prettierrc b/.oxfmtrc.json similarity index 61% rename from .prettierrc rename to .oxfmtrc.json index 3dd4f06..4312a38 100644 --- a/.prettierrc +++ b/.oxfmtrc.json @@ -1,4 +1,5 @@ { + "$schema": "./node_modules/oxfmt/configuration_schema.json", "printWidth": 140, "semi": true, "tabWidth": 2, @@ -7,5 +8,6 @@ "trailingComma": "all", "bracketSpacing": true, "arrowParens": "avoid", - "insertPragma": false + "sortPackageJson": false, + "ignorePatterns": [] } diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..4fc5535 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,37 @@ +{ + "$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" + }, + "rules": { + // Node's `__dirname`, which the parse tests use to load their fixtures, is deliberate. + "no-underscore-dangle": "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/**"], + "rules": { + "no-await-in-loop": "off" + } + }, + { + "files": ["src/__tests__/**"], + "plugins": ["vitest"] + } + ], + "ignorePatterns": ["dist/**"] +} diff --git a/README.md b/README.md index 5b56c17..3b26cb0 100644 --- a/README.md +++ b/README.md @@ -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 (`&` → `&`, `<` → `<`, 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 (`&` → `&`, `<` → `<`, 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, options?: BuildOptions): string` @@ -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 `` and a newline | ## Output Shape diff --git a/package-lock.json b/package-lock.json index 305784f..7a0ac8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "@types/node": "^26.6.3", "esbuild": "^0.28.2", "npm-run-all": "^4.1.5", - "prettier": "^3.9.9", + "oxfmt": "^0.70.0", + "oxlint": "^1.85.0", "release-it": "^21.1.0", "typescript": "^7.0.2", "vitest": "^5.0.2" @@ -1011,6 +1012,700 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.70.0.tgz", + "integrity": "sha512-Xd7YO4/T2axEj6FTLcj4Why3mTBqFMg+x24xtorT4Lb2+1g82090GH0a/4U1m0pABGYiix2bq1pqkYrmV3f0Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.70.0.tgz", + "integrity": "sha512-x9rlMYyKXdgKdYyUJzGsK1ZV8P4di/J32ipzcS6Jet6p9r9UAh28neXIMtdlSaJJycdi61Z4YkcLKLpk8ueFjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.70.0.tgz", + "integrity": "sha512-IUTUPvrBVYy7POh4stXzRdz4IVC/1QSaviCWoyenSlOhGu0X9j5K07vCTM9biLjAA2Zs31l0Rj5vvRpj9n95wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.70.0.tgz", + "integrity": "sha512-vw745q870oTd6J517O24asoX4/E+eK0nxYIFoedSLqgJ+nI5En7+ZS82iZSHZ69zevQrnOXiyHP01dA+t8xD8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.70.0.tgz", + "integrity": "sha512-NO14EgSM9dFkcg+MfGPxvsKqXYs9LKaxPrOKXpv1R0rLokGGFDcCq6dBMq18dE4wlpFOovX0UZY2uh1P30O7QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.70.0.tgz", + "integrity": "sha512-139OEhHarj9CYoJ/i9gXlPv4KLBGtLj2toseOWYFf09QwlhklaZk+wW3aOvlqoeZtuayvkoSNVWra7WJ21s3VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.70.0.tgz", + "integrity": "sha512-GEh2PY3IWTE0M24eNhTduountANSbWyDmMnzFSQE/nGg/bjPugbUgiGuFu+xdqcQSd/HKSwH80/F2yVVD48yhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.70.0.tgz", + "integrity": "sha512-En5i+UJmZSPxuSf47F2Hl5YOzKB0bicQLnGQkeTCMQ35cWLtbrSwACJKfiLqRZrk05DwSnsJkhBRaM3OURtIaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.70.0.tgz", + "integrity": "sha512-WWOoV5W9Im3flVwOVrWn/2DUlOF8v5vcCip+kcNuaMpulRCh6nzzt1Su2vcL2F908YJIXNV3HvegbBHuyLwKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.70.0.tgz", + "integrity": "sha512-YUouneIqW+5n7aE8xx/zeZ6/utr/KH7oykcGoFyd8Uz8uh591T1oKlnoWA3BsRq/ZR42oY1w4MUYvS/0e/MQOA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.70.0.tgz", + "integrity": "sha512-iEnMf21S5aGVa4hViDGY8sAQ/AHyCu2JPyrQF8P06wtHhSkD1YJBeT4m/KiGewgf7+a5XCYSCRIPcRQa1xwEoQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.70.0.tgz", + "integrity": "sha512-91Sdniaj20fQzyMeCxMDzTP4c9s4RB8dGQ308xHhDR0n6U7+1Xq7N9klE7mfXq8iV3lRmIGSXi5X23Hn/0XX/g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.70.0.tgz", + "integrity": "sha512-uUV30M6E+2TKKGMaKiwfeL4RZrviHXlUxsrYJ/jFBb+1EZy+pnFT+hF73eeWdzh5NqPOAnw0iiMAIqjqiLZPFg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.70.0.tgz", + "integrity": "sha512-ivMcX6kNDPhqtbOaBt/ItFlLlTlXNHLgRuNmxP6Na6UuYXRT10llpJcAPbGeRgjjb3Qzv4jwPp3fB0hui50WNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.70.0.tgz", + "integrity": "sha512-w+S+fERxYmlZSyZlJK/U292FjyBoH8cCEj21/tYJX6atX5kNSn+HDkhlFQKT2zcMwUW0uAtUL/bOrlwJZwqRdA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.70.0.tgz", + "integrity": "sha512-Zlom1Xkx257R8bk4ZI4zJsrGno2opknz1+5v5baka3nn4FPyvNSdh8JUL4CdN1S1OWRMvJ9UJQ+RfIqGGCUEfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.70.0.tgz", + "integrity": "sha512-FQgPW5R17vzt7cgrJ8eG/dqX00o2xHsqFeLfw4xzA9FRHpN/DjFo9YDonvIIXGxiEuS9F/jZPnGO+KHNKuCo4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.70.0.tgz", + "integrity": "sha512-ZfZublNhZ+XBndMiXhkiLlPE+XyGRDa0CweeTL6t1fZypfCh1LTg7e5CvnOeTBunq15MskOcRempumSPGAaCQA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.70.0.tgz", + "integrity": "sha512-HlIZEn+WzLQL0DszNzldiRl/DPRCX5R0Vkt6qeUPR1YHwy52hZZo4x6HoTOVmKRP2wUiwPGtKsihNY/f8KRaBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.85.0.tgz", + "integrity": "sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.85.0.tgz", + "integrity": "sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.85.0.tgz", + "integrity": "sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.85.0.tgz", + "integrity": "sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.85.0.tgz", + "integrity": "sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.85.0.tgz", + "integrity": "sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.85.0.tgz", + "integrity": "sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.85.0.tgz", + "integrity": "sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.85.0.tgz", + "integrity": "sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.85.0.tgz", + "integrity": "sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.85.0.tgz", + "integrity": "sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.85.0.tgz", + "integrity": "sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.85.0.tgz", + "integrity": "sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.85.0.tgz", + "integrity": "sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.85.0.tgz", + "integrity": "sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.85.0.tgz", + "integrity": "sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.85.0.tgz", + "integrity": "sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.85.0.tgz", + "integrity": "sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.85.0.tgz", + "integrity": "sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@phun-ky/typeof": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@phun-ky/typeof/-/typeof-2.0.3.tgz", @@ -4479,6 +5174,107 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oxfmt": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.70.0.tgz", + "integrity": "sha512-IsHxZ4y0wQLLMhnrJblBJgZsLDzfULrJnAw5j/QqsTlMa/m3AqsbToi+W71uhBGaqlqq/PbbjvHc09TJwdv3Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinypool": "2.1.2" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.70.0", + "@oxfmt/binding-android-arm64": "0.70.0", + "@oxfmt/binding-darwin-arm64": "0.70.0", + "@oxfmt/binding-darwin-x64": "0.70.0", + "@oxfmt/binding-freebsd-x64": "0.70.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.70.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.70.0", + "@oxfmt/binding-linux-arm64-gnu": "0.70.0", + "@oxfmt/binding-linux-arm64-musl": "0.70.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.70.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.70.0", + "@oxfmt/binding-linux-riscv64-musl": "0.70.0", + "@oxfmt/binding-linux-s390x-gnu": "0.70.0", + "@oxfmt/binding-linux-x64-gnu": "0.70.0", + "@oxfmt/binding-linux-x64-musl": "0.70.0", + "@oxfmt/binding-openharmony-arm64": "0.70.0", + "@oxfmt/binding-win32-arm64-msvc": "0.70.0", + "@oxfmt/binding-win32-ia32-msvc": "0.70.0", + "@oxfmt/binding-win32-x64-msvc": "0.70.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxlint": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.85.0.tgz", + "integrity": "sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.85.0", + "@oxlint/binding-android-arm64": "1.85.0", + "@oxlint/binding-darwin-arm64": "1.85.0", + "@oxlint/binding-darwin-x64": "1.85.0", + "@oxlint/binding-freebsd-x64": "1.85.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", + "@oxlint/binding-linux-arm-musleabihf": "1.85.0", + "@oxlint/binding-linux-arm64-gnu": "1.85.0", + "@oxlint/binding-linux-arm64-musl": "1.85.0", + "@oxlint/binding-linux-ppc64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-musl": "1.85.0", + "@oxlint/binding-linux-s390x-gnu": "1.85.0", + "@oxlint/binding-linux-x64-gnu": "1.85.0", + "@oxlint/binding-linux-x64-musl": "1.85.0", + "@oxlint/binding-openharmony-arm64": "1.85.0", + "@oxlint/binding-win32-arm64-msvc": "1.85.0", + "@oxlint/binding-win32-ia32-msvc": "1.85.0", + "@oxlint/binding-win32-x64-msvc": "1.85.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, "node_modules/pac-proxy-agent": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-9.1.0.tgz", @@ -4714,22 +5510,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/prettier": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", - "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/protocols": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", @@ -5592,6 +6372,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.2.tgz", + "integrity": "sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index 83f695d..4adc372 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,10 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit -p tsconfig.typecheck.json", + "format": "oxfmt", + "format:check": "oxfmt --check", + "lint": "oxlint", + "lint:fix": "oxlint --fix", "release": "node scripts/release.mjs", "release:ci": "release-it", "release:increment": "node scripts/derive-increment.mjs --explain" @@ -44,7 +48,8 @@ "@types/node": "^26.6.3", "esbuild": "^0.28.2", "npm-run-all": "^4.1.5", - "prettier": "^3.9.9", + "oxfmt": "^0.70.0", + "oxlint": "^1.85.0", "release-it": "^21.1.0", "typescript": "^7.0.2", "vitest": "^5.0.2" diff --git a/src/__tests__/build.test.ts b/src/__tests__/build.test.ts index f471770..c045336 100644 --- a/src/__tests__/build.test.ts +++ b/src/__tests__/build.test.ts @@ -146,9 +146,7 @@ describe('build', () => { describe('builder edge cases', () => { it('builds elements with special characters in text that need escaping', () => { - expect(build({ root: 'Tom & Jerry <"friends">' })).toBe( - 'Tom & Jerry <"friends">', - ); + expect(build({ root: 'Tom & Jerry <"friends">' })).toBe('Tom & Jerry <"friends">'); }); it('handles deeply nested objects', () => { @@ -159,11 +157,7 @@ describe('build', () => { it('handles arrays of objects with mixed content', () => { const obj = { root: { - item: [ - { '@_id': '1', '#text': 'first' }, - { '@_id': '2', child: 'nested' }, - 'plain', - ], + item: [{ '@_id': '1', '#text': 'first' }, { '@_id': '2', child: 'nested' }, 'plain'], }, }; const result = build(obj); diff --git a/src/__tests__/parse.test.ts b/src/__tests__/parse.test.ts index c003d61..5952e81 100644 --- a/src/__tests__/parse.test.ts +++ b/src/__tests__/parse.test.ts @@ -285,9 +285,7 @@ describe('parse', () => { }); it('throws on mismatched closing tags', () => { - expect(() => parse('text', { strict: true })).toThrow( - 'Mismatched closing tag: expected but found ', - ); + expect(() => parse('text', { strict: true })).toThrow('Mismatched closing tag: expected but found '); }); it('does not throw for well-formed XML in strict mode', () => { @@ -295,9 +293,7 @@ describe('parse', () => { }); it('throws on extra closing tags in strict mode', () => { - expect(() => parse('text', { strict: true })).toThrow( - 'Unexpected closing tag ', - ); + expect(() => parse('text', { strict: true })).toThrow('Unexpected closing tag '); }); it('silently ignores unclosed tags when strict is false (default)', () => { From 549ff644141fc56f1895b359a6e013290f012f4b Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Fri, 2 Oct 2026 13:51:56 -0500 Subject: [PATCH 2/8] refactor(parse): resolve oxlint findings No behaviour change. - addChild captures nothing from parse(), so it moves to module scope rather than being recreated on every call. - parseAttributes drops its coerce parameter, unused since 1.1.0 stopped coercing attribute values. - The reverse() that orders the unclosed-tag error is kept and the rule silenced on that line: it reverses the fresh array from map(), so nothing shared is mutated, and the suggested toReversed() is ES2023, which esbuild does not polyfill for the ES2022 build target. --- src/parse.ts | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index 184c8d9..414bd5e 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -40,6 +40,19 @@ function parseValue(val: string, coerce: boolean): string | number | boolean { return val; } +function addChild(parent: Record, key: string, value: unknown): void { + if (key in parent) { + const existing = parent[key]; + if (Array.isArray(existing)) { + existing.push(value); + } else { + parent[key] = [existing, value]; + } + } else { + parent[key] = value; + } +} + interface StackEntry { tagName: string; children: Record; @@ -66,19 +79,6 @@ export function parse(xml: string, options?: ParseOptions): Record, key: string, value: unknown): void { - if (key in parent) { - const existing = parent[key]; - if (Array.isArray(existing)) { - existing.push(value); - } else { - parent[key] = [existing, value]; - } - } else { - parent[key] = value; - } - } - while (i < len) { if (xml[i] === '<') { // Flush any pending text content @@ -212,7 +212,7 @@ export function parse(xml: string, options?: ParseOptions): Record = {}; if (!ignoreAttributes && attrString) { - parseAttributes(attrString, attrs, attrPrefix, rmNSPrefix, processEnt, parseTagVal); + parseAttributes(attrString, attrs, attrPrefix, rmNSPrefix, processEnt); } if (selfClosing) { @@ -246,6 +246,9 @@ export function parse(xml: string, options?: ParseOptions): Record e.tagName) + // Reverses the fresh array from map(), so nothing shared is mutated. toReversed() is ES2023, + // which esbuild does not polyfill for the ES2022 target. + // oxlint-disable-next-line unicorn/no-array-reverse .reverse() .join(', '); throw new Error(`Unclosed tag(s): ${unclosed}`); @@ -269,14 +272,7 @@ function findTagEnd(xml: string, start: number): number { return -1; } -function parseAttributes( - str: string, - attrs: Record, - prefix: string, - rmNS: boolean, - processEnt: boolean, - coerce: boolean, -): void { +function parseAttributes(str: string, attrs: Record, prefix: string, rmNS: boolean, processEnt: boolean): void { const re = /([^\s=]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g; let match: RegExpExecArray | null; while ((match = re.exec(str)) !== null) { From 70f3deb210e400bdb8220c63d2a735340f81b76d Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Fri, 2 Oct 2026 13:51:56 -0500 Subject: [PATCH 3/8] ci: enforce formatting and linting on commit and in CI A pre-commit hook blocks commits whose staged files are unformatted or fail lint, running both so one attempt reports everything to fix. The prepare script wires it up on npm install via core.hooksPath. The hook is the copy shared with sf-formula-parser, soql-parser-js and simple-excel. CI gates on format:check and lint ahead of typecheck, because the hook can be bypassed with --no-verify. --- .githooks/pre-commit | 62 ++++++++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 3 ++ package.json | 1 + 3 files changed, 66 insertions(+) create mode 100755 .githooks/pre-commit diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..3cd603a --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,62 @@ +#!/bin/sh +# Blocks a commit when any staged file is unformatted or fails 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 with jetstreamapp/sf-formula-parser, jetstreamapp/soql-parser-js +# and jetstreamapp/simple-excel - keep the copies in sync when changing it. + +OXFMT="./node_modules/.bin/oxfmt" +OXLINT="./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 + +# Added, copied, modified and renamed paths only - deleted paths are not there to check. +staged=$(git diff --cached --name-only --diff-filter=ACMR) + +if [ -z "$staged" ]; then + exit 0 +fi + +# Both tools skip what they do not handle, so the whole staged list goes to each rather than +# being filtered by extension here, which would drift as the tools gain file types. +# --no-error-on-unmatched-pattern: a staged list with nothing for that tool is not an error. +# The list is newline delimited, so convert to NUL to keep paths containing spaces intact. +check() { + printf '%s\n' "$staged" | tr '\n' '\0' | xargs -0 "$@" --no-error-on-unmatched-pattern +} + +# Both run even when the first fails, so one commit attempt reports everything to fix. +status=0 + +if ! check "$OXFMT" --check; then + cat >&2 <<'MSG' + +Commit blocked: the staged files listed above are not formatted. + + npm run format # format the repo + git add -u # re-stage the result +MSG + status=1 +fi + +if ! check "$OXLINT"; then + cat >&2 <<'MSG' + +Commit blocked: the staged 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 # re-stage the result +MSG + status=1 +fi + +exit $status diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9392859..a2aad38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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. diff --git a/package.json b/package.json index 4adc372..a299f30 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "format:check": "oxfmt --check", "lint": "oxlint", "lint:fix": "oxlint --fix", + "prepare": "git config core.hooksPath .githooks || true", "release": "node scripts/release.mjs", "release:ci": "release-it", "release:increment": "node scripts/derive-increment.mjs --explain" From 16fbc589a7ba21c2c2d62a83dc1eb2efde9bb80d Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Fri, 2 Oct 2026 13:51:56 -0500 Subject: [PATCH 4/8] docs: document the oxc toolchain and commit hook AGENTS.md gains formatting, linting and commit hook sections, and lists the hook and oxfmt config among the files shared with the sibling repositories. The changelog records the change under Changed. --- AGENTS.md | 41 ++++++++++++++++++++++++++++++++++++++++- CHANGELOG.md | 15 +++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 226ec27..e086634 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,44 @@ 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/` 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 staged files and +**blocks the commit** if either fails, so skipping those steps will stop the commit rather than slip +through. 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: @@ -48,4 +86,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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1f5b5..cb85ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 when staged files are unformatted or fail 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 From 7f8b84f5cf3c0ad9a2829f49520b568177a9157f Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sat, 10 Oct 2026 10:15:08 -0500 Subject: [PATCH 5/8] chore: remove shared Claude Code permissions The project allow list auto-approved every npm script, including the release scripts. Drop it so each contributor's own settings apply. --- .claude/settings.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 36be7a1..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "permissions": { - "allow": ["Bash(npx vitest:*)", "Bash(npx tsc:*)", "Bash(npx oxfmt:*)", "Bash(npx oxlint:*)", "Bash(npm test:*)", "Bash(npm run:*)"] - } -} From 524fc45ac70c36982c583de046f040ec442f6991 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sat, 10 Oct 2026 10:15:08 -0500 Subject: [PATCH 6/8] chore(lint): scope oxlint exceptions to the code that needs them Rename the parse test's local __dirname to fixturesDir so no-underscore-dangle needs no repo-wide exception, and limit the no-await-in-loop override to scripts/release.mjs, the only file with a polling loop. --- .oxlintrc.json | 6 +----- AGENTS.md | 4 ++-- src/__tests__/parse.test.ts | 6 +++--- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 4fc5535..0da908f 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -15,15 +15,11 @@ "restriction": "off", "nursery": "off" }, - "rules": { - // Node's `__dirname`, which the parse tests use to load their fixtures, is deliberate. - "no-underscore-dangle": "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/**"], + "files": ["scripts/release.mjs"], "rules": { "no-await-in-loop": "off" } diff --git a/AGENTS.md b/AGENTS.md index e086634..6cdbfdf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,8 +32,8 @@ 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/` and the in-place `reverse()` in `src/parse.ts`, where the -suggested `toReversed()` is above the ES2022 build target. +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 diff --git a/src/__tests__/parse.test.ts b/src/__tests__/parse.test.ts index 5952e81..f960458 100644 --- a/src/__tests__/parse.test.ts +++ b/src/__tests__/parse.test.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; import { parse } from '../parse.js'; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const fixturesDir = dirname(fileURLToPath(import.meta.url)); describe('parse', () => { describe('basic parsing', () => { @@ -324,8 +324,8 @@ describe('parse', () => { describe('Anonymous Apex', () => { it('parses an Anonymous Apex SOAP response', () => { - const xml = readFileSync(join(__dirname, 'anonymous-apex-soap-response.xml'), 'utf-8'); - const expectedLog = readFileSync(join(__dirname, 'anonymous-apex-response-body.log'), 'utf-8'); + const xml = readFileSync(join(fixturesDir, 'anonymous-apex-soap-response.xml'), 'utf-8'); + const expectedLog = readFileSync(join(fixturesDir, 'anonymous-apex-response-body.log'), 'utf-8'); const result = parse(xml, { trimValues: true, From 8cdf95adf68d34e9b53e181eebc50a5bdcc59182 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sat, 10 Oct 2026 10:15:08 -0500 Subject: [PATCH 7/8] fix(hooks): check the whole codebase as staged in pre-commit The hook passed staged paths to oxfmt and oxlint, which read the working tree. A file formatted on disk but never re-staged could slip through, and unstaged edits could block a clean commit. It now exports the index to a temporary directory and checks every tracked file there, so it sees exactly what the commit will contain. --- .githooks/pre-commit | 45 +++++++++++++++++++++++--------------------- AGENTS.md | 8 +++++--- CHANGELOG.md | 6 +++--- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 3cd603a..b3019bb 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,5 +1,6 @@ #!/bin/sh -# Blocks a commit when any staged file is unformatted or fails lint. +# 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. @@ -8,8 +9,9 @@ # This file is shared verbatim with jetstreamapp/sf-formula-parser, jetstreamapp/soql-parser-js # and jetstreamapp/simple-excel - keep the copies in sync when changing it. -OXFMT="./node_modules/.bin/oxfmt" -OXLINT="./node_modules/.bin/oxlint" +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 @@ -18,43 +20,44 @@ for bin in "$OXFMT" "$OXLINT"; do fi done -# Added, copied, modified and renamed paths only - deleted paths are not there to check. -staged=$(git diff --cached --name-only --diff-filter=ACMR) +# 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 ` 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 -if [ -z "$staged" ]; then - exit 0 -fi +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 -# Both tools skip what they do not handle, so the whole staged list goes to each rather than -# being filtered by extension here, which would drift as the tools gain file types. -# --no-error-on-unmatched-pattern: a staged list with nothing for that tool is not an error. -# The list is newline delimited, so convert to NUL to keep paths containing spaces intact. -check() { - printf '%s\n' "$staged" | tr '\n' '\0' | xargs -0 "$@" --no-error-on-unmatched-pattern -} +cd "$snapshot" || exit 1 # Both run even when the first fails, so one commit attempt reports everything to fix. status=0 -if ! check "$OXFMT" --check; then +if ! "$OXFMT" --check; then cat >&2 <<'MSG' -Commit blocked: the staged files listed above are not formatted. +Commit blocked: the files listed above are not formatted. npm run format # format the repo - git add -u # re-stage the result + git add -u # stage the result MSG status=1 fi -if ! check "$OXLINT"; then +if ! "$OXLINT"; then cat >&2 <<'MSG' -Commit blocked: the staged files listed above have lint errors. +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 # re-stage the result + git add -u # stage the result MSG status=1 fi diff --git a/AGENTS.md b/AGENTS.md index 6cdbfdf..5948322 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,9 +37,11 @@ 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 staged files and -**blocks the commit** if either fails, so skipping those steps will stop the commit rather than slip -through. The hook is wired up by the `prepare` script (`git config core.hooksPath .githooks`) on +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 diff --git a/CHANGELOG.md b/CHANGELOG.md index cb85ba4..83ba85d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,9 @@ All notable changes to this project will be documented in this file. 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 when staged files are unformatted or fail lint.** - `npm install` wires it up by pointing `core.hooksPath` at `.githooks/`. Emergency bypass: - `git commit --no-verify` +- **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` From aa508b8d88c3371633063a896a6b076785eaa8e3 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sat, 10 Oct 2026 11:18:27 -0500 Subject: [PATCH 8/8] chore(hooks): name every repo that shares the pre-commit hook Each copy named only the other repos, so the four files could never be byte-identical. The header now lists all four, so every copy matches exactly. --- .githooks/pre-commit | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index b3019bb..a61d20f 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -6,8 +6,8 @@ # 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 with jetstreamapp/sf-formula-parser, jetstreamapp/soql-parser-js -# and jetstreamapp/simple-excel - keep the copies in sync when changing it. +# 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"