Skip to content

perf: shrink every export's bundle without a breaking change - #594

Merged
hyanmandian merged 21 commits into
hyan/confident-babbage-dj7wiifrom
claude/bundle-size
Sep 29, 2026
Merged

hyanmandian merged 21 commits into
hyan/confident-babbage-dj7wiifrom
claude/bundle-size

Conversation

@hyanmandian

@hyanmandian hyanmandian commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Stacked on #593, the tip of the stack. Its base branch is hyan/confident-babbage-dj7wii, so the diff shown here is this PR alone.

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.

Export Before (min / gzip) After (min / gzip)
isValidCest 119,470 / 27,094 7,750 / 2,400
isValidCbo 122,066 / 31,392 16,616 / 5,637
isValidCnae 96,249 / 21,690 9,791 / 3,501
isValidNbs 83,846 / 14,164 8,690 / 2,328
isValidCfop 70,660 / 7,105 2,858 / 1,451
isValidServiceItem 27,822 / 9,088 1,193 / 694
isValidLegalNature 6,040 / 2,168 649 / 427
isValidNcm 117,044 / 25,239 84,542 / 23,347
getCid10 1,055,220 / 150,419 1,011,867 / 126,443
getMunicipalities 160,276 / 51,553 157,285 / 50,580
isValidPixPayload 6,229 / 2,788 4,256 / 1,917
isValidBankAccount 7,727 / 2,965 5,351 / 2,394
formatPhone 3,651 / 1,715 2,691 / 1,222
isValidCpf 1,478 / 836 500 / 334
every other export −425 B or more, most −835 to −890 B

How

