Repository navigation
Validation round: fix every audited edge case, docs and convention drift - #615
Conversation
A local midnight that falls in a daylight saving gap (1 December 1950 in America/Sao_Paulo) is read as 01:00, and addBusinessDays keeps the time of day of its input, as date-fns does, so the properties compared a 01:00 result with a midnight expectation. They now compare the day.
…an linearly Add the contracted and remaining prepositions (ao, aos, à, às, ante, após, até, num, numa, para, pela, pelas, pelo, pelos, perante, sob, sobre) to the default lowerCaseWords, and accept S.A without the final dot as a company designation. "entre" stays out of the list: the municipality "Desterro de Entre Rios" would otherwise lose its capital. nextWordIndex no longer slices the token array, so long input is linear instead of quadratic. JSDoc structure and the IBGE claim are fixed and the prose comments are moved into the JSDoc. Output changes (wrong to correct): "casa para todos" "Casa Para Todos" to "Casa para Todos" (same for the other added prepositions when they link two words); "empresa s.a" "Empresa S.a" to "Empresa S.A". Running capitalize over every IBGE municipality name in original, upper and lower case changes no output.
A validator that threw made validate throw too, although the schema is meant
to report failures as issues and the JSDoc says it never throws. The call is
now wrapped and a throw counts as an invalid value. The second parameter is
renamed from config to options, which is invisible to callers.
Output change (wrong to correct): a wrapped function that throws used to
propagate the error out of validate and now returns { issues: [...] }. The
isValid* utilities never throw, so their behaviour is the same.
…path entry Subpath consumers could not name the type of options.gender, since only the root entry re-exported it. The Based on note of the JSDoc is rewritten as a clean sentence under the URL. No output change.
…ctions The number and currency sections now list the rules the code already implements: where the e goes, the singular, the de before round millions, zero reais for a negative amount that truncates to nothing, and the cents cutoff above about 90 trillion. English and Portuguese.
…alidate version The Hono snippet passed a string schema to the param target, which hands the validator an object, so every request was rejected. The route now validates the JSON body with an object schema. The VeeValidate line needs version 5, which the guide now says (English and Portuguese).
…omplete CEP The English guide linked document-field.md, which docsify resolves from the site root and 404s; it now points at guides/document-field.md like the Portuguese one. The vanilla snippet left aria-busy on the CEP field when a pending lookup was followed by an incomplete CEP.
… B, A race The guide imported the deprecated getCities. The four snippets now use getMunicipalities, with the IBGE code as the option value and the name as the label, since names repeat across states. The size figure is 148 KB, the size of the municipality table and its helpers in dist. In React and Vue, picking A, then B, then A again while the first table was still loading left the select on the loading message forever; the abort now also clears the asked state. The em dash of the intro is gone. English and Portuguese.
…and keep tables literal
Amazonas 8 December was the only state entry without a since, so it was listed as an optional
holiday for every year from 1900. The earliest norm located is Lei Municipal de Manaus nº 496/1999,
so it now starts in 1999. ELECTION_DATE_OVERRIDES is a plain literal and the memo an object literal
instead of top-level Map instances. The prose comments moved into JSDoc, the HolidayPeriod keys are
documented, the RS decree of 1995 has its URL and the legacy planalto links are the https ccivil_03
ones.
Output change (wrong to correct): getHolidays({ year, stateCode: "AM" }) no longer lists Nossa
Senhora da Conceição on 8 December for years before 1999, so isHoliday is false and isBusinessDay is
true on that day for AM before 1999, where 2.4.0 said the opposite.
…liday counts The getHolidays example was headed national holidays but mixed optional and religious entries and cut the list at random; it now lists the 15 entries of 2024. isHoliday says that years outside 1900 to 2099 are false and that optional and religious entries count, and the AM 8 December is documented as starting in 1999. English, Portuguese and the JSDoc.
The docs and the getNfseKeyInfo JSDoc said a separator anywhere in an NFS-e key is rejected and that
isValidNfseKey('3550308 2 2 ...') is false. The code accepts the mask characters at the boundaries
of the 8 fields and rejects one inside a field, as the isValidNfseKey JSDoc already said. The docs
(EN and PT) and the JSDoc now match the code and the example is true. No output change.
…tNfeKey
formatNfeKey only dropped the letters of the NFe, CTe, MDFe, BPe and NFCom prefixes, so the digit of
NF3e ended up in front of the key and the last digit was cut. It now strips the prefixes like
parseNfeKey and isValidNfeKey do. It also returned digits for values that are not keys (-1, 1.5,
10n, functions); like parseNfeKey it now reads only strings and non-negative safe integers and
returns an empty string otherwise.
Output changes:
- formatNfeKey('NF3e' + key): '3351 7045 ...' (a leading 3, last digit lost) -> '3517 0458 ...'
- formatNfeKey(-1): '1' -> ''
- formatNfeKey(1.5): '15' -> ''
- formatNfeKey(10n): '10' -> ''
- formatNfeKey(() => 1): '1' -> ''
…istroProfissional too
Both validators stripped everything that is not a letter or a digit before checking the shape, so
'A@B#C1$2%3^4', 'ABC1234' followed by an emoji and '12@3#4$5/SP' were valid. The other validators of
the package (isValidNfeKey, isValidCertidao, isValidProcessoJuridico, isValidIsbn) only accept
whitespace, '.', '-' and '/'. isValidLicensePlate now accepts that mask only between the third
character and the last four, and isValidRegistroProfissional accepts it anywhere but rejects any
other character. getFormatLicensePlate and convertLicensePlateToMercosul call isValidLicensePlate,
so they follow.
Output changes (true -> false):
- isValidLicensePlate('A@B#C1$2%3^4'), ('ABC1234' + emoji), ('A-B-C-1-2-3-4'), ('ABC12-34')
- isValidRegistroProfissional({ value: '12@3#4$5/SP', council: 'OAB' }), ({ value: '123456/SP' +
emoji, council: 'OAB' })
- getFormatLicensePlate('A-BC1234'): 'LLLNNNN' -> null
- convertLicensePlateToMercosul('A-BC1234'): 'ABC1234'-based plate -> ''
…d isValidNfeKey
toUpperCase folds some non-ASCII letters into ASCII ones, so isValidVin('9BWZZZ377VT004' + 'ſ51')
read the long s as S and a 16 character VIN ending in 'ß' became 17 characters ('SS'). isValidNfeKey
had the same fold: a key with 'ſ' in the CNPJ area was valid although parseNfeKey drops that
character, so validity and parsing disagreed. Both now test the raw value against an ASCII pattern
before upper casing.
Output changes (true -> false):
- isValidVin('9BWZZZ377VT004ſ51'), isValidVin('9BWZZZ377VT0042ß')
- isValidNfeKey('35260712ABſ34501DE35550010000001231102030404')
isValidCertidao names CertidaoType in its options and isValidRegistroProfissional names RegistroProfissionalCouncil in its params, but their own entries only exported the option types. Both now re-export the type their signature names, like get-nfse-key-info does with StateCode. No runtime change.
…uer document isValidNfeKey applies rule B03-10 of the MOC, which the authorizer enforces only for documents sent after NT 2019.001, so a key issued earlier with a cNF equal to the document number is turned down. It also checks only the key's own check digit, not the CPF or CNPJ of the issuer. Both are now stated in the JSDoc and in the EN and PT docs, with the way to check the issuer through getNfeKeyInfo. No output change.
The behaviour each comment described is already in the JSDoc of the function or obvious from the code, and the package keeps only directive comments in src. No behaviour change.
…nse plate JSDoc The paragraph explaining the Resolução CNJ nº 65/2008 and the Resolução CONTRAN nº 969/2022 sat after the closing fence of @example, so JSDoc tools rendered it as part of the example. It now sits in the description above @PARAM in the processo jurídico and license plate modules. formatProcessoJuridico and formatCertidao document the optional options.pad as [options.pad], and the long NFS-e, GTIN and processo paragraphs are wrapped at the same width. No behaviour change.
…te from K to Z
Anexo II, item 2, of Resolução CONTRAN nº 969/2022 reserves the range A to J to convert an old
format plate (digits 0 to 9), so a new Mercosul plate never carries it in the fifth position.
generateLicensePlate drew that letter from the whole alphabet, so 10 of 26 generated plates were in
the converted-only range. Every generated plate is still accepted by all the validators.
Output change (random distribution): the fifth character of generateLicensePlate() and
generateLicensePlate('LLLNLNN') is now one of K to Z instead of A to Z.
…ter params The parameter was called options but typed GenerateProcessoJuridicoParams, and the JSDoc documented options.year and options.court. It is now params everywhere, with the optional fields in brackets. The call signature and types are unchanged. No behaviour change.
The JSDoc cited NT 2021.003 as v1.50 of September 2026, but the PDF at the linked URL is v1.30 of December 2023. The rules the library relies on (I03-10 and I12-10, rejections 611 and 612, and the 8, 12, 13 and 14 digit field description) are in that version, so only the version wording changes. No behaviour change.
…nd quote the outputs The isValidVin docs did not say that separators and spaces inside the VIN are rejected (the JSDoc did), the formatLicensePlate docs left out the progressive mask that makes it usable as an input mask, and the formatCertidao, formatProcessoJuridico and parseProcessoJuridico examples printed their results without quotes while the other families quote them. EN and PT docs updated together, plus a test for the VIN separators. No output change.
isValidPhone, isValidMobilePhone, isValidLandlinePhone and isValidServicePhone
reduced the value to digits before judging it, so any character was silently
dropped. A letter is not a mask character. The validators now return false when
the value holds anything other than digits, whitespace and ()+.-/.
Output changes (wrong true becomes false):
- isValidPhone("11 98765-4321x"): true -> false
- isValidMobilePhone("11 98765-4321x"): true -> false
- isValidLandlinePhone("tel 1130000000"): true -> false
- isValidServicePhone("abc190"): true -> false
normalizePhone only removed the country code when 10 or 11 digits were left, so
a number still being typed after an explicit +55 or 0055 kept the 55 and read it
as the DDD. An explicit country code has no ambiguity with DDD 55, so it is now
always removed. A bare 55 keeps the length rule.
Output changes in parsePhone (and so in every reader of normalizePhone), only
for short input that starts with an explicit country code:
- parsePhone("+55 11 9"): "55119" -> "119"
- parsePhone("+55"): "55" -> ""
- parsePhone("0055 (11) 98888"): "00551198888" -> "1198888"
Full length numbers are unchanged.
formatPhone read the digits of the value before looking for a country code, so
it lost the "+" that makes +55 unambiguous, and the sn and nanp masks never
dropped it at all. Every mask now drops an explicit +55 or 0055 (a bare 55 stays
under sn and nanp, since it may be the DDD), and e164 keeps at most the 11
national digits, as international already did.
Output changes:
- formatPhone("+5511987654321"): "55119-8765" -> "11987-6543" (default sn)
- formatPhone("+5511987654321", { mask: "nanp" }): "(55) 11987-6543" -> "(11) 98765-4321"
- formatPhone("+55", { mask: "e164" }): "+5555" -> ""
- formatPhone("+55 11 9", { mask: "auto" }): "55119" -> "+55 11 9"
- formatPhone("+55 11 9", { mask: "international" }): "+55 55 119" -> "+55 11 9"
- formatPhone("119888877660000", { mask: "e164" }): "+55119888877660000" -> "+5511988887766"
isValidPhone with accept: ["service"] read "+55 0800 123 4567" as a service
number, but isValidServicePhone rejected the same value. isValidServicePhone now
resolves the country code the same way. The structural check moves to an
internal so the validator and the resolver no longer import each other.
Output changes (false becomes true):
- isValidServicePhone("+55 0800 123 4567")
- isValidServicePhone("+55 190")
- isValidServicePhone("0055 4004-1234")
- isValidServicePhone("5508001234567")
isValidEmail capped domain labels at 63 characters but not the local part or the
whole address, and rejected a valid punycode top-level domain because the final
label had to be letters only. The local part is now capped at 64 characters and
the address at 254 (RFC 5321, section 4.5.3.1), and a final label may be a
punycode label (xn--). The docs now list what is rejected.
Output changes:
- isValidEmail("a".repeat(65) + "@a.com"): true -> false
- isValidEmail("a".repeat(300) + "@a.com"): true -> false
- isValidEmail("user@example.xn--p1ai"): false -> true
PHONE_MASKS was a top-level new Set and the municipality area code index a top-level new Map, which defeats tree shaking. Both are now plain literals, the index filled on first use. Prose comments move out of the source, and the notes that sat after @returns, @example and @see in getAreaCodeInfo, getAreaCodesByState and isValidMobilePhone move into the description, so tooling no longer renders them inside the wrong tag. No output change.
generatePhone drew a landline first digit from 2 to 6. Resolucao Anatel 777/2025,
art. 21 narrows the STFC range to 2 to 5 from 1 March 2027, and isValidLandlinePhone
already documents that change. Drawing 2 to 5 keeps every generated landline valid
before and after it.
Output change: generatePhone("landline") no longer returns a number whose first
digit after the DDD is 6 (the distribution of a random value, not a wrong result).
The formatCurrency and findCodeIndex caches were top-level new Map calls that bundlers cannot prove pure. They are created on first use now. Prose // comments in src are gone (the one that carried a reason moved into a JSDoc), and the isValidPixKey entry re-exports PixKeyType like getPixKeyInfo does. No output change.
_internals holds one function per folder and read-state-code exported two. normalizeStateCode now lives in _internals/normalize-state-code with its own tests, and readStateCode imports it. The eleven utils that only normalize import it from the new folder. No behavior change.
getBankByCode and getBankByIspb embed the full bank table (about 38 KB) like getBanks, and the deprecated getMunicipality and getCities embed the municipality table, but the rule did not name them. Docs only.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
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❌ 3 size regressions. 73 grew, 18 shrank, 3 new out of 198 exports.
What changed (94)
Show the other 74
All exports (198)
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 @@
## main #615 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 239 247 +8
Lines 2430 2500 +70
Branches 714 733 +19
=========================================
+ Hits 2430 2500 +70
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:
|
…the supported years Past about year 275,000 the fator de vencimento resolves to an Invalid Date on both sides of the comparison, and Bun and Deno do not treat two Invalid Dates as equal the way vitest does, so the property drew its reference date from any year and failed on those runtimes. The date is now drawn between 1900 and 2199.
…EP range alone The cross-check of a provider's state only needs the state code of the CEP range, not the whole State object getStateByCep builds, so the lookup no longer bundles the states table: about 3 KB less in a bundle that imports getAddressInfoByCep alone. Same result for every CEP.
d12faad to
0df8ce1
Compare
|
Tree-shaking: the three flagged exports grow on purpose. |
normalizePhone now strips an explicit +55 or 0055, so the 13 digit value left for
"+555511987654321" was stripped a second time inside isValidMobilePhone and the key was
accepted, and getPixKeyInfo returned the malformed +555511987654321. A phone key is +55
followed by the 11 digit national number exactly once, so the national digits are now
required to be 11 long before they are validated as a mobile number.
Output change (wrong to correct, false in 2.4.0 as well):
isValidPixKey("+555511987654321"), "+55+5511987654321" and "0055+5511987654321" go from
true to false, and getPixKeyInfo of the same values from a phone key to null.
… zero as the same CEP The cross-check of the answered CEP compared the provider's digits as they came, so a provider answering 1310100 for 01310-100 was treated as not knowing the CEP, which 2.4.0 accepted. The digits are now left padded to 8 before the comparison. A shortened CEP that is another CEP is still rejected. Output change (wrong to correct): a provider answer with the leading zero dropped resolves instead of throwing GetAddressInfoByCepNotFoundError.
…ead of the daylight saving one Decreto 8.112/2013 is the daylight saving decree, revoked in 2019, not the legal time norm. The zones come from Decreto 2.784/1913 as amended by Lei 11.662/2008 and Lei 12.876/2013.
…ps to the next cycle A slip due 3499 days before the reference date keeps its date and one due 3500 days before it is read as the next cycle, about 9.6 years and not the 8 years the text said. The 15 years ahead half never applies to an old cycle candidate, so it is dropped.
Replace the login-walled gov.br CPF URL with the Meu CPF page, drop the Goiás notice URL that redirects to an unrelated page and the passport service URL that redirects to a service page (the Polícia Federal FAQ stays), point the PT state-city guide at the renamed bundle section, remove a duplicated isValidBoleto example, reword the isValidCst number 7 note, add the missing semicolons in the generate import examples and spell hífens in PT.
… read as arrecadação FEBRABAN product identifier 8 marks the arrecadação, so a cobrança bancária barcode of a bank code from 800 to 899 (only 804 exists) is not accepted in barcode form, while its linha digitável is.
… the plate format alias generateCep: the two ranges no state owns are 1.1% of all CEPs, about 1 in 90, not 1 in 80. The CEP ranges note now says the Roraima block sits between the two Amazonas ranges and a Goiás range between the two Distrito Federal ones. getLegalNature names the three retired codes without a successor (2100, 3050, 3123). GenerateLicensePlateFormat is documented as an alias of LicensePlateFormat.
… document the rest Stryker's 100% threshold failed on 17 mutants. Nine are equivalent and now carry a disable directive with the reason: the cache writes of findCodeIndex and formatCurrency, the look-ahead bound of capitalize, the replacement of the XML Id prefix in formatNfeKey, the format check of generateLicensePlate, the message of confirmAddress, the two guards of toStandardSchema and the string guard of getCepInfoByAddress. The other eight are killed by new tests: the providers of getAddressInfoByCep answering without a CEP for the CEP 00000000, and the non-string values of isValidPhone and isValidServicePhone. No behavior changes.
…any value
isValidPhone only ran the character check on strings, so an object whose toString gives "190x"
was read as the service number 190 while the string "190x" was rejected. The check now runs on
toStringSafe(value) for every input type.
Output change: an object (or array) whose string form holds a character other than digits,
whitespace and ()+.-/ goes from valid to invalid, like the same string. Numbers are unchanged,
their string form is digits only. isValidServicePhone(190) stays false while
isValidPhone(190, { accept: ["service"] }) stays true, as in 2.4.0.
The DDD index is a plain object, so a key such as "constructor" read an inherited member. A code that is not an own key of the index now gives NaN instead of undefined or an inherited value. Callers only pass 7 digit codes read from the same table, so no public result changes.
…rmatter cache generateLicensePlate checked safeFormat === DEFAULT_FORMAT before the Mercosul letter index, but index 4 of the old format is a digit, so the check was dead code and its directive hid three killable mutants. The check and the directive are gone; Stryker scores 100% on the file without any directive. formatCurrency kept a directive above `formatters ??= new Map()`, a statement with no mutant, and one above the cache hit return, which also hid the killable variant of the condition. The cache is now read as `get(key) ?? store(...)`, so only the directive of the set call, whose removal changes nothing observable, is left. No behavior changes.
909fa89 to
0ad6d4e
Compare
Function-by-function validation of every public export against its official source, the project conventions and the docs (EN and PT), then the fixes. First PR of a stack of three: this one (behaviour and docs), then the bundle size work, then the runtime performance work.
How it was checked
Nine independent audits, one per family (documents, payments, phone and e-mail, CEP/states/municipalities, holidays and business days, classification codes, keys/registries/vehicles, text and Standard Schema, and a cross-cutting pass over the public surface). Each one re-derived the rules from the official texts, fetched every
@seelink, ran the edge cases (masked input, numbers, hostile values), executed every docs and JSDoc example, and compared the datasets with their live sources (IBGE municipalities/states/regions, Anatel DDDs, Correios ranges, Bacen STR banks, CONFAZ/CONCLA/Receita tables): no check-digit algorithm or dataset was wrong. The findings were edge cases, docs that contradicted the code, citations and convention drift.Output changes (all wrong to correct, none breaking)
Documents and formatters
format*built on the shared mask helper returns""for a value with no digits even withpad: true(it returned the full zero mask):formatCpf("", { pad: true })is now"". Affects the CPF, CNPJ, PIS, CNH, voter ID, CNS, SUFRAMA, CEI, CNO, CAEPF, CEP, certidão, CEST, CNAE, NCM, NF-e key, processo, boleto and legal nature formatters.Phone and e-mail
()+.-/(isValidPhone("11 98765-4321x")wastrue).+55/0055is stripped at any length (parsePhone("+55 11 9")was"55119"), and everyformatPhonemask drops it (nanpgave"(55) 11987-6543");e164keeps at most 11 national digits.isValidServicePhoneaccepts a country code, asisValidPhone({ accept: ["service"] })already did.isValidEmailcaps the local part at 64 and the address at 254 characters and accepts a punycode final label (user@example.xn--p1ai).generatePhone("landline")no longer draws a first digit of 6 (Res. Anatel 777/2025 art. 21, from March 2027).CEP and address
getAddressInfoByCeptreats a provider answer whose CEP or state contradicts the request as not found (BrasilAPI answered a Paraná address for the RS CEP99999-999), and aborts the providers that lost the race.getCepInfoByAddressrejects a blank, non-string or under-3-character city or street withGetCepInfoByAddressValidationErrorbefore any request.generateCeponly draws inside the ranges a state owns (00000-000–00999-999and78900-000–78999-999are gone).1310100for01310-100) is read as the same CEP.Payments
isValidBoletorejects letters and stray characters, and accepts the 44-digit cobrança bancária barcode (the arrecadação barcode was already accepted);getBoletoInforeads it too.getBoletoInfofalls back to today for an invalid or non-DatereferenceDate(an invalid Date gave the 1997 base date and a non-Date threw).isValidPixPayloadrequires the CRC to be the last top-level object and rejects a repeated ID (crafted payloads only).+55exactly once (+555511987654321stays invalid, as in 2.4.0; the stricter+55handling of the phone fixes had made it valid).getFormatLicensePlateandconvertLicensePlateToMercosulfollow the stricter plate mask ofisValidLicensePlate; the formatters and parsers stay lenient, as every formatter reads only letters and digits.Keys, registries, vehicles
formatNfeKeystrips the XMLIdprefixes (NF3eleaked its3) and returns""for a non-string.isValidLicensePlateandisValidRegistroProfissionalreject characters outside the mask ("A@B#C1$2%3^4"wastrue).isValidVinandisValidNfeKeyno longer foldß/ſinto ASCII letters.generateLicensePlatedraws the fifth character of a new Mercosul plate from K to Z (A to J is reserved for converted plates, Res. CONTRAN 969/2022 Anexo II).Classification codes
isValidLegalNatureaccepts numbers like its siblings, and legal nature codes accept the mask only after the third digit ("-2-0-6-2"was valid).isValidCstaccepts the bare 2-digit Tabela B codes fortax: "icms"(the NF-eCSTfield next toorig); nullishoptionsread as none inisValidCst/isValidClassTrib.Text, schema, holidays
capitalizekeeps more prepositions in lower case (ao, aos, à, às, para, pela(s), pelo(s), sob, sobre, até, num, numa, ante, após, perante) andS.Atyped without the final dot upper case; checked against every IBGE municipality name in original, upper and lower case with 0 changes.toStandardSchemareports a throwing validator as an issue instead of throwing.New API (additive)
formatCbo(NNNN-NN),formatCfop(N.NNN),parseNbsand apadoption onformatNbs, so every masked classification code has its formatter and parser.Conventions and docs
scripts/now emit the committed mask regexes (the weekly dataset refresh would have reverted the mask-run change).new Map/new Setleft (lazy caches), no prose//comments left (Stryker/eslint directives stay), one-line JSDoc on every exported type key,normalizeStateCodein its own_internalsfolder,buildLegalNaturemoved to_internals(still re-exported,@deprecated, from the 2.4.0 subpath).PixKeyType,CertidaoType,RegistroProfissionalCouncil,NumberToWordsGender,LegalNature,IsValidCnpjOptions,FormatCnpjOptions,LicensePlateFormat).1-01, "single separator" wording, invalidgenerateCnpjexamples, ...), notes added where a caller needs them (alphanumeric CNPJ needsversion: 2, numbers lose leading zeros, multi-zone states ingetTimezoneByState, the exact window of the fator de vencimento (a slip due up to 3,499 days beforereferenceDatekeeps its date, from 3,500 days on it reads as the next cycle), ...), citations updated (e-Financeira Anexo II, Lei 12.876/2013, Bradesco for the 1997 base date, planaltoccivil_03links), guides fixed (Hono snippet, state-city race andgetMunicipalities, address-form links andaria-busy).For the maintainer
Unreleased on
mainalready (not this PR): the mask convention of fix(validators): read the mask of every identifier the way isValidCpf does #611 makes date-shaped strings valid identifiers (isValidCep("29/09/2026"),isValidCnh("65.843.791/599")), and the Carnaval Monday ponto facultativo moves defaultaddBusinessDaysresults by one day around Carnaval. Both are intended there; worth a line in the 2.5.0 notes.isValidVinno longer enforces the check digit by default since 38a2194 (already onmain): values 2.4.0 rejected are now valid. Documented and sourced, but it is the one default that widens against 2.4.0.NCM
85181020enters into force on 01/10/2026: regenerate the NCM table on or after that date before releasing 2.5.0.Not done on purpose (would be new API or a v3 change):
getProcessoJuridicoInfo,getVoterIdInfo,generateCns/generateCei, ISBN-10, credit card format/brand, a B03-10 opt-out for pre-2019 NF-e keys,rejectZerosfor IE.Gates
The
tree-shaking: acceptedlabel covers three intended growths:generateCep(+1.2 KB, the Correios ranges table),getAddressInfoByCep(+1.4 KB, the CEP range cross-check and the abort of the losing providers) andisValidServicePhone(+357 B, the country code and the character check).An independent differential run over 10.5M calls (npm 2.4.0 vs
mainvs this stack) found no output change outside the list above.vp check, 8,368 tests with 100% coverage (also underTZ=America/Sao_Paulo), build, jscpd, knip,check:api(no breaking change against 2.4.0: 731 type assertions hold), commitlint.