Visitar URL original
Keep the package loadable on node 12, and enforce it by beaugunderson 路 Pull Request #216 路 beaugunderson/ip-address 路 GitHub
Skip to content

Keep the package loadable on node 12, and enforce it - #216

Merged
beaugunderson merged 5 commits into
mainfrom
bg-node-12
Jul 31, 2026
Merged

beaugunderson merged 5 commits into
mainfrom
bg-node-12

Conversation

@beaugunderson

Copy link
Copy Markdown
Owner

Written by Claude + Beau, so the words here are not entirely my own.

What broke

engines has claimed node >= 12 all along, but nothing enforced it. A tsconfig target of es2020 let a nullish coalescing operator reach dist as written, and node 12 cannot parse it, so require('ip-address') threw a SyntaxError at load. Not a broken code path: a package that could not be loaded at all on the oldest runtime it claims to support. Reproduced against a real node 12.22.12:

dist/ipv6.js:221  host = result[1] ?? result[2];
SyntaxError: Unexpected token '?'

The fix

Lowering the target to es2019 makes tsc downlevel that operator and refuse any newer syntax at compile time, so the emitted output cannot drift past node 12 again without a build failure. lib stays at es2020 for the bigint types, and every es2020 library addition (matchAll, allSettled, globalThis, BigInt) already exists in node 12. The two BigInt literals become BigInt() calls, which is what a target below es2020 requires and what the surrounding code already did. The source change is two lines.

Why node 12 is not a matrix entry

It would fail on the toolchain and report nothing about the library. mocha 11 requires node ^18.18 || ^20.9 || >=21.1, c8 requires 20 || >=22, chai 6 and tsx require node 18 or newer, .mocharc.yml passes --import (a node 20.6+ flag), mocha's own entry point uses node: prefixed requires, and node 12 ships npm 6.14, which cannot read a lockfileVersion 3 lockfile. npm ci dies before a single test runs.

The new smoke-12 job instead builds dist on a current node and then requires it under node 12, which is what a consumer on an old runtime does and the only place a regression in emitted syntax shows up. The smoke test is dependency-free CommonJS covering both address families, subnets, BigInt round trips, byte arrays, and fromURL, the site of the operator in question.

Lint

Nothing linted the TypeScript. ESLint's default extension list is .js alone, so eslint . only ever saw the config file itself and reported clean while seventeen findings sat in the library and its tests, and seventeen files had drifted from prettier's output. Both are resolved here and both are now enforced in CI, with eslint pointed at .ts as well and warnings treated as failures.

The check reports rather than corrects. Correcting in CI would mean granting a workflow write access to the repository, and a token-authored commit does not retrigger workflows, so a pull request would end up with its head unchecked; fork pull requests would need fixing by hand either way. The same check therefore runs from a pre-commit hook, installed by pointing core.hooksPath at a committed hooks directory, with no new dependency, so a violation is caught before it is pushed and npm run lint:fix corrects everything mechanical.

no-bitwise is off rather than suppressed: masks, prefix lengths and the NAT64 and Teredo encodings are defined in terms of shifts and xors, so the rule fires on correct code throughout. Nine inline suppressions had accumulated against six live findings, and one had drifted onto the line after the operator it was meant to cover.

Extending airbnb-base rather than airbnb drops eslint-plugin-react, eslint-plugin-react-hooks and eslint-plugin-jsx-a11y from a library with no react in it, taking 377 lines out of the lockfile and with them the warning the react plugin printed on every run because it could not detect a version. The rules that remain are the same ones; airbnb adds only react and accessibility rules on top of that base. This is the one change here that was not asked for, so it is easy to revert on its own.

Generated README

The API reference is generated from the source, down to the line numbers its per-method links point at, so any change under src leaves it stale until npm run docs runs again. That happened twice while writing these commits. CI now regenerates it and fails if the result differs from what is committed.

Verified locally

Against node 12.22.12 and node 24.18.0: the smoke test passes on both, dist contains no ?? or ?., the 3571 existing tests pass, npm run lint is clean, and the committed README matches its generator's output. The pre-commit hook was checked both ways, blocking an unformatted file and passing on a clean tree.