One commit per change, each with its own before/after in the message:

  1. perf(root): every root import carried ~850 B of module-level statements a bundler cannot drop (new Set/new Map in capitalize and 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 of isValidCpf or isValidCep is now exactly its subpath size, with esbuild and with rollup.
  2. Lookup validators without the descriptions (CBO, CNAE, CFOP, NBS, service list, CEST, legal natures): each dataset is split into a sorted, packed codes string and a descriptions array aligned by index, both emitted by the dataset's generator (scripts/*.ts, scripts/data.ts and scripts/data-summary.ts list the new files). isValidX imports only the codes; getX is isValidX plus 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.
  3. getCid10 looks the description up by index, dropping the codes of its keyed table (−24 KB gzip). NCM codes ship as one packed string.
  4. getMunicipalities / getMunicipalityByCode iterate the state codes instead of importing the states table (−3 KB each).
  5. Pix: the key type detection calls isValidMobilePhone(normalizePhone(x)) instead of isValidPhone(x, { accept: ["mobile"] }), same semantics, without the landline and service phone code.
  6. isValidBankAccount: rolldown inlined the 1.4 KB COMPE_CODES string twice; the lookup no longer sits in a condition. (Rolldown copies an imported constant into an if/&&/ternary test; every new lookup here returns the value or returns early for that reason.)
  7. mod10: the GS1 variant is its own helper, used only by GTIN.

Checklist

  • My commit/PR title follows Conventional Commits.
  • I added or updated tests covering this change (npm test).
  • I updated the documentation if this adds/changes a utility, in both: the bundle-size tables of docs/getting-started.md and docs/pt-br/getting-started.md (no utility's behaviour changes).
  • npm run check passes locally (format, lint, types).
  • npm run build:llms runs clean.
  • This change does not introduce a breaking change.
  • This change does not add any runtime dependency.

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 U07 codes:

    The getting-started table was re-measured (getCid10 988.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 four U07 entries (+167 B on getCid10).

  • Earlier: feat: add obfuscate to formatPhone, formatPis, formatCnh and formatVoterId #567 dropped obfuscateEmail and obfuscatePixKey, and the perf(pix) commit that only shrank obfuscatePixKey went 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 datasets run uses the same functions.

  • Where the "getter builds on validator" rule yields to size: getLegalNature keeps its own Object.hasOwn check on the table it already reads; getClassTrib and getCstIbsCbs stay 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 --coverage 7680 passed, 100% on every metric; knip; jscpd 0 clones; npm run build; check:api no breaking change; npx commitlint --from origin/main --to HEAD 0 problems.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RLkm9YrtAifc6XCLFVEsdH

@vercel

vercel Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
brazilian-utils Ready Ready Preview Sep 29, 2026 3:16am UTC

@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d825f1f9-c61f-4161-a552-6684f81862fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@brazilian-utils/brazilian-utils@594

commit: 806bd7f

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Tree-shaking report

✅ No size regression. 185 shrank out of 185 exports.

Base Head Δ
Pre-existing exports, all imported 1989.1 KB 1882.9 KB (gzip 346.8 KB) -106.2 KB (-5.3%)
Full import 1989.1 KB 1882.9 KB (gzip 346.8 KB) -106.2 KB (-5.3%)
Exports 185 185 0

What changed (185)

Export Base Head Δ gzip
🟢 isValidCest 116.7 KB 7.6 KB -109.1 KB (-93.5%) 2.3 KB
🟢 isValidCbo 119.2 KB 16.2 KB -103.0 KB (-86.4%) 5.5 KB
🟢 isValidCnae 94.0 KB 9.6 KB -84.4 KB (-89.8%) 3.4 KB
🟢 isValidNbs 81.9 KB 8.5 KB -73.4 KB (-89.6%) 2.3 KB
🟢 isValidCfop 69.0 KB 2.8 KB -66.2 KB (-96.0%) 1.4 KB
🟢 getCid10 1030.7 KB 988.3 KB -42.3 KB (-4.1%) 123.6 KB
🟢 isValidNcm 114.3 KB 82.6 KB -31.7 KB (-27.8%) 22.8 KB
🟢 isValidServiceItem 27.2 KB 1.2 KB -26.0 KB (-95.7%) 694 B
🟢 isValidLegalNature 5.9 KB 649 B -5.3 KB (-89.3%) 427 B
🟢 getCbo 119.3 KB 115.7 KB -3.5 KB (-3.0%) 29.5 KB
🟢 getMunicipalities 156.5 KB 153.6 KB -2.9 KB (-1.9%) 49.4 KB
🟢 getMunicipalityByCode 156.6 KB 153.7 KB -2.9 KB (-1.9%) 49.5 KB
🟢 getCnae 94.1 KB 91.6 KB -2.4 KB (-2.6%) 20.0 KB
🟢 getCest 117.9 KB 115.5 KB -2.3 KB (-2.0%) 26.1 KB
🟢 isValidBankAccount 7.5 KB 5.2 KB -2.3 KB (-30.7%) 2.3 KB
🟢 getPixKeyInfo 4.6 KB 2.5 KB -2.0 KB (-44.6%) 1.2 KB
🟢 isValidPixKey 4.4 KB 2.3 KB -2.0 KB (-46.5%) 1.1 KB
🟢 getPixPayloadInfo 6.6 KB 4.6 KB -1.9 KB (-29.4%) 2.1 KB
🟢 isValidPixPayload 6.1 KB 4.2 KB -1.9 KB (-31.8%) 1.9 KB
🟢 generatePixPayload 6.5 KB 4.5 KB -1.9 KB (-29.7%) 2.0 KB
Show the other 165
Export Base Head Δ gzip
🟢 getCfop 69.1 KB 67.6 KB -1.4 KB (-2.1%) 6.3 KB
🟢 getNbs 82.0 KB 80.6 KB -1.4 KB (-1.7%) 13.1 KB
🟢 getServiceItem 27.3 KB 26.1 KB -1.2 KB (-4.4%) 8.4 KB
🟢 getLegalNature 6.6 KB 5.6 KB -1.0 KB (-15.7%) 1.8 KB
🟢 isValidIe 6.2 KB 5.1 KB -1.0 KB (-16.6%) 1.8 KB
🟢 isValidIE 6.2 KB 5.1 KB -1.0 KB (-16.6%) 1.8 KB
🟢 getGtinInfo 1.8 KB 817 B -1.0 KB (-56.1%) 555 B
🟢 isValidGtin 1.4 KB 428 B -1.0 KB (-70.9%) 313 B
🟢 generateBoleto 2.4 KB 1.4 KB -1015 B (-41.6%) 779 B
🟢 getBoletoInfo 3.4 KB 2.4 KB -1015 B (-29.2%) 1.2 KB
🟢 isValidBoleto 2.6 KB 1.6 KB -1015 B (-38.0%) 892 B
🟢 isValidCreditCard 1.5 KB 555 B -1015 B (-64.6%) 389 B
🟢 differenceInBusinessDays 9.3 KB 8.4 KB -1010 B (-10.6%) 3.0 KB
🟢 isBusinessDay 8.8 KB 7.8 KB -1010 B (-11.2%) 2.8 KB
🟢 addBusinessDays 9.5 KB 8.5 KB -1006 B (-10.3%) 3.0 KB
🟢 isHoliday 8.7 KB 7.7 KB -1006 B (-11.4%) 2.7 KB
🟢 subBusinessDays 9.6 KB 8.6 KB -1006 B (-10.3%) 3.1 KB
🟢 getHolidays 8.3 KB 7.4 KB -1002 B (-11.7%) 2.6 KB
🟢 convertDateToWords 3.4 KB 2.4 KB -979 B (-28.4%) 1.2 KB
🟢 getNfseKeyInfo 3.4 KB 2.4 KB -979 B (-28.4%) 1.2 KB
🟢 convertCurrencyToWords 2.9 KB 2.0 KB -978 B (-32.6%) 1.0 KB
🟢 convertLicensePlateToMercosul 1.5 KB 527 B -978 B (-65.0%) 363 B
🟢 convertNumberToWords 2.5 KB 1.6 KB -978 B (-37.7%) 819 B
🟢 formatBoleto 1.6 KB 649 B -978 B (-60.1%) 402 B
🟢 formatCaepf 1.4 KB 487 B -978 B (-66.8%) 344 B
🟢 formatCei 1.4 KB 484 B -978 B (-66.9%) 343 B
🟢 formatCep 1.4 KB 470 B -978 B (-67.5%) 334 B
🟢 formatCEP 1.4 KB 470 B -978 B (-67.5%) 334 B
🟢 formatCertidao 1.5 KB 513 B -978 B (-65.6%) 350 B
🟢 formatCest 1.4 KB 474 B -978 B (-67.4%) 337 B
🟢 formatCid10 1.4 KB 459 B -978 B (-68.1%) 330 B
🟢 formatCnae 1.4 KB 474 B -978 B (-67.4%) 340 B
🟢 formatCnh 1.5 KB 517 B -978 B (-65.4%) 361 B
🟢 formatCno 1.4 KB 488 B -978 B (-66.7%) 344 B
🟢 formatCnpj 1.6 KB 620 B -978 B (-61.2%) 420 B
🟢 formatCNPJ 1.6 KB 620 B -978 B (-61.2%) 420 B
🟢 formatCns 1.4 KB 483 B -978 B (-66.9%) 340 B
🟢 formatCpf 1.5 KB 521 B -978 B (-65.2%) 365 B
🟢 formatCPF 1.5 KB 521 B -978 B (-65.2%) 365 B
🟢 formatCurrency 1.9 KB 985 B -978 B (-49.8%) 592 B
🟢 formatIban 1.2 KB 267 B -978 B (-78.6%) 228 B
🟢 formatLegalNature 1.4 KB 470 B -978 B (-67.5%) 337 B
🟢 formatLicensePlate 1.3 KB 361 B -978 B (-73.0%) 270 B
🟢 formatNbs 1.4 KB 462 B -978 B (-67.9%) 334 B
🟢 formatNcm 1.4 KB 475 B -978 B (-67.3%) 338 B
🟢 formatNfeKey 1.5 KB 601 B -978 B (-61.9%) 383 B
🟢 formatPassport 1.1 KB 187 B -978 B (-83.9%) 174 B
🟢 formatPis 1.5 KB 521 B -978 B (-65.2%) 365 B
🟢 formatProcessoJuridico 1.4 KB 490 B -978 B (-66.6%) 344 B
🟢 formatSuframa 1.4 KB 476 B -978 B (-67.3%) 338 B
🟢 formatVoterId 1.5 KB 523 B -978 B (-65.2%) 365 B
🟢 generateCep 1.1 KB 130 B -978 B (-88.3%) 136 B
🟢 generateCnh 1.5 KB 565 B -978 B (-63.4%) 343 B
🟢 generateCnpj 1.8 KB 843 B -978 B (-53.7%) 523 B
🟢 generateCNPJ 1.8 KB 843 B -978 B (-53.7%) 523 B
🟢 generateCpf 1.6 KB 628 B -978 B (-60.9%) 423 B
🟢 generateCPF 1.6 KB 628 B -978 B (-60.9%) 423 B
🟢 generateLicensePlate 1.2 KB 266 B -978 B (-78.6%) 227 B
🟢 generatePassport 1.2 KB 226 B -978 B (-81.2%) 191 B
🟢 generatePis 1.3 KB 357 B -978 B (-73.3%) 272 B
🟢 generateRenavam 1.3 KB 378 B -978 B (-72.1%) 284 B
🟢 generateSuframa 1.4 KB 451 B -978 B (-68.4%) 335 B
🟢 generateVoterId 1.8 KB 852 B -978 B (-53.4%) 551 B
🟢 getAddressInfoByCep 4.3 KB 3.3 KB -978 B (-22.4%) 1.4 KB
🟢 GetAddressInfoByCepError 1.1 KB 112 B -978 B (-89.7%) 124 B
🟢 GetAddressInfoByCepNotFoundError 1.2 KB 205 B -978 B (-82.7%) 145 B
🟢 GetAddressInfoByCepServiceError 1.2 KB 204 B -978 B (-82.7%) 143 B
🟢 GetAddressInfoByCepValidationError 1.2 KB 203 B -978 B (-82.8%) 144 B
🟢 getAreaCodeInfo 4.0 KB 3.1 KB -978 B (-23.8%) 964 B
🟢 getAreaCodesByState 1.7 KB 809 B -978 B (-54.7%) 459 B
🟢 getBankByCode 38.8 KB 37.8 KB -978 B (-2.5%) 9.2 KB
🟢 getBankByIspb 38.8 KB 37.8 KB -978 B (-2.5%) 9.2 KB
🟢 getBanks 38.5 KB 37.6 KB -978 B (-2.5%) 9.0 KB
🟢 GetCepInfoByAddressError 1.1 KB 112 B -978 B (-89.7%) 124 B
🟢 GetCepInfoByAddressNotFoundError 1.2 KB 205 B -978 B (-82.7%) 145 B
🟢 GetCepInfoByAddressValidationError 1.2 KB 203 B -978 B (-82.8%) 144 B
🟢 getCertidaoInfo 2.0 KB 1.0 KB -978 B (-48.6%) 594 B
🟢 getCities 154.4 KB 153.4 KB -978 B (-0.6%) 49.2 KB
🟢 getClassTrib 50.9 KB 50.0 KB -978 B (-1.9%) 9.0 KB
🟢 getCnpjInfo 1.9 KB 964 B -978 B (-50.4%) 542 B
🟢 getCstIbsCbs 1.9 KB 967 B -978 B (-50.3%) 534 B
🟢 getFormatLicensePlate 1.3 KB 315 B -978 B (-75.6%) 245 B
🟢 getIbanInfo 1.7 KB 752 B -978 B (-56.5%) 494 B
🟢 getLegalNaturesByCategory 6.6 KB 5.6 KB -978 B (-14.5%) 1.9 KB
🟢 getMunicipality 155.0 KB 154.0 KB -978 B (-0.6%) 49.7 KB
🟢 getNfeKeyInfo 3.2 KB 2.2 KB -978 B (-29.9%) 1.2 KB
🟢 getStateByCep 4.6 KB 3.6 KB -978 B (-20.9%) 1.0 KB
🟢 getStateByIbgeCode 3.3 KB 2.4 KB -978 B (-28.8%) 671 B
🟢 getStateCodeByName 3.3 KB 2.3 KB -978 B (-29.2%) 649 B
🟢 getStateNameByCode 3.2 KB 2.2 KB -978 B (-29.9%) 591 B
🟢 getStates 3.1 KB 2.2 KB -978 B (-30.6%) 539 B
🟢 getTimezoneByState 1.7 KB 737 B -978 B (-57.0%) 327 B
🟢 isValidCaepf 1.7 KB 782 B -978 B (-55.6%) 479 B
🟢 isValidCei 1.7 KB 714 B -978 B (-57.8%) 472 B
🟢 isValidCep 1.1 KB 163 B -978 B (-85.7%) 163 B
🟢 isValidCEP 1.1 KB 163 B -978 B (-85.7%) 163 B
🟢 isValidCertidao 1.7 KB 758 B -978 B (-56.3%) 462 B
🟢 isValidCid10 27.2 KB 26.2 KB -978 B (-3.5%) 6.8 KB
🟢 isValidClassTrib 2.8 KB 1.8 KB -978 B (-34.7%) 639 B
🟢 isValidCnh 1.6 KB 610 B -978 B (-61.6%) 370 B
🟢 isValidCno 1.7 KB 718 B -978 B (-57.7%) 474 B
🟢 isValidCnpj 1.7 KB 796 B -978 B (-55.1%) 447 B
🟢 isValidCNPJ 1.7 KB 796 B -978 B (-55.1%) 447 B
🟢 isValidCns 1.7 KB 746 B -978 B (-56.7%) 475 B
🟢 isValidCpf 1.4 KB 500 B -978 B (-66.2%) 334 B
🟢 isValidCPF 1.4 KB 500 B -978 B (-66.2%) 334 B
🟢 isValidCsosn 1.3 KB 330 B -978 B (-74.8%) 262 B
🟢 isValidCst 1.9 KB 995 B -978 B (-49.6%) 569 B
🟢 isValidCstIbsCbs 1.8 KB 894 B -978 B (-52.2%) 505 B
🟢 isValidEmail 1.1 KB 171 B -978 B (-85.1%) 157 B
🟢 isValidIban 1.5 KB 531 B -978 B (-64.8%) 376 B
🟢 isValidLicensePlate 1.2 KB 267 B -978 B (-78.6%) 216 B
🟢 isValidNfeKey 2.6 KB 1.6 KB -978 B (-37.1%) 954 B
🟢 isValidNfseKey 3.0 KB 2.1 KB -978 B (-31.7%) 1.1 KB
🟢 isValidPassport 1.1 KB 197 B -978 B (-83.2%) 186 B
🟢 isValidPis 1.4 KB 418 B -978 B (-70.1%) 316 B
🟢 isValidPIS 1.4 KB 418 B -978 B (-70.1%) 316 B
🟢 isValidRenavam 1.5 KB 516 B -978 B (-65.5%) 356 B
🟢 isValidSuframa 1.5 KB 590 B -978 B (-62.4%) 411 B
🟢 isValidVin 1.8 KB 848 B -978 B (-53.6%) 573 B
🟢 isValidVoterId 1.6 KB 706 B -978 B (-58.1%) 427 B
🟢 parseBoleto 1.2 KB 247 B -978 B (-79.8%) 215 B
🟢 parseCaepf 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCbo 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseCei 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCep 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseCertidao 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCest 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseCfop 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseCid10 1.1 KB 177 B -978 B (-84.7%) 170 B
🟢 parseCnae 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseCnh 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCno 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCnpj 1.2 KB 298 B -978 B (-76.6%) 240 B
🟢 parseCns 1.1 KB 199 B -978 B (-83.1%) 184 B
🟢 parseCpf 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCurrency 1.6 KB 610 B -978 B (-61.6%) 409 B
🟢 parseIban 1.2 KB 227 B -978 B (-81.2%) 202 B
🟢 parseLegalNature 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseLicensePlate 1.1 KB 177 B -978 B (-84.7%) 170 B
🟢 parseNcm 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseNfeKey 1.4 KB 412 B -978 B (-70.4%) 312 B
🟢 parseNfseKey 1.2 KB 253 B -978 B (-79.4%) 219 B
🟢 parsePassport 1.1 KB 177 B -978 B (-84.7%) 169 B
🟢 parsePis 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseProcessoJuridico 1.1 KB 199 B -978 B (-83.1%) 182 B
🟢 parseSuframa 1.1 KB 198 B -978 B (-83.2%) 181 B
🟢 parseVoterId 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 removeAccents 1.1 KB 99 B -978 B (-90.8%) 119 B
🟢 toStandardSchema 1.2 KB 284 B -978 B (-77.5%) 233 B
🟢 generateLegalNature 6.0 KB 5.0 KB -974 B (-15.9%) 1.6 KB
🟢 generatePhone 1.6 KB 670 B -974 B (-59.2%) 419 B
🟢 getLegalNatures 6.0 KB 5.1 KB -974 B (-15.8%) 1.6 KB
🟢 isValidServicePhone 1.6 KB 706 B -974 B (-58.0%) 372 B
🟢 formatPhone 3.6 KB 2.6 KB -960 B (-26.3%) 1.2 KB
🟢 isValidLandlinePhone 1.6 KB 717 B -958 B (-57.2%) 463 B
🟢 isValidMobilePhone 1.7 KB 758 B -958 B (-55.8%) 496 B
🟢 isValidPhone 2.7 KB 1.8 KB -958 B (-34.2%) 909 B
🟢 parsePhone 1.3 KB 370 B -958 B (-72.1%) 281 B
🟢 isValidRegistroProfissional 2.0 KB 1.2 KB -839 B (-41.1%) 724 B
🟢 getCepInfoByAddress 2.8 KB 2.0 KB -835 B (-28.9%) 1.0 KB
🟢 getCpfInfo 1.8 KB 994 B -835 B (-45.7%) 614 B
🟢 isValidProcessoJuridico 1.4 KB 699 B -765 B (-52.3%) 429 B
🟢 generateProcessoJuridico 1.5 KB 821 B -745 B (-47.6%) 523 B
🟢 capitalize 2.8 KB 2.4 KB -425 B (-14.9%) 1.2 KB
All exports (185)
Export Base Head Δ gzip
🟢 GetAddressInfoByCepError 1.1 KB 112 B -978 B (-89.7%) 124 B
🟢 GetAddressInfoByCepNotFoundError 1.2 KB 205 B -978 B (-82.7%) 145 B
🟢 GetAddressInfoByCepServiceError 1.2 KB 204 B -978 B (-82.7%) 143 B
🟢 GetAddressInfoByCepValidationError 1.2 KB 203 B -978 B (-82.8%) 144 B
🟢 GetCepInfoByAddressError 1.1 KB 112 B -978 B (-89.7%) 124 B
🟢 GetCepInfoByAddressNotFoundError 1.2 KB 205 B -978 B (-82.7%) 145 B
🟢 GetCepInfoByAddressValidationError 1.2 KB 203 B -978 B (-82.8%) 144 B
🟢 addBusinessDays 9.5 KB 8.5 KB -1006 B (-10.3%) 3.0 KB
🟢 capitalize 2.8 KB 2.4 KB -425 B (-14.9%) 1.2 KB
🟢 convertCurrencyToWords 2.9 KB 2.0 KB -978 B (-32.6%) 1.0 KB
🟢 convertDateToWords 3.4 KB 2.4 KB -979 B (-28.4%) 1.2 KB
🟢 convertLicensePlateToMercosul 1.5 KB 527 B -978 B (-65.0%) 363 B
🟢 convertNumberToWords 2.5 KB 1.6 KB -978 B (-37.7%) 819 B
🟢 differenceInBusinessDays 9.3 KB 8.4 KB -1010 B (-10.6%) 3.0 KB
🟢 formatBoleto 1.6 KB 649 B -978 B (-60.1%) 402 B
🟢 formatCEP 1.4 KB 470 B -978 B (-67.5%) 334 B
🟢 formatCNPJ 1.6 KB 620 B -978 B (-61.2%) 420 B
🟢 formatCPF 1.5 KB 521 B -978 B (-65.2%) 365 B
🟢 formatCaepf 1.4 KB 487 B -978 B (-66.8%) 344 B
🟢 formatCei 1.4 KB 484 B -978 B (-66.9%) 343 B
🟢 formatCep 1.4 KB 470 B -978 B (-67.5%) 334 B
🟢 formatCertidao 1.5 KB 513 B -978 B (-65.6%) 350 B
🟢 formatCest 1.4 KB 474 B -978 B (-67.4%) 337 B
🟢 formatCid10 1.4 KB 459 B -978 B (-68.1%) 330 B
🟢 formatCnae 1.4 KB 474 B -978 B (-67.4%) 340 B
🟢 formatCnh 1.5 KB 517 B -978 B (-65.4%) 361 B
🟢 formatCno 1.4 KB 488 B -978 B (-66.7%) 344 B
🟢 formatCnpj 1.6 KB 620 B -978 B (-61.2%) 420 B
🟢 formatCns 1.4 KB 483 B -978 B (-66.9%) 340 B
🟢 formatCpf 1.5 KB 521 B -978 B (-65.2%) 365 B
🟢 formatCurrency 1.9 KB 985 B -978 B (-49.8%) 592 B
🟢 formatIban 1.2 KB 267 B -978 B (-78.6%) 228 B
🟢 formatLegalNature 1.4 KB 470 B -978 B (-67.5%) 337 B
🟢 formatLicensePlate 1.3 KB 361 B -978 B (-73.0%) 270 B
🟢 formatNbs 1.4 KB 462 B -978 B (-67.9%) 334 B
🟢 formatNcm 1.4 KB 475 B -978 B (-67.3%) 338 B
🟢 formatNfeKey 1.5 KB 601 B -978 B (-61.9%) 383 B
🟢 formatPassport 1.1 KB 187 B -978 B (-83.9%) 174 B
🟢 formatPhone 3.6 KB 2.6 KB -960 B (-26.3%) 1.2 KB
🟢 formatPis 1.5 KB 521 B -978 B (-65.2%) 365 B
🟢 formatProcessoJuridico 1.4 KB 490 B -978 B (-66.6%) 344 B
🟢 formatSuframa 1.4 KB 476 B -978 B (-67.3%) 338 B
🟢 formatVoterId 1.5 KB 523 B -978 B (-65.2%) 365 B
🟢 generateBoleto 2.4 KB 1.4 KB -1015 B (-41.6%) 779 B
🟢 generateCNPJ 1.8 KB 843 B -978 B (-53.7%) 523 B
🟢 generateCPF 1.6 KB 628 B -978 B (-60.9%) 423 B
🟢 generateCep 1.1 KB 130 B -978 B (-88.3%) 136 B
🟢 generateCnh 1.5 KB 565 B -978 B (-63.4%) 343 B
🟢 generateCnpj 1.8 KB 843 B -978 B (-53.7%) 523 B
🟢 generateCpf 1.6 KB 628 B -978 B (-60.9%) 423 B
🟢 generateLegalNature 6.0 KB 5.0 KB -974 B (-15.9%) 1.6 KB
🟢 generateLicensePlate 1.2 KB 266 B -978 B (-78.6%) 227 B
🟢 generatePassport 1.2 KB 226 B -978 B (-81.2%) 191 B
🟢 generatePhone 1.6 KB 670 B -974 B (-59.2%) 419 B
🟢 generatePis 1.3 KB 357 B -978 B (-73.3%) 272 B
🟢 generatePixPayload 6.5 KB 4.5 KB -1.9 KB (-29.7%) 2.0 KB
🟢 generateProcessoJuridico 1.5 KB 821 B -745 B (-47.6%) 523 B
🟢 generateRenavam 1.3 KB 378 B -978 B (-72.1%) 284 B
🟢 generateSuframa 1.4 KB 451 B -978 B (-68.4%) 335 B
🟢 generateVoterId 1.8 KB 852 B -978 B (-53.4%) 551 B
🟢 getAddressInfoByCep 4.3 KB 3.3 KB -978 B (-22.4%) 1.4 KB
🟢 getAreaCodeInfo 4.0 KB 3.1 KB -978 B (-23.8%) 964 B
🟢 getAreaCodesByState 1.7 KB 809 B -978 B (-54.7%) 459 B
🟢 getBankByCode 38.8 KB 37.8 KB -978 B (-2.5%) 9.2 KB
🟢 getBankByIspb 38.8 KB 37.8 KB -978 B (-2.5%) 9.2 KB
🟢 getBanks 38.5 KB 37.6 KB -978 B (-2.5%) 9.0 KB
🟢 getBoletoInfo 3.4 KB 2.4 KB -1015 B (-29.2%) 1.2 KB
🟢 getCbo 119.3 KB 115.7 KB -3.5 KB (-3.0%) 29.5 KB
🟢 getCepInfoByAddress 2.8 KB 2.0 KB -835 B (-28.9%) 1.0 KB
🟢 getCertidaoInfo 2.0 KB 1.0 KB -978 B (-48.6%) 594 B
🟢 getCest 117.9 KB 115.5 KB -2.3 KB (-2.0%) 26.1 KB
🟢 getCfop 69.1 KB 67.6 KB -1.4 KB (-2.1%) 6.3 KB
🟢 getCid10 1030.7 KB 988.3 KB -42.3 KB (-4.1%) 123.6 KB
🟢 getCities 154.4 KB 153.4 KB -978 B (-0.6%) 49.2 KB
🟢 getClassTrib 50.9 KB 50.0 KB -978 B (-1.9%) 9.0 KB
🟢 getCnae 94.1 KB 91.6 KB -2.4 KB (-2.6%) 20.0 KB
🟢 getCnpjInfo 1.9 KB 964 B -978 B (-50.4%) 542 B
🟢 getCpfInfo 1.8 KB 994 B -835 B (-45.7%) 614 B
🟢 getCstIbsCbs 1.9 KB 967 B -978 B (-50.3%) 534 B
🟢 getFormatLicensePlate 1.3 KB 315 B -978 B (-75.6%) 245 B
🟢 getGtinInfo 1.8 KB 817 B -1.0 KB (-56.1%) 555 B
🟢 getHolidays 8.3 KB 7.4 KB -1002 B (-11.7%) 2.6 KB
🟢 getIbanInfo 1.7 KB 752 B -978 B (-56.5%) 494 B
🟢 getLegalNature 6.6 KB 5.6 KB -1.0 KB (-15.7%) 1.8 KB
🟢 getLegalNatures 6.0 KB 5.1 KB -974 B (-15.8%) 1.6 KB
🟢 getLegalNaturesByCategory 6.6 KB 5.6 KB -978 B (-14.5%) 1.9 KB
🟢 getMunicipalities 156.5 KB 153.6 KB -2.9 KB (-1.9%) 49.4 KB
🟢 getMunicipality 155.0 KB 154.0 KB -978 B (-0.6%) 49.7 KB
🟢 getMunicipalityByCode 156.6 KB 153.7 KB -2.9 KB (-1.9%) 49.5 KB
🟢 getNbs 82.0 KB 80.6 KB -1.4 KB (-1.7%) 13.1 KB
🟢 getNfeKeyInfo 3.2 KB 2.2 KB -978 B (-29.9%) 1.2 KB
🟢 getNfseKeyInfo 3.4 KB 2.4 KB -979 B (-28.4%) 1.2 KB
🟢 getPixKeyInfo 4.6 KB 2.5 KB -2.0 KB (-44.6%) 1.2 KB
🟢 getPixPayloadInfo 6.6 KB 4.6 KB -1.9 KB (-29.4%) 2.1 KB
🟢 getServiceItem 27.3 KB 26.1 KB -1.2 KB (-4.4%) 8.4 KB
🟢 getStateByCep 4.6 KB 3.6 KB -978 B (-20.9%) 1.0 KB
🟢 getStateByIbgeCode 3.3 KB 2.4 KB -978 B (-28.8%) 671 B
🟢 getStateCodeByName 3.3 KB 2.3 KB -978 B (-29.2%) 649 B
🟢 getStateNameByCode 3.2 KB 2.2 KB -978 B (-29.9%) 591 B
🟢 getStates 3.1 KB 2.2 KB -978 B (-30.6%) 539 B
🟢 getTimezoneByState 1.7 KB 737 B -978 B (-57.0%) 327 B
🟢 isBusinessDay 8.8 KB 7.8 KB -1010 B (-11.2%) 2.8 KB
🟢 isHoliday 8.7 KB 7.7 KB -1006 B (-11.4%) 2.7 KB
🟢 isValidBankAccount 7.5 KB 5.2 KB -2.3 KB (-30.7%) 2.3 KB
🟢 isValidBoleto 2.6 KB 1.6 KB -1015 B (-38.0%) 892 B
🟢 isValidCEP 1.1 KB 163 B -978 B (-85.7%) 163 B
🟢 isValidCNPJ 1.7 KB 796 B -978 B (-55.1%) 447 B
🟢 isValidCPF 1.4 KB 500 B -978 B (-66.2%) 334 B
🟢 isValidCaepf 1.7 KB 782 B -978 B (-55.6%) 479 B
🟢 isValidCbo 119.2 KB 16.2 KB -103.0 KB (-86.4%) 5.5 KB
🟢 isValidCei 1.7 KB 714 B -978 B (-57.8%) 472 B
🟢 isValidCep 1.1 KB 163 B -978 B (-85.7%) 163 B
🟢 isValidCertidao 1.7 KB 758 B -978 B (-56.3%) 462 B
🟢 isValidCest 116.7 KB 7.6 KB -109.1 KB (-93.5%) 2.3 KB
🟢 isValidCfop 69.0 KB 2.8 KB -66.2 KB (-96.0%) 1.4 KB
🟢 isValidCid10 27.2 KB 26.2 KB -978 B (-3.5%) 6.8 KB
🟢 isValidClassTrib 2.8 KB 1.8 KB -978 B (-34.7%) 639 B
🟢 isValidCnae 94.0 KB 9.6 KB -84.4 KB (-89.8%) 3.4 KB
🟢 isValidCnh 1.6 KB 610 B -978 B (-61.6%) 370 B
🟢 isValidCno 1.7 KB 718 B -978 B (-57.7%) 474 B
🟢 isValidCnpj 1.7 KB 796 B -978 B (-55.1%) 447 B
🟢 isValidCns 1.7 KB 746 B -978 B (-56.7%) 475 B
🟢 isValidCpf 1.4 KB 500 B -978 B (-66.2%) 334 B
🟢 isValidCreditCard 1.5 KB 555 B -1015 B (-64.6%) 389 B
🟢 isValidCsosn 1.3 KB 330 B -978 B (-74.8%) 262 B
🟢 isValidCst 1.9 KB 995 B -978 B (-49.6%) 569 B
🟢 isValidCstIbsCbs 1.8 KB 894 B -978 B (-52.2%) 505 B
🟢 isValidEmail 1.1 KB 171 B -978 B (-85.1%) 157 B
🟢 isValidGtin 1.4 KB 428 B -1.0 KB (-70.9%) 313 B
🟢 isValidIE 6.2 KB 5.1 KB -1.0 KB (-16.6%) 1.8 KB
🟢 isValidIban 1.5 KB 531 B -978 B (-64.8%) 376 B
🟢 isValidIe 6.2 KB 5.1 KB -1.0 KB (-16.6%) 1.8 KB
🟢 isValidLandlinePhone 1.6 KB 717 B -958 B (-57.2%) 463 B
🟢 isValidLegalNature 5.9 KB 649 B -5.3 KB (-89.3%) 427 B
🟢 isValidLicensePlate 1.2 KB 267 B -978 B (-78.6%) 216 B
🟢 isValidMobilePhone 1.7 KB 758 B -958 B (-55.8%) 496 B
🟢 isValidNbs 81.9 KB 8.5 KB -73.4 KB (-89.6%) 2.3 KB
🟢 isValidNcm 114.3 KB 82.6 KB -31.7 KB (-27.8%) 22.8 KB
🟢 isValidNfeKey 2.6 KB 1.6 KB -978 B (-37.1%) 954 B
🟢 isValidNfseKey 3.0 KB 2.1 KB -978 B (-31.7%) 1.1 KB
🟢 isValidPIS 1.4 KB 418 B -978 B (-70.1%) 316 B
🟢 isValidPassport 1.1 KB 197 B -978 B (-83.2%) 186 B
🟢 isValidPhone 2.7 KB 1.8 KB -958 B (-34.2%) 909 B
🟢 isValidPis 1.4 KB 418 B -978 B (-70.1%) 316 B
🟢 isValidPixKey 4.4 KB 2.3 KB -2.0 KB (-46.5%) 1.1 KB
🟢 isValidPixPayload 6.1 KB 4.2 KB -1.9 KB (-31.8%) 1.9 KB
🟢 isValidProcessoJuridico 1.4 KB 699 B -765 B (-52.3%) 429 B
🟢 isValidRegistroProfissional 2.0 KB 1.2 KB -839 B (-41.1%) 724 B
🟢 isValidRenavam 1.5 KB 516 B -978 B (-65.5%) 356 B
🟢 isValidServiceItem 27.2 KB 1.2 KB -26.0 KB (-95.7%) 694 B
🟢 isValidServicePhone 1.6 KB 706 B -974 B (-58.0%) 372 B
🟢 isValidSuframa 1.5 KB 590 B -978 B (-62.4%) 411 B
🟢 isValidVin 1.8 KB 848 B -978 B (-53.6%) 573 B
🟢 isValidVoterId 1.6 KB 706 B -978 B (-58.1%) 427 B
🟢 parseBoleto 1.2 KB 247 B -978 B (-79.8%) 215 B
🟢 parseCaepf 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCbo 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseCei 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCep 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseCertidao 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCest 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseCfop 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseCid10 1.1 KB 177 B -978 B (-84.7%) 170 B
🟢 parseCnae 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseCnh 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCno 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCnpj 1.2 KB 298 B -978 B (-76.6%) 240 B
🟢 parseCns 1.1 KB 199 B -978 B (-83.1%) 184 B
🟢 parseCpf 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseCurrency 1.6 KB 610 B -978 B (-61.6%) 409 B
🟢 parseIban 1.2 KB 227 B -978 B (-81.2%) 202 B
🟢 parseLegalNature 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseLicensePlate 1.1 KB 177 B -978 B (-84.7%) 170 B
🟢 parseNcm 1.1 KB 198 B -978 B (-83.2%) 182 B
🟢 parseNfeKey 1.4 KB 412 B -978 B (-70.4%) 312 B
🟢 parseNfseKey 1.2 KB 253 B -978 B (-79.4%) 219 B
🟢 parsePassport 1.1 KB 177 B -978 B (-84.7%) 169 B
🟢 parsePhone 1.3 KB 370 B -958 B (-72.1%) 281 B
🟢 parsePis 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 parseProcessoJuridico 1.1 KB 199 B -978 B (-83.1%) 182 B
🟢 parseSuframa 1.1 KB 198 B -978 B (-83.2%) 181 B
🟢 parseVoterId 1.1 KB 199 B -978 B (-83.1%) 183 B
🟢 removeAccents 1.1 KB 99 B -978 B (-90.8%) 119 B
🟢 subBusinessDays 9.6 KB 8.6 KB -1006 B (-10.3%) 3.1 KB
🟢 toStandardSchema 1.2 KB 284 B -978 B (-77.5%) 233 B
How this is measured

Every 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 tree-shaking: accepted label.

@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (6782fd0) to head (806bd7f).

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     
Flag Coverage Δ
node 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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
@hyanmandian
hyanmandian added this pull request to stack #614 September 29, 2026 04:08
@hyanmandian
hyanmandian merged commit 1f82b56 into main Sep 29, 2026
30 checks passed

This branch was successfully deployed

1 active deployment
Preview — 806bd7f1 Deployed Sep 29, 2026 by vercel[bot]
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.

2 participants