chore: Add ESLint and Unify the Source Style - #32
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The page sources and the tests had drifted into opposite conventions, and nothing enforced either:
js/beforetest/letconstvarThat 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.jstreatsjs/as browser scripts sharing one global scope andtest//tools/as CommonJS. The 44 names that actually cross a file boundary are listed explicitly sono-undefcatches a typo in a cross-file call.After:
varletconsteslint --fixhandled 416 of 436 findings. Itsprefer-templatefixer 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 == nullcould not become=== null. The value isundefinedwhen no prefix matches, so the strict form would have skipped the throw and produced aTypeErroron the next line instead ofMalformed request: unknown prefix. Corrected to=== undefined. ESLint's own fixer declines to touch== nullfor exactly this reason, which I confirmed before running--fix:getUrlParamtested whether the href contained the parameter name anywhere. A URL whose path containedinvoicebut had no?invoice=returnedundefinedrather than the default, andisEmptyOrSpacesthen threw on it. Now it looks the key up and falls back.The rest:
getUrlVarsusedString.replaceas a loop and discarded the result →matchAll. NotURLSearchParams, which decodes+to a space and would break a BOLT 12 string written with continuations:str.length == 0→===case 'f':declared aconstwithout a blockcatchblocks discarded the error they caught and now pass it ascauseBOLT12_PREFIXESwas left unused when M11 replaced the unsupported-prefix throw — genuinely dead, and the first thing the linter earned its place by findingSplitting hex literals at a record boundary is deliberate in
tlv.test.js, sono-useless-concatis disabled there with a note on what the split means.reportUnusedDisableDirectivesis on, so that comment becomes an error if it stops being needed.test/dead-code.test.js
no-unused-varsusesvars: '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.htmland 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
SyntaxErrorwhen 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:Verification
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 stringsprefer-templaterewrote.toFixedis pinned separately on 14 numeric cases either side of the exponential-notation boundaries, sinceMath.pow→**andArray.join→repeattouch 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 overfile://.CI
A
lintjob runsnpm ci && npm run lint. Thetestjob 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.jsonis now committed fornpm ci.Not fixed here
toFixed(1e-7)returns0.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.