Skip to content

chore: Add ESLint and Unify the Source Style - #32

Merged
Xtrimmer merged 1 commit into
masterfrom
xtrimmer/eslint
Aug 6, 2026
Merged

Xtrimmer merged 1 commit into
masterfrom
xtrimmer/eslint

Conversation

@Xtrimmer

@Xtrimmer Xtrimmer commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Problem

The page sources and the tests had drifted into opposite conventions, and nothing enforced either:

js/ before test/
let 289 8
const 77 434
template literals 0 208
var 7 0

That is mostly an artefact of how the BOLT 12 work landed: across M0–M11 I matched the 2018 house style inside js/ and wrote the tests in modern style, so ~2,000 lines of new code took the idiom of the file it sat beside rather than the idiom of the language.

Solution

eslint.config.js treats js/ as browser scripts sharing one global scope and test//tools/ as CommonJS. The 44 names that actually cross a file boundary are listed explicitly so no-undef catches a typo in a cross-file call.

After:

before after
var 7 0
let 289 31
const 77 341
template literals 0 122

eslint --fix handled 416 of 436 findings. Its prefer-template fixer keeps the spacing from the concatenation it replaces, leaving ${ what } in 60 places, so a script trims the padding — the emitted strings are identical either way.

The 20 done by hand

Two were latent bugs rather than style:

prefix == null could not become === null. The value is undefined when no prefix matches, so the strict form would have skipped the throw and produced a TypeError on the next line instead of Malformed request: unknown prefix. Corrected to === undefined. ESLint's own fixer declines to touch == null for exactly this reason, which I confirmed before running --fix:

2:12  error  Expected '===' and instead saw '=='  eqeqeq
--- diff after --fix ---
  (unchanged)

getUrlParam tested whether the href contained the parameter name anywhere. A URL whose path contained invoice but had no ?invoice= returned undefined rather than the default, and isEmptyOrSpaces then threw on it. Now it looks the key up and falls back.

The rest:

  • getUrlVars used String.replace as a loop and discarded the result → matchAll. Not URLSearchParams, which decodes + to a space and would break a BOLT 12 string written with continuations:
    current regex : "l+no1zcss9mk8y3wkklf"
    URLSearchParams: "l no1zcss9mk8y3wkklf"
    
  • str.length == 0===
  • case 'f': declared a const without a block
  • three catch blocks discarded the error they caught and now pass it as cause
  • BOLT12_PREFIXES was left unused when M11 replaced the unsupported-prefix throw — genuinely dead, and the first thing the linter earned its place by finding

Splitting hex literals at a record boundary is deliberate in tlv.test.js, so no-useless-concat is disabled there with a note on what the split means. reportUnusedDisableDirectives is on, so that comment becomes an error if it stops being needed.

test/dead-code.test.js

no-unused-vars uses vars: 'local', because a top-level name in a script is the cross-file interface, not an unused binding. ESLint lints one file at a time and cannot tell the difference. A new test covers that at project scope instead, where it can see the other sources, index.html and the tests — strictly more accurate than the per-file rule.

It also fails when two source files declare the same top-level name. That is a SyntaxError when the page loads them as separate <script> tags, it is invisible to every other test, and it cost me a debugging cycle in M7. Both checks are verified to actually fire, by injecting the faults:

injecting an unused function       -> not ok 10 - every name merkle.js defines is used somewhere
                                            +   'neverUsedAnywhere'
injecting the exact M7 collision   -> not ok 2 - the vendored library does not clash with a source name
                                            +   'taggedHash'

Verification

npm run lint  →  0 problems
npm test      →  539 tests, 539 pass, 0 fail, 0 todo   (18 new)

Behaviour is unchanged, checked at the level that matters for a refactor that rewrites error strings: 42 successful decodes and 71 error messages across all four request forms, byte-identical to master. The error paths are the strongest part of that — they are precisely the strings prefer-template rewrote.

toFixed is pinned separately on 14 numeric cases either side of the exponential-notation boundaries, since Math.pow** and Array.joinrepeat touch display arithmetic.

The page still renders 16/16 invoices, 20/20 offers, 5/5 payer proofs and 1/1 invoice request, with 23/23 invalid proofs showing the error banner, and the ?invoice= parameter still populates and decodes over file://.

CI

A lint job runs npm ci && npm run lint. The test job deliberately does not install anything — the suite uses only Node builtins, so it keeps proving the project has no runtime dependencies. ESLint is the first devDependency; package-lock.json is now committed for npm ci.

Not fixed here

toFixed(1e-7) returns 0.00000009999999999999999. That is a pre-existing floating-point artefact in the 2018 helper, unrelated to linting, and I left it alone rather than change display arithmetic in a style PR.

The page sources and the tests had drifted into opposite conventions: js/ used
let almost everywhere and built strings by concatenation, while test/ used const
and template literals. Nothing enforced either.

eslint.config.js treats js/ as browser scripts sharing one global scope and test/
and tools/ as CommonJS. The 44 names that cross a file boundary are listed so
no-undef catches a typo in a cross-file call.

eslint --fix handled 416 of 436 findings. Its prefer-template fixer keeps the
spacing from the concatenation it replaces, leaving ${  x  } in 60 places, so a
script trims the padding; the strings are unchanged either way.

The rest were done by hand:

- prefix == null became prefix === undefined. The value is undefined rather than
  null when no prefix matches, so === null would have stopped the throw and left
  a TypeError on the next line. eslint's fixer leaves == null alone for this
  reason.
- str.length == 0 became ===.
- case 'f' declared a const without a block.
- getUrlVars used String.replace as a loop and discarded the result. It now uses
  matchAll. URLSearchParams would decode '+' to a space and break a bolt12 string
  written with continuations, so the raw regex stays.
- getUrlParam tested whether the href contained the parameter name anywhere, so a
  path containing "invoice" returned undefined instead of the default, and
  isEmptyOrSpaces then threw on it.
- Three catch blocks discarded the error they caught and now pass it as cause.
- BOLT12_PREFIXES was left unused when M11 replaced the unsupported-prefix throw.

Splitting hex literals at a record boundary is deliberate in tlv.test.js, so
no-useless-concat is disabled there with a note on what the split means.

no-unused-vars uses vars: local, because a top-level name in a script is the
cross-file interface rather than an unused binding. test/dead-code.test.js covers
that instead, where it can see the other sources, index.html and the tests. It
also fails on two files declaring the same top-level name, which is a SyntaxError
when the page loads them as separate scripts and cost a debugging cycle in M7.
@Xtrimmer Xtrimmer closed this Aug 6, 2026
@Xtrimmer Xtrimmer reopened this Aug 6, 2026
@Xtrimmer
Xtrimmer merged commit 48a4a87 into master Aug 6, 2026
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