engines has claimed node >= 12 all along, but nothing enforced it, and a
tsconfig target of es2020 let a nullish coalescing operator reach dist as
written. Node 12 cannot parse it, so requiring the package threw a
SyntaxError at load: not a broken code path but a package that could not be
loaded at all on the oldest runtime it claims.

Lowering the target to es2019 makes tsc downlevel that operator and refuse
any newer syntax at compile time, so the emitted output cannot drift past
node 12 again without a build failure. lib stays at es2020 for the bigint
types; every es2020 library addition already exists in node 12. The two
BigInt literals become BigInt() calls, which is what a target below es2020
requires and what the surrounding code already does.

The dev toolchain cannot run on node 12 to catch this: mocha 11, chai 6, tsx
and c8 all require node 18 or newer, mocha's own entry point uses node:
prefixed requires, and node 12 ships npm 6.14 which cannot read a
lockfileVersion 3 lockfile. A matrix entry would fail on the toolchain and
report nothing about the library. The new job instead builds dist on a
current node and then requires it under node 12, which is what a consumer on
an old runtime does and the only place a regression in emitted syntax shows
up. The smoke test covers both address families, subnets, BigInt round
trips, byte arrays and fromURL, the site of the operator in question.
Seventeen files had drifted from prettier's output, which made a formatting
warning on a touched file indistinguishable from one already there and cost
time on the previous commit deciding which was which. Everything prettier
covers now matches it.

Two categories are excluded rather than reformatted. The vendored InterMapper
address fixtures stay in the shape they arrived in so they remain comparable
against their source. Build output is listed as well, so a local build or
coverage run cannot make a check of the whole tree fail on files that are not
in the repository.

The example block at the top of the README aligns its result comments into a
column, which prettier would collapse, so the template marks that one fence
prettier-ignore. The marker is an HTML comment and flows through into the
generated README, where it does not render.
Nothing linted the TypeScript: eslint's default extension list is .js alone,
so `eslint .` only ever saw the config file itself and reported clean while
seventeen findings sat in the library and its tests. They are all resolved
here so the rules can be enforced.

no-bitwise is off rather than suppressed. Masks, prefix lengths and the NAT64
and Teredo encodings are defined in terms of shifts and xors, so the rule
fires on correct code throughout: nine inline suppressions had accumulated
against six live findings, and one suppression had drifted onto the line after
the operator it was meant to cover. The suppressions are gone with it.

The two ignored catch bindings become optional catch bindings, which es2019
allows and node 12 supports. The NAT64 bit assembly becomes an array joined
at the end, so the reserved u octet can be named instead of appearing as a
bare string in the middle of a concatenation. A shadowed path in the readme
builder and a shadowed v6 in a test are renamed for what they hold, and the
closure returned by isCorrect gets a name.

Build scripts are allowed to write to stdout, which is how they report
progress.
There was no lint script and no CI step for one, so formatting drift and lint
findings only surfaced when someone happened to run the tools by hand. CI now
runs both against the whole tree, with eslint pointed at .ts as well as .js
and warnings treated as failures so nothing accumulates below the threshold.

The check reports rather than corrects. Correcting in CI means granting a
workflow write access to the repository, and a token-authored commit does not
retrigger workflows, so a pull request would end up with its head unchecked;
fork pull requests would still need fixing by hand either way. The same check
therefore runs from a pre-commit hook, installed by pointing core.hooksPath at
a committed hooks directory, so a violation is caught before it is pushed and
`npm run lint:fix` corrects everything mechanical.

Extending airbnb-base rather than airbnb drops eslint-plugin-react,
eslint-plugin-react-hooks and eslint-plugin-jsx-a11y from a library with no
react in it, taking 377 lines out of the lockfile and with them the warning
the react plugin printed on every run because it could not detect a version.
The rules that remain are the same ones; airbnb adds only react and
accessibility rules on top of that base.
The API reference in the README is generated from the source, including the
line numbers its per-method source links point at, so any change under src
leaves it stale until `npm run docs` is run again. That happened twice while
writing the previous commits and was only noticed by hand.

CI now regenerates it and fails if the result differs from what is committed.
@beaugunderson
beaugunderson merged commit bac8810 into main Jul 31, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant