perf: shrink every export's bundle without a breaking change - #594
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
Tree-shaking report✅ No size regression. 185 shrank out of 185 exports.
What changed (185)
Show the other 165
All exports (185)
How this is measuredEvery export is imported alone into an esbuild consumer bundle (minified, tree-shaken) built from the head and from the base of this pull request; the sizes are the resulting bundles, gzip is their gzipped size. 🔴 marks a regression: a pre-existing export that grew more than 20% and more than 256 B, or the bundle importing every pre-existing export growing more than 5%. 🟡 is growth under the threshold, 🟢 a decrease, ⚪ no change, 🆕 an export that does not exist on the base (never a regression), 🗑️ an export that was removed. An intentional increase is accepted with the |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## hyan/confident-babbage-dj7wii #594 +/- ##
===============================================================
Coverage 100.00% 100.00%
===============================================================
Files 221 223 +2
Lines 2290 2303 +13
Branches 695 697 +2
===============================================================
+ Hits 2290 2303 +13
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
4f5f5de to
d0227eb
Compare
d0227eb to
1469056
Compare
1469056 to
2cc91bb
Compare
2cc91bb to
f92e14a
Compare
f92e14a to
eccfd23
Compare
eccfd23 to
ac9ade8
Compare
A consumer bundler keeps a top-level statement of the root bundle unless it can prove it free
of side effects. Seven of them could not be proven so, and were kept in every bundle that
imports anything from the root entry, whatever it imports:
- capitalize: `new Set(STATE_CODES)`, `new Set(TRAILING_DESIGNATIONS)` and
`new Set(COMPANY_DESIGNATIONS)`, plus the `UPPER_CASE_WORDS` array spread (which also kept
the three lists it spreads). The state check now goes through `isStateCode`, the two
designation checks through `Array.prototype.includes`, and the default upper case list is
assembled inside `capitalize`.
- processo-juridico: the `new Map` of tribunals (whose ranges are calls) and
`generateProcessoJuridico`'s `[...map.keys()]`. The map is now built on the first call of
`getProcessoJuridicoTribunals()` and kept.
- is-valid-pix-url: a `new RegExp` built from template pieces is now the equivalent literal
(same `source` and `flags`, checked).
- phone: the `${"00"}${"55"}` template is now the literal "0055".
- is-valid-pix-payload: `PIX_CRC_TAG.length + PIX_CRC_LENGTH` is now the literal constant
`PIX_CRC_FIELD_LENGTH` (8).
Single-import size through the root entry (esbuild, min/gzip), every one of the 188 exports
shrinks by 850-890 B / 460-505 B:
- isValidCpf 1354/804 -> 500/334 (subpath 501/331)
- isValidCep 1013/634 -> 159/160 (subpath 167/165)
- removeAccents 953/594 -> 99/119 (subpath 99/119)
- capitalize 2597/1356 -> 2174/1105
- full import 2275844/438450 -> 2275780/438419
rollup gives the same picture (isValidCpf 985 -> 501 B, removeAccents 593 -> 103 B): a root
import now costs what the subpath import costs.
Behaviour is identical: every Set/Map lookup became a lookup over the same strings with the
same SameValueZero equality, and the literals equal the values they replace. A fast-check
differential test against the previous implementation (capitalize with random text, word lists
and malformed options; isValidProcessoJuridico over every court/tribunal pair and random input;
generateProcessoJuridico's null cases and drawn court/tribunal pairs; isValidPixPayload over
mutated payloads; isValidPixUrl; normalizePhone) found no difference. isValidPixUrl gains a test of
single-character host labels, which kills the mutants the regex literal newly exposes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
isValidCbo looked a code up in CBO_TITLES, the `code -> title` record, so validating a code bundled every title. The CBO table is now generated as two modules: `cbo.ts` holds the 6 digit codes back to back in one string literal (one code per source line, joined by line continuations, so a refresh still diffs code by code), and `cbo-descriptions.ts` holds the titles in an array aligned with them by index. The new `findCodeIndex` internal finds a code on a code boundary of that string; isValidCbo imports the codes only, and getCbo, which already starts with `if (!isValidCbo(value)) return null`, reads the title at the index of the code. Encodings measured for both utils (esbuild, min/gzip, the table alone): keyed record (before) isValid 120780/30456 get 120825/30488 string[] of codes + titles[] isValid 24293/6014 get 126176/30624 newline separated codes + titles isValid 18914/5804 get 120806/30465 packed codes + titles[] (chosen) isValid 16284/5315 get 118159/29964 Single-import size (esbuild, root entry, min/gzip): isValidCbo 121092/30719 -> 16620/5643 getCbo 121168/30756 -> 118520/30227 scripts/cbo.ts now renders both files through `renderCbo` (and only fetches when run as the entry point, so the render can be reused); `scripts/lookup-table.ts` holds the shared split, the packed serialization and the writer. data.ts formats the new file and data-summary.ts reports it; a line continuation is dropped from the summary sample. The committed files were regenerated from the committed table through `renderCbo` itself (the MTE host is not reachable from here), then linted and formatted as data.ts does. Behaviour is identical: the codes and titles are the same 2694 pairs. A fast-check differential test against the previous implementation (every code in every mask, as a number, every one-character neighbour, truncations, 200k random inputs) found no difference. A new test keeps the codes and titles aligned: one title per code, codes of the table width in strictly ascending order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
isValidCnae looked a code up in CNAE_SUBCLASSES, the `code -> description` record, so validating a code bundled every description. As for the CBO table, scripts/cnae.ts now renders two modules through `renderCnae`: `cnae.ts` holds the 1332 7 digit codes packed into one string literal, read by `findCodeIndex`, and `cnae-descriptions.ts` holds the descriptions aligned with them by index. isValidCnae imports the codes only; getCnae, which already returns null unless isValidCnae accepts the value, reads the description at the index of the code. Single-import size (esbuild, root entry, min/gzip): isValidCnae 95275/21055 -> 9795/3506 getCnae 95351/21112 -> 93827/20515 data.ts formats the new file and data-summary.ts reports it. The committed files were regenerated from the committed table through `renderCnae` itself (the IBGE service is not reachable from here), then linted and formatted as data.ts does. Behaviour is identical: the same code/description pairs. A fast-check differential test against the previous implementation (every code in every mask, as a number, every one-character neighbour, truncations, 200k random inputs) found no difference, and the alignment test now covers the CNAE table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
…ions isValidCfop looked a code up in CFOP_TABLE, the `code -> description` record, so validating a code bundled every description. As for the CBO and CNAE tables, scripts/cfop.ts now renders two modules through `renderCfop`: `cfop.ts` holds the 619 4 digit codes packed into one string literal, read by `findCodeIndex`, and `cfop-descriptions.ts` holds the descriptions aligned with them by index. isValidCfop imports the codes only; getCfop, which already returns null unless isValidCfop accepts the value, reads the description at the index of the code. Single-import size (esbuild, root entry, min/gzip): isValidCfop 69686/6494 -> 2862/1456 getCfop 69757/6529 -> 69252/6498 data.ts formats the new file and data-summary.ts reports it. The committed files were regenerated from the committed table through `renderCfop` itself (the CONFAZ site is not reachable from here), then linted and formatted as data.ts does. Behaviour is identical: the same code/description pairs. A fast-check differential test against the previous implementation (every code in every mask, as a number, every one-character neighbour, truncations, 200k random inputs) found no difference, and the alignment test now covers the CFOP table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
isValidNbs looked a code up in NBS_DESCRIPTIONS, the `code -> description` record, so validating a code bundled every description. As for the CBO, CNAE and CFOP tables, scripts/nbs.ts now renders two modules through `renderNbs`: `nbs.ts` holds the 920 9 digit codes packed into one string literal, read by `findCodeIndex`, and `nbs-descriptions.ts` holds the descriptions, now an array aligned with the codes by index. isValidNbs imports the codes only; getNbs, which already returns null unless isValidNbs accepts the value, reads the description at the index of the code. Single-import size (esbuild, root entry, min/gzip): isValidNbs 82872/13509 -> 8694/2334 getNbs 82951/13545 -> 82516/13382 data.ts formats the new file and data-summary.ts reports it. The committed files were regenerated from the committed table through `renderNbs` itself (the MDIC host is not reachable from here), then linted and formatted as data.ts does. Behaviour is identical: the same code/description pairs. A fast-check differential test against the previous implementation (every code in every mask, as a number, every one-character neighbour, truncations, 200k random inputs) found no difference, and the alignment test now covers the NBS table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
The alignment checks of the split lookup tables lived in one file that imported the codes and the descriptions of every table, which runs into the lint cap of 10 imports per file as tables are added. The checks move to `expectAlignedLookupTable` in the test helpers, called from one test file per table next to its codes module (cbo, cnae, cfop, nbs). Same assertions, no size change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
…riptions isValidServiceItem looked a subitem up in SERVICE_ITEM_DESCRIPTIONS, the `code -> description` record, so validating a subitem bundled every description. As for the other lookup tables, scripts/service-items.ts now renders two modules through `renderServiceItems`: `service-items.ts` holds the 200 4 digit subitems packed into one string literal, read by `findCodeIndex`, and `service-item-descriptions.ts` holds the descriptions, now an array aligned with the subitems by index. isValidServiceItem imports the subitems only; getServiceItem, which already returns null unless isValidServiceItem accepts the value, reads the description at the index of the subitem. Single-import size (esbuild, root entry, min/gzip): isValidServiceItem 26848/8474 -> 1197/698 getServiceItem 26985/8543 -> 26745/8601 getServiceItem loses 240 B minified but gains 58 B gzipped: the 200 packed subitems compress a little worse than the 200 quoted keys they replace, while the validator drops 96% of its size. data.ts formats the new file and data-summary.ts reports it. The committed files were regenerated from the committed table through `renderServiceItems` itself, with the workbook name the committed header records (the NFS-e site is not reachable from here), then linted and formatted as data.ts does. Behaviour is identical: the same subitem/description pairs. A fast-check differential test against the previous implementation (every subitem with and without the dot and the leading zero, as a number, every one-character neighbour, truncations, 200k random inputs) found no difference, and the service list gets its alignment test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
isValidCest looked a code up in CEST_TABLE, the `code -> description` record, so validating a code bundled every description. As for the other lookup tables, scripts/cest.ts now renders two modules through `renderCest`: `cest.ts` holds the 1040 7 digit codes packed into one string literal, read by `findCodeIndex`, and `cest-descriptions.ts` holds the descriptions, an array aligned with the codes by index, next to CEST_SEGMENTS (only getCest reads the segments). isValidCest imports the codes only; getCest, which already returns null unless isValidCest accepts the value, reads the description at the index of the code. isValidCest checks the format with an early return rather than `format && lookup`: written as one `&&` expression, the rolldown build inlines the codes literal into that call and keeps the named constant for getCest too, so the root bundle carried the 7 KB string twice and getCest grew to 125604/28895 instead of shrinking. Single-import size (esbuild, root entry, min/gzip): isValidCest 118496/26434 -> 7754/2404 getCest 119747/26875 -> 118325/26776 data.ts formats the new file and data-summary.ts reports it. The committed files were regenerated from the committed tables through `renderCest` itself, with the amendment line the committed header records (the CONFAZ site is not reachable from here), then linted and formatted as data.ts does. Behaviour is identical: the same code/description pairs and segment names. A fast-check differential test against the previous implementation (every code in every mask, as a number, every one-character neighbour, truncations, 200k random inputs) found no difference, and the CEST table gets its alignment test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
getCid10 kept a lookup of its own, the `code -> description` record, because building it on isValidCid10 would have bundled two keyed datasets (the codes of CID10_SUBCATEGORIES and the keys of the description record). scripts/cid10.ts now renders the descriptions through `renderCid10` as an array in ascending code order, which is the order of CID10_SUBCATEGORIES: every category followed by its subcategories. getCid10 starts with `if (!isValidCid10(value)) return null` and reads the description at the index of the code, the descriptions of the categories before it plus the position of its fourth character, so the 14233 code keys are gone from its bundle. Single-import size (esbuild, root entry, min/gzip): getCid10 1054242/149872 -> 1011867/126444 isValidCid10 unchanged (26843/7002) The committed files were regenerated from the committed table through `renderCid10` itself (the DATASUS host is not reachable from here), then linted and formatted as data.ts does; cid10.ts only has its header reworded. Behaviour is identical: before the change, every code of CID10_SUBCATEGORIES had a description and every description a code, so getCid10 returned null exactly when isValidCid10 was false; the description order matches the code order (checked, and kept by a new alignment test). A fast-check differential test against the previous implementation (every code in upper and lower case, with the dot, a hyphen and whitespace, every one-character neighbour, truncations, 200k random inputs) found no difference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
getMunicipalityByCode and getMunicipalities imported the states `DATA` table only to walk the state codes in order, which bundled every state's name, region and IBGE code. They now walk `STATE_CODES`, the list of the same 27 codes in the same order (a test of isStateCode already keeps the two in step), so the table drops out of both bundles. Single-import size (esbuild, root entry, min/gzip): getMunicipalityByCode 159397/50986 -> 157384/50650 getMunicipalities 159298/50918 -> 157285/50580 Behaviour is identical: the same codes are visited in the same order, so the first match and the order getMunicipalities' stable sort keeps for equal names do not change. A differential test against the previous implementation (getMunicipalities with no state, every listed and several unknown states and random input; getMunicipalityByCode for every municipality code as a string, a number and without its first digit, plus 20k random inputs) found no difference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
detectPixKeyType, behind isValidPixKey, getPixKeyInfo, obfuscatePixKey and generatePixPayload,
checked a phone key with `isValidPhone(digits, { accept: ["mobile"] })`, which bundles the
landline and service phone rules isValidPhone can dispatch to even though only the mobile one is
ever accepted here. It now calls `isValidMobilePhone(digits)`.
Why the result is the same for every input: with `accept: ["mobile"]` isValidPhone skips the
service and landline branches and returns `isValidMobilePhone(value, options)` when the
normalized value has 11 digits, false otherwise. isValidMobilePhone itself returns false for
anything whose normalized form is not 11 digits long, and the only option passed, `accept`, is
not one it reads (its `version` stays undefined, the default rule, in both calls).
Single-import size (esbuild, root entry, min/gzip):
isValidPixKey 3563/1567 -> 2460/1157
getPixKeyInfo 3716/1646 -> 2613/1227
obfuscatePixKey 6047/2517 -> 5645/2386
generatePixPayload 5778/2517 -> 4651/2083
A fast-check differential test against the previous implementation (detectPixKeyType and the
four exports over phone-like strings with and without the country code, e-mails, EVPs, CPF/CNPJ
shapes and arbitrary values, 100k runs each) found no difference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
…kAccount
isListedBankCode scanned COMPE_CODES with `if (COMPE_CODES.startsWith(bankCode, i))`. rolldown's
default constant inlining ("smart" mode) copies an imported constant into the test of an `if`,
a ternary or a logical expression, so the build wrote the whole 1.2 KB literal into that
condition while keeping the named constant for `COMPE_CODES.length`: the published bundle, and
every consumer bundle of isValidBankAccount, carried it twice. The scan now goes through
`findCodeIndex(COMPE_CODES, bankCode) !== -1`, returned rather than tested, so the constant is
read once, through its name.
Single-import size (esbuild, root entry, min/gzip):
isValidBankAccount 6737/2425 -> 5388/2412
Behaviour is identical: findCodeIndex runs the same loop, a `startsWith` at every multiple of
the code length, which is 3 here as before (the bank code is checked to have 3 digits first). A
differential test against the previous implementation (every 3 digit bank code with each check
digit, plus 300k random parameter sets and arbitrary values) found no difference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
The mod10 internal took a `variant` option and carried both rules: Luhn (weights 2 and 1, the digits of each product added) for the boleto, credit card, bank account and IE checks, and GS1 (weights 3 and 1, products added as they are) for isValidGtin alone. Every user of one rule bundled the other. mod10 is now the Luhn rule only, and `gs1CheckDigit` holds the GS1 one; isValidGtin calls it directly. The mod10 tests of the GS1 variant move to gs1CheckDigit. Single-import size (esbuild, root entry, min/gzip): isValidGtin 472/347 -> 408/303 getGtinInfo 861/588 -> 797/545 isValidCreditCard 596/421 -> 559/392 isValidBoleto 1619/872 -> 1582/858 getBoletoInfo 2350/1214 -> 2313/1201 generateBoleto 1337/734 -> 1300/719 isValidBankAccount 5388/2413 -> 5351/2395 isValidIe 4987/1737 -> 4950/1704 Behaviour is identical: both functions keep the loop and the final `remainder > 0 ? 10 - remainder : 0` of the variant they come from, so they agree with the previous mod10 for any string, digits or not. A fast-check differential test against the previous implementation (500k strings for both rules, 200k isValidGtin inputs and options) found no difference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
…scriptions isValidLegalNature checked `Object.hasOwn(LEGAL_NATURE, code)`, so validating a code bundled the description of every legal nature. scripts/legal-natures.ts now also renders, through `renderLegalNatures`, LEGAL_NATURE_CODES: the 4 digit code of every entry of LEGAL_NATURE, official and legacy, packed into one string that `findCodeIndex` reads. isValidLegalNature checks the normalized code has 4 characters and is listed there. getLegalNature, which reads LEGAL_NATURE for the description anyway, now makes that same `Object.hasOwn` check itself instead of calling isValidLegalNature, so it does not bundle the code list on top of the table. The other legal nature utils keep reading LEGAL_NATURE as they did. Single-import size (esbuild, root entry, min/gzip): isValidLegalNature 5062/1620 -> 649/427 getLegalNature 5784/1897 -> 5697/1873 getLegalNatures, getLegalNaturesByCategory and generateLegalNature unchanged (+/-4 B of identifier renaming) Behaviour is identical: a code of LEGAL_NATURE is 4 characters long, so after the length check a match on a 4 character boundary of the packed list is a match of a key, and a prototype key such as "constructor" fails the length check as it failed `Object.hasOwn`. getLegalNature runs the exact expression isValidLegalNature ran (its value is a string or a safe integer by then). A new test keeps LEGAL_NATURE_CODES equal to the keys of LEGAL_NATURE; a differential test against the previous implementation (every number 0-99999 as a number and padded or not, every code masked, truncated and extended, prototype keys, 200k random inputs) found no difference. The committed file was regenerated from the committed table through `renderLegalNatures` itself (the CONCLA site is not reachable from here), then linted and formatted as data.ts does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
NCM_CODES was an array of 10.5k quoted 8 digit strings. scripts/ncm.ts now renders it, through
`renderNcm`, with the `serializeCodes` form the other code tables use: the codes back to back in
one string literal, one per source line joined by line continuations, so a refresh still diffs
code by code. isValidNcm keeps its lazily built Set, now filled from the codes the string holds
(`/\d{8}/g`), so a lookup stays a Set lookup after the first call.
Single-import size (esbuild, root entry, min/gzip):
isValidNcm 116070/24486 -> 84546/23343
Behaviour is identical: the string holds the same codes in the same order (checked: splitting it
gives the previous array), so the Set is the same. A new test keeps the string made of 8 digit
codes, unique and ascending; a differential test against the previous implementation (every code
plain, as a number, masked, truncated and extended, 140k numbers, 200k random inputs) found no
difference. The committed file was regenerated from the committed list through `renderNcm`
itself (the Siscomex portal is not reachable from here), then linted and formatted as data.ts
does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
The bundle size table of the getting started page (English and Portuguese) is refreshed from `node scripts/tree-shaking.ts --json`: a root import of isValidCpf is now about 0.5 KB minified (0.3 KB gzipped) instead of 1.4 KB (0.8 KB), in the README too. The lookup validators no longer share a row with their getters, since they now bundle the codes alone: isValidCbo, isValidCnae, isValidNbs, isValidCest and isValidCid10 get rows of their own, and isValidCfop (2.8 KB) and isValidServiceItem (1.2 KB) leave the table of heavy utils. The getCities, isValidCid10 and getCid10 notes of the utilities page get their new sizes as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH
ac9ade8 to
c5b1b7d
Compare
What does this PR do?
A function by function pass over how the utils import each other, applying every separation or inversion that shrinks what a consumer bundles, without a breaking change: same exports, names, signatures, types, subpaths and results for every input (
check:api: no breaking change against 2.4.0).Every one of the 185 exports got smaller (measured against this PR's base, #593, with
scripts/tree-shaking.ts: each export imported alone through the root entry, esbuild, minified). Full import: 2,035,135 → 1,926,362 B, gzip 387,753 → 354,476 B.isValidCestisValidCboisValidCnaeisValidNbsisValidCfopisValidServiceItemisValidLegalNatureisValidNcmgetCid10getMunicipalitiesisValidPixPayloadisValidBankAccountformatPhoneisValidCpfHow
One commit per change, each with its own before/after in the message:
perf(root): every root import carried ~850 B of module-level statements a bundler cannot drop (new Set/new Mapincapitalizeand the processo jurídico tables, a spread array,new RegExp(template), a template literal, a computed length). They are now literals or built inside the function. A root import ofisValidCpforisValidCepis now exactly its subpath size, with esbuild and with rollup.scripts/*.ts,scripts/data.tsandscripts/data-summary.tslist the new files).isValidXimports only the codes;getXisisValidXplus one index lookup. The packed string is written one code per line, so a dataset refresh still diffs line by line. Tests pin that codes and descriptions stay aligned and sorted.getCid10looks the description up by index, dropping the codes of its keyed table (−24 KB gzip). NCM codes ship as one packed string.getMunicipalities/getMunicipalityByCodeiterate the state codes instead of importing the states table (−3 KB each).isValidMobilePhone(normalizePhone(x))instead ofisValidPhone(x, { accept: ["mobile"] }), same semantics, without the landline and service phone code.isValidBankAccount: rolldown inlined the 1.4 KBCOMPE_CODESstring twice; the lookup no longer sits in a condition. (Rolldown copies an imported constant into anif/&&/ternary test; every new lookup here returns the value or returns early for that reason.)mod10: the GS1 variant is its own helper, used only by GTIN.Checklist
npm test).docs/getting-started.mdanddocs/pt-br/getting-started.md(no utility's behaviour changes).npm run checkpasses locally (format, lint, types).npm run build:llmsruns clean.Additional context
Rebased again (2026-09-27) on refactor: getters build on their validators, reject signed and fractional numbers, spell out truncated names #593's second official-source pass. The CID-10 commit here was redone on top of refactor: getters build on their validators, reject signed and fractional numbers, spell out truncated names #593's new SIM
U07codes:renderCid10;U07entries and their header notes.The getting-started table was re-measured (
getCid10988.3 KB / 123.6 KB gzip, the bank getters 37.6–37.8 KB). Every other file of this PR has the same delta as before the rebase. The comparison table above predates the fourU07entries (+167 B ongetCid10).Earlier: feat: add obfuscate to formatPhone, formatPis, formatCnh and formatVoterId #567 dropped
obfuscateEmailandobfuscatePixKey, and theperf(pix)commit that only shrankobfuscatePixKeywent with them.Behaviour: each changed util was run against its previous implementation with fast-check (every code of every dataset in every mask, as a number, with every one-character change; every legal nature number 0 to 99999; every bank code; ~200k phone keys; 20k to 500k random inputs each), with no difference. Those differential runs are local; the permanent tests are the alignment and shape tests above.
Generated files: every data file is produced by its generator's render function; re-running it reproduces every file byte for byte, and the weekly
Update datasetsrun uses the same functions.Where the "getter builds on validator" rule yields to size:
getLegalNaturekeeps its ownObject.hasOwncheck on the table it already reads;getClassTribandgetCstIbsCbsstay as they are because the split would make the getter grow (+53 B and +54 B).Verification (on the rebased head):
npm run check;npx vp test run --coverage7680 passed, 100% on every metric; knip; jscpd 0 clones;npm run build;check:apino breaking change;npx commitlint --from origin/main --to HEAD0 problems.🤖 Generated with Claude Code
https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH