diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index dab2ffb6..80466e04 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -48,8 +48,8 @@ jobs: - name: Check for unused files, exports and dependencies run: npm run check:unused - - name: Check the public API report is up to date - run: npm run build && npx api-extractor run --verbose + - name: Validate the public API + run: npm run check:api - name: Check llms.txt/llms-full.txt are up to date run: npm run build:llms && git diff --exit-code -- docs/llms.txt docs/llms-full.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cee6020..4fae7e44 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,24 +28,24 @@ and is invoked through the `npm` scripts below, so you don't need to install any ### Useful scripts -| Command | What it does | -| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `npm check` | Runs `vp check`: format check, lint and type-check together. Run this before opening a PR. | -| `npm check:fix` | Same as above, but auto-fixes what it can. | -| `npm format` / `npm format:check` | Formats the codebase / checks formatting with `vp fmt`. | -| `npm lint` / `npm lint:fix` | Lints the codebase with `vp lint`. | -| `npm test` | Runs the unit test suite with `vp test`. | -| `npm test:coverage` | Runs tests with coverage (`vp test run --coverage`). | -| `npm test:bun` | Runs the test suite on [Bun](https://bun.sh) (`bun test src`). | -| `npm test:deno` | Runs the test suite on [Deno](https://deno.com) (`deno test`). | -| `npm test:chrome-browser`, `npm test:firefox-browser`, `npm test:edge-browser`, `npm test:safari-browser` | Runs the test suite in real browsers via `vp test --browser.enabled`. | -| `npm build` | Builds the library with `vp build`. | -| `npm run check:duplication` | Runs [jscpd](https://jscpd.dev) over `src` and `scripts`; any copy-pasted block of 5+ lines / 50+ tokens fails. | -| `npm run check:unused` | Runs [knip](https://knip.dev): unused files, exports, types and dependencies fail. | -| `npm run test:mutation` | Runs [Stryker](https://stryker-mutator.io) mutation tests (`stryker run`); pass `-- --mutate src//.ts` for one file. | -| `npm run check:api` | Builds the public API report (`api/brazilian-utils.api.md`) with API Extractor; commit the updated file. | -| `npm run check:commits` | Checks the commit messages since `origin/main` with commitlint (Conventional Commits). | -| `npm run check:lockfile` | Checks `package-lock.json` only resolves to the npm registry over HTTPS with integrity hashes (lockfile-lint). | +| Command | What it does | +| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `npm check` | Runs `vp check`: format check, lint and type-check together. Run this before opening a PR. | +| `npm check:fix` | Same as above, but auto-fixes what it can. | +| `npm format` / `npm format:check` | Formats the codebase / checks formatting with `vp fmt`. | +| `npm lint` / `npm lint:fix` | Lints the codebase with `vp lint`. | +| `npm test` | Runs the unit test suite with `vp test`. | +| `npm test:coverage` | Runs tests with coverage (`vp test run --coverage`). | +| `npm test:bun` | Runs the test suite on [Bun](https://bun.sh) (`bun test src`). | +| `npm test:deno` | Runs the test suite on [Deno](https://deno.com) (`deno test`). | +| `npm test:chrome-browser`, `npm test:firefox-browser`, `npm test:edge-browser`, `npm test:safari-browser` | Runs the test suite in real browsers via `vp test --browser.enabled`. | +| `npm build` | Builds the library with `vp build`. | +| `npm run check:duplication` | Runs [jscpd](https://jscpd.dev) over `src` and `scripts`; any copy-pasted block of 5+ lines / 50+ tokens fails. | +| `npm run check:unused` | Runs [knip](https://knip.dev): unused files, exports, types and dependencies fail. | +| `npm run test:mutation` | Runs [Stryker](https://stryker-mutator.io) mutation tests (`stryker run`); pass `-- --mutate src//.ts` for one file. | +| `npm run check:api` | Builds the package and runs API Extractor over `dist/brazilian-utils.d.ts`: a public type without a doc comment, or a type the API refers to without exporting, fails. | +| `npm run check:commits` | Checks the commit messages since `origin/main` with commitlint (Conventional Commits). | +| `npm run check:lockfile` | Checks `package-lock.json` only resolves to the npm registry over HTTPS with integrity hashes (lockfile-lint). | Before opening a pull request, make sure `npm check` and `npm test` both pass locally. If your change touches runtime behavior, also consider running the Bun/Deno scripts above. The library is @@ -132,9 +132,15 @@ categories as errors, the `import`, `jsdoc` and `promise` plugins, and a curated on every function, no `console` outside `scripts/`, no `forEach`, no parameter reassignment, no non-null assertions, no unsafe type assertions, JSDoc `@param`/`@returns` with types on exported functions, `type` over `interface`, `T[]` over `Array`, and no default exports outside the -config files. Test files relax the rules that only make sense for production code (return types, -JSDoc, the `unsafe-*` family, since the multi-runtime `expect` shim is untyped) and every -`@ts-expect-error` must carry a description. +config files. On top of the categories, about two hundred `style`/`restriction` rules that +have a clear quality payoff are switched on one by one (inline `type` import specifiers, +`startsWith` over `slice` comparisons, negative indexes, no `reduce`, `await` over `then`, no +`Array#apply`, `max-params` of 4, kebab-case file names, the `promise` invariants, the `jsdoc` +tag checks and the `vitest` matcher preferences, among others); whole categories such as +`no-magic-numbers`, `no-null`, `one-var` or `no-plusplus` stay off because they fight the +check-digit code and the `null`-returning API on purpose. Test files relax the rules that only +make sense for production code (return types, JSDoc, the `unsafe-*` family, since the +multi-runtime `expect` shim is untyped) and every `@ts-expect-error` must carry a description. `tsconfig.json` is `strict` plus `noImplicitOverride`, `noUnusedLocals`, `noUnusedParameters` and `noPropertyAccessFromIndexSignature`. `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes` @@ -176,13 +182,14 @@ pull request so the CI result is not a surprise. `// Stryker disable next-line : ` right above the line; that is the one place an inline comment is accepted in this codebase. -## Public API report +## Public API validation -`api/brazilian-utils.api.md` is generated by [API Extractor](https://api-extractor.com) from the -bundled `dist/brazilian-utils.d.ts` and lists every exported function, type and overload of the -package. CI rebuilds it and fails when the committed file is stale, so any change to a public -signature shows up as a diff in the pull request, which is how "no breaking changes" is reviewed -mechanically. After changing anything exported, run `npm run check:api` and commit the report. +[API Extractor](https://api-extractor.com) runs over the bundled `dist/brazilian-utils.d.ts` in CI +(`npm run check:api`). It fails when a type the public API refers to is not itself exported (a +consumer could not name it) and when an exported function, type or class has no doc comment. The +report it writes lands in the ignored `reports/api/` folder and is not committed: the public +signatures are pinned by the `describe(" types")` blocks in the tests, and the +`src/index.test.ts` export map catches an export that goes missing. ## Supply chain diff --git a/api-extractor.json b/api-extractor.json index c53bd741..947a828b 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -5,8 +5,8 @@ "newlineKind": "lf", "apiReport": { "enabled": true, - "reportFolder": "/api/", - "reportTempFolder": "/reports/api/", + "reportFolder": "/reports/api/", + "reportTempFolder": "/reports/api/temp/", "reportFileName": "brazilian-utils.api.md" }, "docModel": { @@ -21,18 +21,22 @@ "messages": { "compilerMessageReporting": { "default": { - "logLevel": "warning" + "logLevel": "error" } }, "extractorMessageReporting": { "default": { - "logLevel": "warning" + "logLevel": "error" }, "ae-missing-release-tag": { "logLevel": "none" }, "ae-forgotten-export": { - "logLevel": "none", + "logLevel": "error", + "addToApiReportFile": false + }, + "ae-undocumented": { + "logLevel": "error", "addToApiReportFile": false } }, diff --git a/api/brazilian-utils.api.md b/api/brazilian-utils.api.md deleted file mode 100644 index 361a929c..00000000 --- a/api/brazilian-utils.api.md +++ /dev/null @@ -1,853 +0,0 @@ -## API Report File for "@brazilian-utils/brazilian-utils" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -// @public -export const addBusinessDays: (params: AddBusinessDaysParams) => Date | null; - -// @public (undocumented) -export type AddBusinessDaysParams = { - date: Date; - days: number; - stateCode?: StateCode; - includeOptional?: boolean; -}; - -// @public (undocumented) -export type AddressInfo = { - cep: string; - state: string; - city: string; - neighborhood: string; - street: string; -}; - -// @public (undocumented) -export type AreaCodeInfo = { - areaCode: number; - stateCode: StateCode; - stateName: StateName; - region: State["regionName"]; -}; - -// @public -export type Bank = { - code: string; - ispb: string; - name: string; -}; - -// @public (undocumented) -export type BoletoInfo = { - amount: number; - expirationDate: Date | null; - bankCode: string; - type?: "arrecadacao"; - segment?: number; - value?: number; - hasEffectiveValue?: boolean; -}; - -// @public -export const capitalize: (value: string, options?: CapitalizeOptions) => string; - -// @public (undocumented) -export type CapitalizeOptions = { - lowerCaseWords?: string[]; - upperCaseWords?: string[]; -}; - -// @public -export type Cbo = { - code: string; - title: string; -}; - -// @public (undocumented) -export type CepAddressInfo = { - cep: string; - logradouro: string; - complemento: string; - bairro: string; - localidade: string; - uf: string; - ibge?: string; - gia?: string; - ddd?: string; - siafi?: string; -}; - -// @public (undocumented) -export type CepProvider = "viacep" | "widenet" | "brasilapi"; - -// @public (undocumented) -export type Certidao = { - registryCns: string; - acervo: string; - service: string; - year: number; - type: CertidaoType; - typeCode: number; - book: string; - page: string; - term: string; - checkDigits: string; -}; - -// @public (undocumented) -export type CertidaoType = (typeof CERTIDAO_TYPES)[number]; - -// @public -export type Cfop = { - code: string; - description: string; -}; - -// @public -export type Cnae = { - code: string; - description: string; -}; - -// @public -export const convertCurrencyToWords: (value: number, options?: ConvertCurrencyToWordsOptions) => string; - -// @public (undocumented) -export type ConvertCurrencyToWordsOptions = { - case?: WordsCase; -}; - -// @public -export const convertDateToWords: (value: Date | string, options?: ConvertDateToWordsOptions) => string; - -// @public (undocumented) -export type ConvertDateToWordsOptions = { - case?: WordsCase; - style?: "full" | "month"; - weekday?: boolean; -}; - -// @public -export const convertLicensePlateToMercosul: (value: string) => string; - -// @public -export const convertNumberToWords: (value: number, options?: ConvertNumberToWordsOptions) => string; - -// @public (undocumented) -export type ConvertNumberToWordsOptions = { - gender?: NumberToWordsGender; - case?: WordsCase; -}; - -// @public -export const differenceInBusinessDays: (params: DifferenceInBusinessDaysParams) => number | null; - -// @public (undocumented) -export type DifferenceInBusinessDaysParams = { - from: Date; - to: Date; - stateCode?: StateCode; - includeOptional?: boolean; -}; - -// @public -export const formatBoleto: (value: string | number, options?: FormatBoletoOptions) => string; - -// @public (undocumented) -export type FormatBoletoOptions = Pick; - -// @public -export const formatCaepf: (value: string | number, options?: FormatCaepfOptions) => string; - -// @public (undocumented) -export type FormatCaepfOptions = Pick; - -// @public -export const formatCei: (value: string | number, options?: FormatCeiOptions) => string; - -// @public (undocumented) -export type FormatCeiOptions = Pick; - -// @public -const formatCep: (value: string | number, options?: FormatCepOptions) => string; -export { formatCep as formatCEP } -export { formatCep } - -// @public (undocumented) -export type FormatCepOptions = Pick; - -// @public -export const formatCertidao: (value: string | number, options?: FormatCertidaoOptions) => string; - -// @public (undocumented) -export type FormatCertidaoOptions = Pick; - -// @public -export const formatCnae: (value: string | number) => string; - -// @public -export const formatCnh: (value: string | number, options?: FormatCnhOptions) => string; - -// @public (undocumented) -export type FormatCnhOptions = Pick; - -// @public -export const formatCno: (value: string | number, options?: FormatCnoOptions) => string; - -// @public (undocumented) -export type FormatCnoOptions = Pick; - -// @public -const formatCnpj: (value: string | number, options?: FormatCnpjOptions) => string; -export { formatCnpj as formatCNPJ } -export { formatCnpj } - -// @public (undocumented) -export type FormatCnpjOptions = Pick & { - version?: 1 | 2; - obfuscate?: boolean; -}; - -// @public -export const formatCns: (value: string | number, options?: FormatCnsOptions) => string; - -// @public (undocumented) -export type FormatCnsOptions = Pick; - -// @public -const formatCpf: (value: string | number, options?: FormatCpfOptions) => string; -export { formatCpf as formatCPF } -export { formatCpf } - -// @public (undocumented) -export type FormatCpfOptions = Pick & { - obfuscate?: boolean; -}; - -// @public -export const formatCurrency: (value: string | number, options?: FormatCurrencyOptions) => string; - -// @public (undocumented) -export type FormatCurrencyOptions = { - symbol?: boolean; - precision?: number; -}; - -// @public -export const formatIban: (value: string) => string; - -// @public -export const formatLegalNature: (value: string | number) => string; - -// @public -export const formatLicensePlate: (value: string) => string; - -// @public -export const formatNcm: (value: string | number) => string; - -// @public -export const formatNfeKey: (value: string) => string; - -// @public -export const formatPassport: (passport: string) => string; - -// @public -export const formatPhone: (value: string | number, options?: FormatPhoneOptions) => string; - -// @public (undocumented) -export type FormatPhoneOptions = { - mask?: PhoneMask; -}; - -// @public -export const formatPis: (value: string | number, options?: FormatPisOptions) => string; - -// @public (undocumented) -export type FormatPisOptions = Pick; - -// @public -export const formatProcessoJuridico: (value: string | number, options?: FormatProcessoJuridicoOptions) => string; - -// @public (undocumented) -export type FormatProcessoJuridicoOptions = Pick; - -// @public -export const formatVoterId: (value: string | number) => string; - -// @public -export const generateBoleto: (options?: GenerateBoletoOptions) => string; - -// @public (undocumented) -export type GenerateBoletoOptions = { - type?: "bancario" | "arrecadacao"; -}; - -// @public -export const generateCep: () => string; - -// @public -export const generateCnh: () => string; - -// @public -const generateCnpj: (version?: 1 | 2) => string; -export { generateCnpj as generateCNPJ } -export { generateCnpj } - -// @public -const generateCpf: (state?: StateCode) => string; -export { generateCpf as generateCPF } -export { generateCpf } - -// @public -export const generateLegalNature: () => string; - -// @public -export const generateLicensePlate: (format?: GenerateLicensePlateFormat) => string; - -// @public (undocumented) -export type GenerateLicensePlateFormat = LicensePlateFormat; - -// @public -export const generatePassport: () => string; - -// @public -export const generatePhone: (type?: GeneratePhoneType) => string; - -// @public (undocumented) -export type GeneratePhoneType = "mobile" | "landline" | "service"; - -// @public -export const generatePis: () => string; - -// @public -export const generatePixPayload: (params: GeneratePixPayloadParams) => string | null; - -// @public (undocumented) -export type GeneratePixPayloadParams = { - key?: string; - url?: string; - merchantName: string; - merchantCity: string; - amount?: number; - txid?: string; - description?: string; -}; - -// @public -export const generateProcessoJuridico: (options?: GenerateProcessoJuridicoOptions) => string | null; - -// @public (undocumented) -export type GenerateProcessoJuridicoOptions = { - year?: number; - court?: number; -}; - -// @public -export const generateVoterId: (state?: StateCode | "ZZ") => string; - -// @public -export const getAddressInfoByCep: (cep: string | number, options?: GetAddressInfoByCepOptions) => Promise; - -// @public (undocumented) -export class GetAddressInfoByCepError extends Error { - constructor(message: string); -} - -// @public (undocumented) -export class GetAddressInfoByCepNotFoundError extends GetAddressInfoByCepError { - constructor(message: string); -} - -// @public (undocumented) -export type GetAddressInfoByCepOptions = { - providers?: CepProvider[]; -}; - -// @public (undocumented) -export class GetAddressInfoByCepServiceError extends GetAddressInfoByCepError { - constructor(message: string); -} - -// @public (undocumented) -export class GetAddressInfoByCepValidationError extends GetAddressInfoByCepError { - constructor(message: string); -} - -// @public -export const getAreaCodeInfo: (areaCode: string | number) => AreaCodeInfo | null; - -// @public -export const getAreaCodesByState: (stateCode: string) => number[]; - -// @public -export const getBankByCode: (code: string | number) => Bank | null; - -// @public -export const getBankByIspb: (value: string | number) => Bank | null; - -// @public -export const getBanks: () => Bank[]; - -// @public -export const getBoletoInfo: (value: string, options?: GetBoletoInfoOptions) => BoletoInfo | undefined; - -// @public (undocumented) -export type GetBoletoInfoOptions = { - referenceDate?: Date; -}; - -// @public -export const getCbo: (value: string | number) => Cbo | null; - -// @public -export const getCepInfoByAddress: (input: GetCepInfoByAddressOptions) => Promise; - -// @public (undocumented) -export class GetCepInfoByAddressError extends Error { - constructor(message: string); -} - -// @public (undocumented) -export class GetCepInfoByAddressNotFoundError extends GetCepInfoByAddressError { - constructor(message: string); -} - -// @public (undocumented) -export type GetCepInfoByAddressOptions = { - federalUnit: string; - city: string; - street: string; -}; - -// @public (undocumented) -export class GetCepInfoByAddressValidationError extends GetCepInfoByAddressError { - constructor(message: string); -} - -// @public -export const getCfop: (value: string | number) => Cfop | null; - -// @public (undocumented) -export const getCities: (state?: StateCode) => string[]; - -// @public -export const getCnae: (value: string | number) => Cnae | null; - -// @public -export const getFormatLicensePlate: (value: string) => LicensePlateFormat | null; - -// @public -export function getHolidays(year: number): Holiday[]; - -// @public (undocumented) -export function getHolidays(options: GetHolidaysOptions): Holiday[]; - -// @public (undocumented) -export type GetHolidaysOptions = { - year: number; - stateCode?: StateCode; -}; - -// @public -export const getLegalNature: (value: string | number) => LegalNature | null; - -// @public -export const getLegalNatures: () => Record; - -// @public -export const getMunicipalities: (stateCode?: string) => Municipality[]; - -// @public -export const getMunicipality: (options: GetMunicipalityOptions) => Promise<[string, string] | null | string>; - -// @public -export const getMunicipalityByCode: (code: string | number) => Municipality | null; - -// @public (undocumented) -export type GetMunicipalityByCodeOptions = { - code: string; -}; - -// @public (undocumented) -export type GetMunicipalityByNameOptions = { - municipalityName: string; - uf: string; -}; - -// @public (undocumented) -export type GetMunicipalityOptions = GetMunicipalityByCodeOptions | GetMunicipalityByNameOptions; - -// @public -export const getStateByIbgeCode: (code: string | number) => State | null; - -// @public -export const getStateCodeByName: (name: string) => StateCode | null; - -// @public -export const getStateNameByCode: (code: string) => StateName | null; - -// @public -export const getStates: () => State[]; - -// @public -export const getTimezoneByState: (stateCode: string) => string | null; - -// @public (undocumented) -export type Holiday = { - name: string; - date: Date; - type: HolidayType; -}; - -// @public (undocumented) -export type HolidayType = "national" | "state" | "optional" | "religious"; - -// @public (undocumented) -export type Iban = { - countryCode: "BR"; - checkDigits: string; - bankIspb: string; - branch: string; - account: string; - accountType: "C" | "P"; - owner: string; -}; - -// @public -export const isBusinessDay: (value: Date, options?: IsBusinessDayOptions) => boolean; - -// @public (undocumented) -export type IsBusinessDayOptions = { - stateCode?: StateCode; - includeOptional?: boolean; -}; - -// @public -export const isHoliday: (options?: IsHolidayOptions) => boolean; - -// @public (undocumented) -export type IsHolidayOptions = { - targetDate: Date; - stateCode?: StateCode; -}; - -// @public -export const isValidBankAccount: (params: IsValidBankAccountOptions) => boolean; - -// @public (undocumented) -export type IsValidBankAccountOptions = { - bankCode: string; - agency: string; - account: string; - digit: string; -}; - -// @public @deprecated (undocumented) -export type IsValidBankAccountParams = IsValidBankAccountOptions; - -// @public -export const isValidBoleto: (value: string) => boolean; - -// @public -export const isValidCaepf: (value: string | number) => boolean; - -// @public -export const isValidCbo: (value: string | number) => boolean; - -// @public -export const isValidCei: (value: string | number) => boolean; - -// @public -const isValidCep: (cep: string | number) => boolean; -export { isValidCep as isValidCEP } -export { isValidCep } - -// @public -export const isValidCertidao: (value: string | number, options?: IsValidCertidaoOptions) => boolean; - -// @public (undocumented) -export type IsValidCertidaoOptions = { - accept?: CertidaoType[]; -}; - -// @public -export const isValidCfop: (value: string | number) => boolean; - -// @public -export const isValidCnae: (value: string | number) => boolean; - -// @public -export const isValidCnh: (value: string) => boolean; - -// @public -export const isValidCno: (value: string | number) => boolean; - -// @public -const isValidCnpj: (cnpj: string, options?: IsValidCnpjOptions) => boolean; -export { isValidCnpj as isValidCNPJ } -export { isValidCnpj } - -// @public (undocumented) -export type IsValidCnpjOptions = { - version?: 1 | 2; -}; - -// @public -export const isValidCns: (value: string | number) => boolean; - -// @public -const isValidCpf: (cpf: string) => boolean; -export { isValidCpf as isValidCPF } -export { isValidCpf } - -// @public -export const isValidCreditCard: (value: string | number) => boolean; - -// @public -export const isValidCsosn: (value: string | number) => boolean; - -// @public -export const isValidCst: (value: string | number, options?: IsValidCstOptions) => boolean; - -// @public -export type IsValidCstOptions = { - tax?: "icms" | "ipi" | "pis" | "cofins"; -}; - -// @public -export const isValidEmail: (value: string) => boolean; - -// @public -export const isValidIban: (value: string) => boolean; - -// @public -const isValidIe: (stateCode: StateCode, ie: string) => boolean; -export { isValidIe as isValidIE } -export { isValidIe } - -// @public -export const isValidLandlinePhone: (value: string) => boolean; - -// @public -export const isValidLegalNature: (code: string) => boolean; - -// @public -export const isValidLicensePlate: (value: string) => boolean; - -// @public -export const isValidMobilePhone: (value: string, options?: IsValidMobilePhoneOptions) => boolean; - -// @public (undocumented) -export type IsValidMobilePhoneOptions = { - version?: PhoneVersion; -}; - -// @public -export const isValidNcm: (value: string | number) => boolean; - -// @public -export const isValidNfeKey: (value: string) => boolean; - -// @public -export const isValidPassport: (passport: string | number) => boolean; - -// @public -export const isValidPhone: (value: string, options?: IsValidPhoneOptions) => boolean; - -// @public (undocumented) -export type IsValidPhoneOptions = { - version?: PhoneVersion; - accept?: PhoneType[]; -}; - -// @public -const isValidPis: (pis: string) => boolean; -export { isValidPis as isValidPIS } -export { isValidPis } - -// @public -export const isValidPixKey: (value: string, options?: IsValidPixKeyOptions) => boolean; - -// @public (undocumented) -export type IsValidPixKeyOptions = { - accept?: PixKeyType[]; -}; - -// @public -export const isValidPixPayload: (value: string) => boolean; - -// @public -export const isValidProcessoJuridico: (value: string) => boolean; - -// @public -export const isValidRegistroProfissional: (value: string, options: IsValidRegistroProfissionalOptions) => boolean; - -// @public (undocumented) -export type IsValidRegistroProfissionalOptions = { - council: RegistroProfissionalCouncil; - stateCode?: StateCode; -}; - -// @public -export const isValidRenavam: (renavam: string | number) => boolean; - -// @public -export const isValidServicePhone: (value: string) => boolean; - -// @public -export const isValidVin: (value: string) => boolean; - -// @public -export const isValidVoterId: (value: string) => boolean; - -// @public -export type LegalNature = { - code: string; - description: string; -}; - -// @public (undocumented) -export type LicensePlateFormat = "LLLNNNN" | "LLLNLNN"; - -// @public -export type Municipality = { - code: string; - name: string; - stateCode: StateCode; -}; - -// @public (undocumented) -export type NfeKey = { - state: StateCode; - year: number; - month: number; - taxId: string; - model: NfeKeyModel; - series: number; - number: number; - emissionType: number; - code: string; - checkDigit: number; -}; - -// @public (undocumented) -export type NumberToWordsGender = "masculine" | "feminine"; - -// @public -export const parseBoleto: (value: string | number) => string; - -// @public -export const parseCep: (value: string | number) => string; - -// @public -export const parseCertidao: (value: string | number) => Certidao | null; - -// @public -export const parseCnh: (value: string | number) => string; - -// @public -export const parseCnpj: (value: string | number, options?: ParseCnpjOptions) => string; - -// @public (undocumented) -export type ParseCnpjOptions = Pick; - -// @public -export const parseCpf: (value: string | number) => string; - -// @public -export const parseCurrency: (value: string, options?: ParseCurrencyOptions) => number; - -// @public (undocumented) -export type ParseCurrencyOptions = { - precision?: number; -}; - -// @public -export const parseIban: (value: string) => Iban | null; - -// @public -export const parseLegalNature: (value: string | number) => string; - -// @public -export const parseLicensePlate: (value: string) => string; - -// @public -export const parseNfeKey: (value: string) => NfeKey | null; - -// @public -export const parsePassport: (passport: string) => string; - -// @public -export const parsePhone: (value: string | number) => string; - -// @public -export const parsePis: (value: string | number) => string; - -// @public -export const parsePixKey: (value: string) => PixKey | null; - -// @public -export const parsePixPayload: (value: string) => PixPayload | null; - -// @public -export const parseProcessoJuridico: (value: string | number) => string; - -// @public -export const parseVoterId: (value: string | number) => string; - -// @public (undocumented) -export type PhoneMask = "auto" | "e164" | "international" | "service" | NationalMask; - -// @public (undocumented) -export type PhoneType = "mobile" | "landline" | "service"; - -// @public (undocumented) -export type PhoneVersion = 1 | 2; - -// @public (undocumented) -export type PixKey = { - type: PixKeyType; - value: string; -}; - -// @public (undocumented) -export type PixKeyType = "cpf" | "cnpj" | "email" | "phone" | "evp"; - -// @public (undocumented) -export type PixPayload = { - key?: string; - url?: string; - description?: string; - merchantName: string; - merchantCity: string; - amount?: number; - txid?: string; - pointOfInitiation?: PixPointOfInitiation; -}; - -// @public (undocumented) -export type PixPointOfInitiation = "static" | "dynamic"; - -// @public -export type RegistroProfissionalCouncil = "OAB" | "CRM" | "CRO" | "CRP" | "CRC"; - -// @public -export const removeAccents: (value: string) => string; - -// @public (undocumented) -export type State = (typeof DATA)[number]; - -// @public (undocumented) -export type StateCode = (typeof DATA)[number]["code"]; - -// @public (undocumented) -export type StateName = (typeof DATA)[number]["name"]; - -// @public -export type WordsCase = "lower" | "sentence" | "upper"; - -// (No @packageDocumentation comment for this package) - -``` diff --git a/package.json b/package.json index f270e303..b6f7bc7e 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "check:duplication": "jscpd", "check:unused": "knip", "test:mutation": "stryker run", - "check:api": "api-extractor run --local --verbose", + "check:api": "npm run build && api-extractor run --local --verbose", "check:commits": "commitlint --from origin/main --to HEAD --verbose", "check:lockfile": "lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https --validate-integrity", "build:data": "node ./scripts/data.ts", diff --git a/scripts/banks.ts b/scripts/banks.ts index c5b3cbe5..b1e45571 100644 --- a/scripts/banks.ts +++ b/scripts/banks.ts @@ -72,13 +72,17 @@ const fetchFromBacen = async (): Promise => { throw new Error(`Bacen STR participants request failed with status ${response.status}`); } - const text = (await response.text()).replace(/^\uFEFF/, ""); + const body = await response.text(); + const text = body.replace(/^\uFEFF/, ""); const [, ...rows] = text.split(/\r\n|\n/).filter((line) => line.length > 0); const banks: BankRow[] = []; for (const row of rows) { - const [ispb, , code, , , name] = parseCsvLine(row); + const fields = parseCsvLine(row); + const ispb = fields[0]; + const code = fields[2]; + const name = fields[5]; if ( ispb === undefined || diff --git a/scripts/cfop.ts b/scripts/cfop.ts index cc3c2a22..e2380e0f 100644 --- a/scripts/cfop.ts +++ b/scripts/cfop.ts @@ -46,7 +46,7 @@ const main = async (): Promise => { const data: Record = {}; for (const line of csv.split("\n")) { - const match = line.match(/^(\d{4});"(.*)"\s*$/); + const match = /^(\d{4});"(.*)"\s*$/.exec(line); if (!match) continue; diff --git a/scripts/cities.ts b/scripts/cities.ts index 11a32d7d..00a54427 100644 --- a/scripts/cities.ts +++ b/scripts/cities.ts @@ -71,22 +71,18 @@ const main = async (): Promise => { throw new Error("IBGE municipalities payload is not an array of city entries"); } - const byState = json.reduce( - (acc, city) => { - const stateInitials = - city.microrregiao?.mesorregiao?.UF?.sigla ?? - city["regiao-imediata"]?.["regiao-intermediaria"]?.UF?.sigla; + const byState: Record = {}; - if (stateInitials === undefined || stateInitials === "") return acc; + for (const city of json) { + const stateInitials = + city.microrregiao?.mesorregiao?.UF?.sigla ?? + city["regiao-imediata"]?.["regiao-intermediaria"]?.UF?.sigla; - const cityNames = (acc[stateInitials] ??= []); + if (stateInitials === undefined || stateInitials === "") continue; - cityNames.push([city.nome, String(city.id)]); - - return acc; - }, - {} as Record, - ); + byState[stateInitials] ??= []; + byState[stateInitials].push([city.nome, String(city.id)]); + } const sortedEntries = Object.entries(byState) .sort(([a], [b]) => a.localeCompare(b)) diff --git a/scripts/data.ts b/scripts/data.ts index 1066217f..52c40380 100644 --- a/scripts/data.ts +++ b/scripts/data.ts @@ -6,16 +6,16 @@ import { resolve } from "node:path"; const scriptsDir = import.meta.dirname; const run = (command: string, args: string[]): Promise => - new Promise((resolveExit) => { + new Promise((_resolve) => { const child = spawn(command, args, { stdio: "inherit", }); child.on("close", (code) => { - resolveExit(code); + _resolve(code); }); child.on("error", () => { - resolveExit(1); + _resolve(1); }); }); diff --git a/scripts/legal-natures.ts b/scripts/legal-natures.ts index c06f337c..c5c40428 100644 --- a/scripts/legal-natures.ts +++ b/scripts/legal-natures.ts @@ -39,14 +39,14 @@ const inflateStreams = (pdf: Buffer): string[] => { while (cursor < pdf.length) { const start = pdf.indexOf("stream", cursor); - if (start < 0) break; + if (start === -1) break; let contentStart = start + "stream".length; if (pdf[contentStart] === 0x0d) contentStart += 1; if (pdf[contentStart] === 0x0a) contentStart += 1; const end = pdf.indexOf("endstream", contentStart); - if (end < 0) break; + if (end === -1) break; try { streams.push(inflateSync(pdf.subarray(contentStart, end)).toString("latin1")); @@ -151,7 +151,7 @@ const parseLegalNatures = (lines: string[]): Record => { const legalNatures: Record = {}; for (const line of lines) { - const match = line.match(/^\s*(\d{3})-(\d)\s*-\s*(.+?)\s*$/); + const match = /^\s*(\d{3})-(\d)\s*-\s*(.+?)\s*$/.exec(line); if (!match) continue; const [, codePrefix, codeSuffix, rawDescription] = match; diff --git a/scripts/llms.ts b/scripts/llms.ts index bf401b2a..22214905 100644 --- a/scripts/llms.ts +++ b/scripts/llms.ts @@ -12,11 +12,8 @@ type UtilSection = { description: string; }; -const SLUG_STRIP_PATTERN = new RegExp( - "[\\u2000-\\u206F\\u2E00-\\u2E7F\\\\'!\"#$%&()*+,./:;<=>?@\\[\\]^`{|}~]", - "g", -); -const VARIATION_SELECTOR_PATTERN = new RegExp("\\uFE0F", "g"); +const SLUG_STRIP_PATTERN = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g; +const VARIATION_SELECTOR_PATTERN = /\uFE0F/g; const EMOJI_PATTERN = /[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu; /** @@ -69,7 +66,7 @@ function firstSentence(paragraph: string): string { /\b(e\.g|i\.e)\./gi, (_match, abbr: string) => `${abbr}${ABBREVIATION_PLACEHOLDER}`, ); - const match = protectedText.match(/[\s\S]*?[.!?](?=\s|$)/); + const match = /[\s\S]*?[.!?](?=\s|$)/.exec(protectedText); const sentence = match ? match[0] : protectedText; return sentence.split(ABBREVIATION_PLACEHOLDER).join(".").trim(); diff --git a/scripts/states.ts b/scripts/states.ts index 814d97f7..d226a107 100644 --- a/scripts/states.ts +++ b/scripts/states.ts @@ -35,6 +35,9 @@ const isState = (value: unknown): value is State => "nome" in value.regiao && typeof value.regiao.nome === "string"; +const union = (values: string[]): string => + values.map((value) => `| ${JSON.stringify(value)}`).join(" "); + const main = async (): Promise => { const response = await fetchWithRetry( "https://servicodados.ibge.gov.br/api/v1/localidades/estados", @@ -64,20 +67,34 @@ const main = async (): Promise => { await writeFile( resolve(scriptsDir, "..", "./src/_internals/constants/states.ts"), - `/** + `/** The two letter code of each Brazilian state, as published by the IBGE. */ +export type StateCode = ${union(states.map((state) => state.code))}; + +/** The name of each Brazilian state, as published by the IBGE. */ +export type StateName = ${union(states.map((state) => state.name))}; + +/** One Brazilian state, as returned by \`getStates\`, \`getStateByIbgeCode\` and the other state utils. */ +export type State = { + /** The two letter code of the state, e.g. \`"SP"\`. */ + readonly code: StateCode; + /** The full name of the state, e.g. \`"São Paulo"\`. */ + readonly name: StateName; + /** The code of the region the state belongs to, e.g. \`"SE"\`. */ + readonly regionCode: "N" | "NE" | "CO" | "SE" | "S"; + /** The full name of the region the state belongs to, e.g. \`"Sudeste"\`. */ + readonly regionName: "Norte" | "Nordeste" | "Centro-Oeste" | "Sudeste" | "Sul"; + /** The 2 digit IBGE code of the Federative Unit ("cUF"), e.g. \`35\`. */ + readonly ibgeCode: number; +}; + +/** * Brazilian states published by the IBGE, sorted by name with \`localeCompare\` in the "pt-BR" * locale. \`ibgeCode\` is the 2-digit IBGE code of the Federative Unit ("cUF"), the same code * found in the first field of every DF-e access key (chave de acesso). * * @see https://servicodados.ibge.gov.br/api/docs/localidades */ -export const DATA = ${JSON.stringify(states)} as const - -export type State = (typeof DATA)[number]; - -export type StateName = (typeof DATA)[number]["name"]; - -export type StateCode = (typeof DATA)[number]["code"];`, +export const DATA: readonly State[] = ${JSON.stringify(states)};`, ); }; diff --git a/scripts/tree-shaking.ts b/scripts/tree-shaking.ts index 9bd66de4..82e89883 100644 --- a/scripts/tree-shaking.ts +++ b/scripts/tree-shaking.ts @@ -341,64 +341,123 @@ const compareSnapshots = (base: Snapshot, head: Snapshot, existing: Measurement) const formatPercent = (value: number): string => `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}%`; +const formatBytes = (bytes: number): string => + Math.abs(bytes) < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`; + +const formatDelta = (bytes: number, percent: number): string => + bytes === 0 + ? "0 B" + : `${bytes > 0 ? "+" : "-"}${formatBytes(Math.abs(bytes))} (${formatPercent(percent)})`; + +const formatCount = (value: number): string => (value > 0 ? `+${value}` : String(value)); + +const MAX_VISIBLE_ROWS = 20; + +const renderRows = (title: string, header: string[], rows: string[]): string[] => { + if (rows.length === 0) return []; + const table = [ + `| ${header.join(" | ")} |`, + `| ${header.map((_column, index) => (index === 0 ? "---" : "---:")).join(" | ")} |`, + ]; + const visible = rows.slice(0, MAX_VISIBLE_ROWS); + const hidden = rows.slice(MAX_VISIBLE_ROWS); + const lines = [`### ${title} (${rows.length})`, "", ...table, ...visible, ""]; + if (hidden.length > 0) { + lines.push( + `
Show the other ${hidden.length}`, + "", + ...table, + ...hidden, + "", + "
", + "", + ); + } + return lines; +}; + +const renderCollapsed = (title: string, header: string[], rows: string[]): string[] => { + if (rows.length === 0) return []; + return [ + `
${title} (${rows.length})`, + "", + `| ${header.join(" | ")} |`, + `| ${header.map((_column, index) => (index === 0 ? "---" : "---:")).join(" | ")} |`, + ...rows, + "", + "
", + "", + ]; +}; + const renderMarkdown = ( base: Snapshot, head: Snapshot, existing: Measurement, result: CompareResult, ): string => { + const grown = result.changed.filter((row) => row.deltaBytes > 0).length; + const shrunk = result.changed.length - grown; + const regressionCount = result.regressions.length + (result.fullImportRegressed ? 1 : 0); + const status = + regressionCount === 0 + ? "✅ **No size regression.**" + : `❌ **${regressionCount} size regression${regressionCount === 1 ? "" : "s"}.**`; + const counts = [ + `${Object.keys(head.exports).length} exports measured`, + grown > 0 ? `${grown} grew` : "", + shrunk > 0 ? `${shrunk} shrank` : "", + result.added.length > 0 ? `${result.added.length} new` : "", + result.removed.length > 0 ? `${result.removed.length} removed` : "", + ].filter((part) => part !== ""); + const lines: string[] = [ "## Tree-shaking report", "", - `Fails when a pre-existing export grows more than ${REGRESSION_PERCENT_THRESHOLD * 100}% and more than ` + - `${REGRESSION_BYTES_THRESHOLD} B, or when importing every export that already existed on the base ` + - `grows more than ${FULL_IMPORT_PERCENT_THRESHOLD * 100}%. New exports never count as a regression.`, + `${status} ${counts.join(", ")}.`, "", - `Pre-existing exports: ${base.full.bytes} B to ${existing.bytes} B (${formatPercent(result.fullDeltaPercent)}, ` + - `gzip ${existing.gzip} B)${result.fullImportRegressed ? ", REGRESSION" : ""}. ` + - `Full import on head: ${head.full.bytes} B (gzip ${head.full.gzip} B).`, + "| | Base | Head | Δ |", + "| --- | ---: | ---: | ---: |", + `| Pre-existing exports, all imported | ${formatBytes(base.full.bytes)} | ${formatBytes(existing.bytes)} (gzip ${formatBytes(existing.gzip)}) | ${result.fullImportRegressed ? "🔴 " : ""}${formatDelta(result.fullDeltaBytes, result.fullDeltaPercent)} |`, + `| Full import | ${formatBytes(base.full.bytes)} | ${formatBytes(head.full.bytes)} (gzip ${formatBytes(head.full.gzip)}) | ${formatDelta(head.full.bytes - base.full.bytes, base.full.bytes === 0 ? 0 : (head.full.bytes - base.full.bytes) / base.full.bytes)} |`, + `| Exports | ${Object.keys(base.exports).length} | ${Object.keys(head.exports).length} | ${formatCount(Object.keys(head.exports).length - Object.keys(base.exports).length)} |`, "", ]; - if (result.changed.length > 0) { - lines.push( - "| name | base | head | delta bytes | delta % | gzip head |", - "| --- | --- | --- | --- | --- | --- |", - ); - for (const row of result.changed) { - const flag = result.regressions.includes(row) ? " (REGRESSION)" : ""; - lines.push( - `| ${row.name}${flag} | ${row.base.bytes} | ${row.head.bytes} | ` + - `${row.deltaBytes > 0 ? "+" : ""}${row.deltaBytes} | ${formatPercent(row.deltaPercent)} | ${row.head.gzip} |`, - ); - } - lines.push(""); - } - - if (result.added.length > 0) { - lines.push("**New exports**", ""); - for (const item of result.added) - lines.push(`- ${item.name}: ${item.bytes} B (gzip ${item.gzip} B)`); - lines.push(""); - } - - if (result.removed.length > 0) { - lines.push("**Removed exports**", ""); - for (const item of result.removed) lines.push(`- ${item.name}: was ${item.bytes} B`); - lines.push(""); - } - - if (result.unchanged.length > 0) { - lines.push( - `
Unchanged exports (${result.unchanged.length})`, - "", - "| name | bytes | gzip |", - "| --- | --- | --- |", - ); - for (const row of result.unchanged) - lines.push(`| ${row.name} | ${row.head.bytes} | ${row.head.gzip} |`); - lines.push("", "
"); - } + const changedRows = result.changed.map((row) => { + const marker = result.regressions.includes(row) ? "🔴" : row.deltaBytes > 0 ? "🟡" : "🟢"; + return `| ${marker} \`${row.name}\` | ${formatBytes(row.base.bytes)} | ${formatBytes(row.head.bytes)} | ${formatDelta(row.deltaBytes, row.deltaPercent)} | ${formatBytes(row.head.gzip)} |`; + }); + lines.push( + ...renderRows("Changed exports", ["Export", "Base", "Head", "Δ", "gzip"], changedRows), + ...renderCollapsed( + "New exports", + ["Export", "Size", "gzip"], + result.added.map( + (item) => `| \`${item.name}\` | ${formatBytes(item.bytes)} | ${formatBytes(item.gzip)} |`, + ), + ), + ...renderCollapsed( + "Removed exports", + ["Export", "Was"], + result.removed.map((item) => `| \`${item.name}\` | ${formatBytes(item.bytes)} |`), + ), + ...renderCollapsed( + "Unchanged exports", + ["Export", "Size", "gzip"], + result.unchanged.map( + (row) => + `| \`${row.name}\` | ${formatBytes(row.head.bytes)} | ${formatBytes(row.head.gzip)} |`, + ), + ), + "
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 ${REGRESSION_PERCENT_THRESHOLD * 100}% and more than ${REGRESSION_BYTES_THRESHOLD} B, or the bundle importing every pre-existing export growing more than ${FULL_IMPORT_PERCENT_THRESHOLD * 100}%. ` + + "🟡 is growth under the threshold and 🟢 is a decrease. New exports never count as a regression. An intentional increase is accepted with the `tree-shaking: accepted` label.", + "", + "
", + ); return lines.join("\n"); }; diff --git a/src/_internals/apply-words-case/apply-words-case.ts b/src/_internals/apply-words-case/apply-words-case.ts index f17887b5..fddb580a 100644 --- a/src/_internals/apply-words-case/apply-words-case.ts +++ b/src/_internals/apply-words-case/apply-words-case.ts @@ -1,4 +1,4 @@ -import type { WordsCase } from "../number-to-words/number-to-words"; +import { type WordsCase } from "../number-to-words/number-to-words"; /** * Applies a `WordsCase` to a "por extenso" string already written out in lowercase. diff --git a/src/_internals/constants/area-codes.ts b/src/_internals/constants/area-codes.ts index 223fc955..faefbc3f 100644 --- a/src/_internals/constants/area-codes.ts +++ b/src/_internals/constants/area-codes.ts @@ -1,4 +1,4 @@ -import type { StateCode } from "./states"; +import { type StateCode } from "./states"; /** * Brazilian DDD (area code) data under the Plano Geral de Numeração. `VALID_AREA_CODES` is kept diff --git a/src/_internals/constants/cities.ts b/src/_internals/constants/cities.ts index 100e7509..caab70e4 100644 --- a/src/_internals/constants/cities.ts +++ b/src/_internals/constants/cities.ts @@ -1,4 +1,4 @@ -import type { StateCode } from "./states"; +import { type StateCode } from "./states"; /** * Brazilian municipalities by state, published by the IBGE. `DATA` holds, for each state, a diff --git a/src/_internals/constants/ibge-uf-codes.ts b/src/_internals/constants/ibge-uf-codes.ts index 99caf18f..a1a09652 100644 --- a/src/_internals/constants/ibge-uf-codes.ts +++ b/src/_internals/constants/ibge-uf-codes.ts @@ -1,4 +1,4 @@ -import type { StateCode } from "./states"; +import { type StateCode } from "./states"; /** * IBGE code of the Federative Unit ("cUF"), keyed by the 2 digit code found in the first diff --git a/src/_internals/constants/states.ts b/src/_internals/constants/states.ts index 88a455fb..e592270e 100644 --- a/src/_internals/constants/states.ts +++ b/src/_internals/constants/states.ts @@ -1,3 +1,77 @@ +/** The two letter code of each Brazilian state, as published by the IBGE. */ +export type StateCode = + | "AC" + | "AL" + | "AP" + | "AM" + | "BA" + | "CE" + | "DF" + | "ES" + | "GO" + | "MA" + | "MT" + | "MS" + | "MG" + | "PA" + | "PB" + | "PR" + | "PE" + | "PI" + | "RJ" + | "RN" + | "RS" + | "RO" + | "RR" + | "SC" + | "SP" + | "SE" + | "TO"; + +/** The name of each Brazilian state, as published by the IBGE. */ +export type StateName = + | "Acre" + | "Alagoas" + | "Amapá" + | "Amazonas" + | "Bahia" + | "Ceará" + | "Distrito Federal" + | "Espírito Santo" + | "Goiás" + | "Maranhão" + | "Mato Grosso" + | "Mato Grosso do Sul" + | "Minas Gerais" + | "Pará" + | "Paraíba" + | "Paraná" + | "Pernambuco" + | "Piauí" + | "Rio de Janeiro" + | "Rio Grande do Norte" + | "Rio Grande do Sul" + | "Rondônia" + | "Roraima" + | "Santa Catarina" + | "São Paulo" + | "Sergipe" + | "Tocantins"; + +/** One Brazilian state, as returned by `getStates`, `getStateByIbgeCode` and the other state utils. */ +export type State = { + /** The two letter code of the state, e.g. `"SP"`. */ + readonly code: StateCode; + /** The full name of the state, e.g. `"São Paulo"`. */ + readonly name: StateName; + /** The code of the region the state belongs to, e.g. `"SE"`. */ + readonly regionCode: "N" | "NE" | "CO" | "SE" | "S"; + /** The full name of the region the state belongs to, e.g. `"Sudeste"`. */ + readonly regionName: "Norte" | "Nordeste" | "Centro-Oeste" | "Sudeste" | "Sul"; + /** The 2 digit IBGE code of the Federative Unit ("cUF"), e.g. `35`. */ + readonly ibgeCode: number; +}; + /** * Brazilian states published by the IBGE, sorted by name with `localeCompare` in the "pt-BR" * locale. `ibgeCode` is the 2-digit IBGE code of the Federative Unit ("cUF"), the same code @@ -5,7 +79,7 @@ * * @see https://servicodados.ibge.gov.br/api/docs/localidades */ -export const DATA = [ +export const DATA: readonly State[] = [ { code: "AC", name: "Acre", regionCode: "N", regionName: "Norte", ibgeCode: 12 }, { code: "AL", name: "Alagoas", regionCode: "NE", regionName: "Nordeste", ibgeCode: 27 }, { code: "AP", name: "Amapá", regionCode: "N", regionName: "Norte", ibgeCode: 16 }, @@ -51,10 +125,4 @@ export const DATA = [ { code: "SP", name: "São Paulo", regionCode: "SE", regionName: "Sudeste", ibgeCode: 35 }, { code: "SE", name: "Sergipe", regionCode: "NE", regionName: "Nordeste", ibgeCode: 28 }, { code: "TO", name: "Tocantins", regionCode: "N", regionName: "Norte", ibgeCode: 17 }, -] as const; - -export type State = (typeof DATA)[number]; - -export type StateName = (typeof DATA)[number]["name"]; - -export type StateCode = (typeof DATA)[number]["code"]; +]; diff --git a/src/_internals/crc16-ccitt/crc16-ccitt.ts b/src/_internals/crc16-ccitt/crc16-ccitt.ts index 30ccac6e..c6d49085 100644 --- a/src/_internals/crc16-ccitt/crc16-ccitt.ts +++ b/src/_internals/crc16-ccitt/crc16-ccitt.ts @@ -28,8 +28,8 @@ export const crc16Ccitt = (value: string): string => { let crc = INITIAL_VALUE; - for (let index = 0; index < bytes.length; index++) { - crc ^= bytes[index] << 8; + for (const byte of bytes) { + crc ^= byte << 8; for (let bit = 0; bit < 8; bit++) { crc = (crc & 0x80_00) === 0 ? (crc << 1) & MASK : ((crc << 1) ^ POLYNOMIAL) & MASK; diff --git a/src/_internals/fetch-sorted-record/fetch-sorted-record.test.ts b/src/_internals/fetch-sorted-record/fetch-sorted-record.test.ts index ef4716bb..b596ca2a 100644 --- a/src/_internals/fetch-sorted-record/fetch-sorted-record.test.ts +++ b/src/_internals/fetch-sorted-record/fetch-sorted-record.test.ts @@ -54,7 +54,9 @@ describe("fetchSortedRecord", () => { async (response) => { const entries: Record = {}; - for (const line of (await response.text()).split("\n")) { + const body = await response.text(); + + for (const line of body.split("\n")) { const [key, value] = line.split(";"); expect(key).toBeDefined(); diff --git a/src/_internals/fetch-with-retry/fetch-with-retry.test.ts b/src/_internals/fetch-with-retry/fetch-with-retry.test.ts index c8efbea6..bab38634 100644 --- a/src/_internals/fetch-with-retry/fetch-with-retry.test.ts +++ b/src/_internals/fetch-with-retry/fetch-with-retry.test.ts @@ -127,13 +127,16 @@ describe("fetchWithRetry", () => { "ETIMEDOUT", ]; - await RETRYABLE_CODES.reduce(async (previous, code) => { - await previous; + const expectEachRetries = async ([code, ...rest]: string[]): Promise => { + if (code === undefined) return; const error = Object.assign(new Error("boom"), { code }); await expectRetrySucceeds(mockFetchRejectingOnceWith(error)); - }, Promise.resolve()); + await expectEachRetries(rest); + }; + + await expectEachRetries(RETRYABLE_CODES); }); it("does not retry when the error code is unknown", async () => { diff --git a/src/_internals/fetch-with-retry/fetch-with-retry.ts b/src/_internals/fetch-with-retry/fetch-with-retry.ts index a61f7823..bbca2f47 100644 --- a/src/_internals/fetch-with-retry/fetch-with-retry.ts +++ b/src/_internals/fetch-with-retry/fetch-with-retry.ts @@ -64,13 +64,17 @@ const wait = (ms: number): Promise => setTimeout(resolve, ms); }); +type Attempt = { + retries: number; + retryDelayMs: number; + attempt: number; + lastError?: unknown; +}; + const attemptFetch = async ( input: string | URL | Request, init: RequestInit, - retries: number, - retryDelayMs: number, - attempt: number, - lastError?: unknown, + { retries, retryDelayMs, attempt, lastError }: Attempt, ): Promise => { if (attempt > retries) { throw lastError; @@ -85,7 +89,12 @@ const attemptFetch = async ( await wait(retryDelayMs * (attempt + 1)); - return attemptFetch(input, init, retries, retryDelayMs, attempt + 1, error); + return attemptFetch(input, init, { + retries, + retryDelayMs, + attempt: attempt + 1, + lastError: error, + }); } }; @@ -108,4 +117,4 @@ const attemptFetch = async ( export const fetchWithRetry = ( input: string | URL | Request, { retries = 2, retryDelayMs = 250, ...init }: FetchWithRetryOptions = {}, -): Promise => attemptFetch(input, init, retries, retryDelayMs, 0); +): Promise => attemptFetch(input, init, { retries, retryDelayMs, attempt: 0 }); diff --git a/src/_internals/is-repeated-digits/is-repeated-digits.ts b/src/_internals/is-repeated-digits/is-repeated-digits.ts index b50912c6..5be496da 100644 --- a/src/_internals/is-repeated-digits/is-repeated-digits.ts +++ b/src/_internals/is-repeated-digits/is-repeated-digits.ts @@ -11,8 +11,5 @@ * isRepeatedDigits(""); // false * ``` */ -export const isRepeatedDigits = (value: string): boolean => { - const firstChar = value[0]; - - return firstChar !== undefined && value === firstChar.repeat(value.length); -}; +export const isRepeatedDigits = (value: string): boolean => + value !== "" && value === value.charAt(0).repeat(value.length); diff --git a/src/_internals/number-to-words/number-to-words.ts b/src/_internals/number-to-words/number-to-words.ts index 3a11ac9f..c7e1f9e0 100644 --- a/src/_internals/number-to-words/number-to-words.ts +++ b/src/_internals/number-to-words/number-to-words.ts @@ -9,6 +9,7 @@ import { ZERO_WORD, } from "../constants/number-words"; +/** The grammatical gender `convertNumberToWords` agrees the number it writes out with. */ export type NumberToWordsGender = "masculine" | "feminine"; /** diff --git a/src/_internals/test/globals.d.ts b/src/_internals/test/globals.d.ts index f28a888f..acb9a676 100644 --- a/src/_internals/test/globals.d.ts +++ b/src/_internals/test/globals.d.ts @@ -1,6 +1,6 @@ declare const Deno: { readonly env: { - get(key: string): string | undefined; + get: (key: string) => string | undefined; }; readonly test: (options: { name: string; diff --git a/src/_internals/test/runtime-deno.ts b/src/_internals/test/runtime-deno.ts index ac756e51..5d30a779 100644 --- a/src/_internals/test/runtime-deno.ts +++ b/src/_internals/test/runtime-deno.ts @@ -415,7 +415,7 @@ function createExpect(actual: unknown): ExpectResult { name, (...args: unknown[]): void => { try { - matcher.apply(undefined, args); + matcher(...args); } catch { return; } @@ -439,7 +439,7 @@ function createExpect(actual: unknown): ExpectResult { ); const matcher = matcherEntry?.[1]; - return matcher?.apply(undefined, args); + return matcher?.(...args); }, ]), ); @@ -475,13 +475,13 @@ function currentSuiteChain(): Suite[] { return [...suiteStack]; } -type DescribeFunction = ((name: string, callback: TestCallback) => void) & { - skip: (name: string, callback: TestCallback) => void; +type DescribeFunction = ((name: string, callback: () => void) => void) & { + skip: (name: string, callback: () => void) => void; }; let skipDepth = 0; -const runSuite = (name: string, callback: TestCallback): void => { +const runSuite = (name: string, callback: () => void): void => { suiteStack.push({ afterEach: [], beforeEach: [], @@ -489,9 +489,7 @@ const runSuite = (name: string, callback: TestCallback): void => { }); try { - Promise.resolve(callback()).catch((error: unknown) => { - throw error; - }); + callback(); } finally { suiteStack.pop(); } diff --git a/src/add-business-days/add-business-days.ts b/src/add-business-days/add-business-days.ts index c2306a7a..a31d2be2 100644 --- a/src/add-business-days/add-business-days.ts +++ b/src/add-business-days/add-business-days.ts @@ -1,8 +1,9 @@ import { HOLIDAYS_MAX_YEAR, HOLIDAYS_MIN_YEAR } from "../_internals/constants/holidays"; -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { isBusinessDay } from "../is-business-day/is-business-day"; +/** The parameters `addBusinessDays` takes: the date to count from, how many business days to add and which holidays count. */ export type AddBusinessDaysParams = { /** The date to count from. Never mutated: a new `Date` is returned. */ date: Date; @@ -83,7 +84,7 @@ export const addBusinessDays = (params: AddBusinessDaysParams): Date | null => { if (!isSupportedYear(date)) return null; - const result = new Date(date.getTime()); + const result = new Date(date); const hours = result.getHours(); // Stryker disable next-line EqualityOperator: when days is 0, remaining is 0 below and the loop never reads step, so > vs >= here is unobservable diff --git a/src/capitalize/capitalize.ts b/src/capitalize/capitalize.ts index 346ea951..70d238fb 100644 --- a/src/capitalize/capitalize.ts +++ b/src/capitalize/capitalize.ts @@ -1,5 +1,6 @@ import { PREPOSITIONS, SEPARATOR_REGEX, WHITESPACE_REGEX } from "./constants"; +/** Options of `capitalize`. */ export type CapitalizeOptions = { /** Words to keep in lower case when they are not the first word (default: the Portuguese prepositions). */ lowerCaseWords?: string[]; diff --git a/src/convert-currency-to-words/convert-currency-to-words.ts b/src/convert-currency-to-words/convert-currency-to-words.ts index b4c3f43f..eaab6d91 100644 --- a/src/convert-currency-to-words/convert-currency-to-words.ts +++ b/src/convert-currency-to-words/convert-currency-to-words.ts @@ -5,6 +5,7 @@ import { type WordsCase, } from "../_internals/number-to-words/number-to-words"; +/** Options of `convertCurrencyToWords`. */ export type ConvertCurrencyToWordsOptions = { /** Letter case applied to the result: `"lower"` (unchanged), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). Defaults to `"lower"`; an invalid value is ignored and `"lower"` is used instead. */ case?: WordsCase; diff --git a/src/convert-date-to-words/convert-date-to-words.ts b/src/convert-date-to-words/convert-date-to-words.ts index 571e5523..300daf25 100644 --- a/src/convert-date-to-words/convert-date-to-words.ts +++ b/src/convert-date-to-words/convert-date-to-words.ts @@ -2,6 +2,7 @@ import { applyWordsCase } from "../_internals/apply-words-case/apply-words-case" import { MONTH_NAMES, WEEKDAY_NAMES } from "../_internals/constants/number-words"; import { numberToWords, type WordsCase } from "../_internals/number-to-words/number-to-words"; +/** Options of `convertDateToWords`. */ export type ConvertDateToWordsOptions = { /** Letter case applied to the result: `"lower"` (unchanged), `"sentence"` (capitalizes only the first letter) or `"upper"` (uppercases everything, keeping accents). Defaults to `"lower"`; an invalid value is ignored and `"lower"` is used instead. */ case?: WordsCase; diff --git a/src/convert-number-to-words/convert-number-to-words.ts b/src/convert-number-to-words/convert-number-to-words.ts index a89c5c16..61ef7e0d 100644 --- a/src/convert-number-to-words/convert-number-to-words.ts +++ b/src/convert-number-to-words/convert-number-to-words.ts @@ -6,6 +6,7 @@ import { type WordsCase, } from "../_internals/number-to-words/number-to-words"; +/** Options of `convertNumberToWords`. */ export type ConvertNumberToWordsOptions = { /** Grammatical gender used to agree "um/dois" and the hundreds group ("duzentos/duzentas", etc.) with the noun the number qualifies. Defaults to `"masculine"`. */ gender?: NumberToWordsGender; diff --git a/src/difference-in-business-days/difference-in-business-days.ts b/src/difference-in-business-days/difference-in-business-days.ts index 031d8330..ad93147e 100644 --- a/src/difference-in-business-days/difference-in-business-days.ts +++ b/src/difference-in-business-days/difference-in-business-days.ts @@ -1,8 +1,9 @@ import { HOLIDAYS_MAX_YEAR, HOLIDAYS_MIN_YEAR } from "../_internals/constants/holidays"; -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { isBusinessDay } from "../is-business-day/is-business-day"; +/** The parameters `differenceInBusinessDays` takes: the two dates to count between and which holidays count. */ export type DifferenceInBusinessDaysParams = { /** The date to count from. Counted as a business day when it is one; never mutated. */ from: Date; diff --git a/src/format-boleto/format-boleto.ts b/src/format-boleto/format-boleto.ts index 9a5158a0..ac435661 100644 --- a/src/format-boleto/format-boleto.ts +++ b/src/format-boleto/format-boleto.ts @@ -1,10 +1,14 @@ import { ARRECADACAO_LINE_LENGTH, ARRECADACAO_PRODUCT } from "../_internals/constants/arrecadacao"; -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { ARRECADACAO_PATTERN, BANCARIO_PATTERN } from "./constants"; -export type FormatBoletoOptions = Pick; +/** Options of `formatBoleto`. */ +export type FormatBoletoOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; /** * Formats a given value as a Brazilian boleto. diff --git a/src/format-caepf/format-caepf.ts b/src/format-caepf/format-caepf.ts index 36d92cb6..24b133f8 100644 --- a/src/format-caepf/format-caepf.ts +++ b/src/format-caepf/format-caepf.ts @@ -1,9 +1,13 @@ -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { PATTERN } from "./constants"; -export type FormatCaepfOptions = Pick; +/** Options of `formatCaepf`. */ +export type FormatCaepfOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; /** * Formats a CAEPF (Cadastro de Atividade Econômica da Pessoa Física) number according to the diff --git a/src/format-cei/format-cei.ts b/src/format-cei/format-cei.ts index c89e271e..f2c8acc0 100644 --- a/src/format-cei/format-cei.ts +++ b/src/format-cei/format-cei.ts @@ -1,9 +1,13 @@ -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { PATTERN } from "./constants"; -export type FormatCeiOptions = Pick; +/** Options of `formatCei`. */ +export type FormatCeiOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; /** * Formats a CEI (Cadastro Específico do INSS) number according to the official mask. diff --git a/src/format-cep/format-cep.ts b/src/format-cep/format-cep.ts index 17e37fdb..5ee15c5d 100644 --- a/src/format-cep/format-cep.ts +++ b/src/format-cep/format-cep.ts @@ -1,8 +1,12 @@ -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -export type FormatCepOptions = Pick; +/** Options of `formatCep`. */ +export type FormatCepOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; /** * Formats a given value as a Brazilian postal code (CEP). diff --git a/src/format-certidao/format-certidao.ts b/src/format-certidao/format-certidao.ts index 63501d41..0c8e6895 100644 --- a/src/format-certidao/format-certidao.ts +++ b/src/format-certidao/format-certidao.ts @@ -1,9 +1,13 @@ import { CERTIDAO_PATTERN } from "../_internals/constants/certidao"; -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -export type FormatCertidaoOptions = Pick; +/** Options of `formatCertidao`. */ +export type FormatCertidaoOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; /** * Formats the matrícula of a certidão de registro civil into the printed mask of the diff --git a/src/format-cnh/format-cnh.ts b/src/format-cnh/format-cnh.ts index 1e520368..baf620f4 100644 --- a/src/format-cnh/format-cnh.ts +++ b/src/format-cnh/format-cnh.ts @@ -1,8 +1,12 @@ -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -export type FormatCnhOptions = Pick; +/** Options of `formatCnh`. */ +export type FormatCnhOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; /** * Formats a Brazilian CNH (Carteira Nacional de Habilitação) number. diff --git a/src/format-cno/format-cno.ts b/src/format-cno/format-cno.ts index dbd22622..43821e35 100644 --- a/src/format-cno/format-cno.ts +++ b/src/format-cno/format-cno.ts @@ -1,9 +1,13 @@ -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { PATTERN } from "./constants"; -export type FormatCnoOptions = Pick; +/** Options of `formatCno`. */ +export type FormatCnoOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; /** * Formats a CNO (Cadastro Nacional de Obras) number according to the official mask. diff --git a/src/format-cnpj/format-cnpj.ts b/src/format-cnpj/format-cnpj.ts index 5661bfb5..db4de68a 100644 --- a/src/format-cnpj/format-cnpj.ts +++ b/src/format-cnpj/format-cnpj.ts @@ -1,10 +1,13 @@ -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { OBFUSCATED_PATTERN, PATTERN } from "./constants"; -export type FormatCnpjOptions = Pick & { +/** Options of `formatCnpj`. */ +export type FormatCnpjOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; /** Which CNPJ format to read: `1` numeric only, `2` alphanumeric (default: `1`). */ version?: 1 | 2; /** Whether to hide the first 2 digits and the 2 check digits with `*` (default: `false`). */ diff --git a/src/format-cns/format-cns.ts b/src/format-cns/format-cns.ts index 0c3d224a..803fc487 100644 --- a/src/format-cns/format-cns.ts +++ b/src/format-cns/format-cns.ts @@ -1,8 +1,12 @@ -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -export type FormatCnsOptions = Pick; +/** Options of `formatCns`. */ +export type FormatCnsOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; /** * Formats a CNS (Cartão Nacional de Saúde) number into the common display groups of 3-4-4-4 diff --git a/src/format-cpf/format-cpf.ts b/src/format-cpf/format-cpf.ts index 300bd143..5c74ecc6 100644 --- a/src/format-cpf/format-cpf.ts +++ b/src/format-cpf/format-cpf.ts @@ -1,9 +1,12 @@ -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { OBFUSCATED_PATTERN, PATTERN } from "./constants"; -export type FormatCpfOptions = Pick & { +/** Options of `formatCpf`. */ +export type FormatCpfOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; /** Whether to hide the first 3 digits and the 2 check digits with `*` (default: `false`). */ obfuscate?: boolean; }; diff --git a/src/format-currency/format-currency.ts b/src/format-currency/format-currency.ts index 14313911..0cc7c717 100644 --- a/src/format-currency/format-currency.ts +++ b/src/format-currency/format-currency.ts @@ -1,6 +1,7 @@ import { DEFAULT_PRECISION, clampPrecision } from "../_internals/clamp-precision/clamp-precision"; import { parseDecimal } from "../_internals/parse-decimal/parse-decimal"; +/** Options of `formatCurrency`. */ export type FormatCurrencyOptions = { /** Whether to prefix the result with the "R$" currency symbol (default: `false`). */ symbol?: boolean; diff --git a/src/format-phone/format-phone.ts b/src/format-phone/format-phone.ts index ada10e0f..185504fc 100644 --- a/src/format-phone/format-phone.ts +++ b/src/format-phone/format-phone.ts @@ -10,17 +10,12 @@ import { normalizePhone } from "../_internals/normalize-phone/normalize-phone"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { stripPhoneCountryCode } from "../_internals/strip-phone-country-code/strip-phone-country-code"; import { isValidServicePhone } from "../is-valid-service-phone/is-valid-service-phone"; -import { - INTERNATIONAL_MASK, - INTERNATIONAL_PREFIX, - LENGTH, - MASK, - type NationalMask, - SERVICE_MASK, -} from "./constants"; +import { INTERNATIONAL_MASK, INTERNATIONAL_PREFIX, LENGTH, MASK, SERVICE_MASK } from "./constants"; -export type PhoneMask = "auto" | "e164" | "international" | "service" | NationalMask; +/** The masks `formatPhone` can apply. */ +export type PhoneMask = "auto" | "e164" | "international" | "service" | "sn" | "nanp"; +/** Options of `formatPhone`. */ export type FormatPhoneOptions = { /** Which mask to apply, or `"auto"` to pick one from the value (default: `"sn"`). */ mask?: PhoneMask; diff --git a/src/format-pis/format-pis.ts b/src/format-pis/format-pis.ts index cad6ac88..a0ecfca4 100644 --- a/src/format-pis/format-pis.ts +++ b/src/format-pis/format-pis.ts @@ -1,8 +1,12 @@ -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -export type FormatPisOptions = Pick; +/** Options of `formatPis`. */ +export type FormatPisOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; /** * Formats a PIS (Programa de Integração Social) number according to the specified pattern. diff --git a/src/format-processo-juridico/format-processo-juridico.ts b/src/format-processo-juridico/format-processo-juridico.ts index 864b32bd..407be631 100644 --- a/src/format-processo-juridico/format-processo-juridico.ts +++ b/src/format-processo-juridico/format-processo-juridico.ts @@ -1,8 +1,12 @@ -import { type FormatParams, format } from "../_internals/format/format"; +import { format } from "../_internals/format/format"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -export type FormatProcessoJuridicoOptions = Pick; +/** Options of `formatProcessoJuridico`. */ +export type FormatProcessoJuridicoOptions = { + /** Whether to left pad the value with zeros up to the number of slots in the pattern (default: `false`). */ + pad?: boolean; +}; /** * Formats a legal process number (processo jurídico) according to a specific pattern. diff --git a/src/generate-boleto/generate-boleto.ts b/src/generate-boleto/generate-boleto.ts index 5ed8ef7e..4abe0ae0 100644 --- a/src/generate-boleto/generate-boleto.ts +++ b/src/generate-boleto/generate-boleto.ts @@ -3,6 +3,7 @@ import { generateRandomNumber } from "../_internals/generate-random-number/gener import { mod10 } from "../_internals/mod10/mod10"; import { mod11 } from "../_internals/mod11/mod11"; +/** Options of `generateBoleto`. */ export type GenerateBoletoOptions = { /** Which kind of bank slip to generate (default: `"bancario"`). */ type?: "bancario" | "arrecadacao"; diff --git a/src/generate-cnpj/generate-cnpj.test.ts b/src/generate-cnpj/generate-cnpj.test.ts index c77a9e0f..b176c6f3 100644 --- a/src/generate-cnpj/generate-cnpj.test.ts +++ b/src/generate-cnpj/generate-cnpj.test.ts @@ -3,6 +3,29 @@ import { describe, expect, test } from "../_internals/test/runtime"; import { isValidCnpj } from "../is-valid-cnpj/is-valid-cnpj"; import { generateCnpj } from "./generate-cnpj"; +const REMAINDER_TWO_DRAWS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; + +const generateWithForcedDraws = ( + draws: number[], + alphabetSize: number, + generate: () => string, +): string => { + const originalRandom = Math.random; + let call = 0; + + Math.random = () => { + const draw = draws[call]; + call += 1; + return (draw + 0.5) / alphabetSize; + }; + + try { + return generate(); + } finally { + Math.random = originalRandom; + } +}; + describe("generateCnpj", () => { describe("version 1 (numeric)", () => { test("should generate a valid numeric CNPJ", () => { @@ -36,6 +59,20 @@ describe("generateCnpj", () => { } }); + test("should compute the first check digit as 9 when the weighted sum leaves remainder 2", () => { + const cnpj = generateWithForcedDraws(REMAINDER_TWO_DRAWS, 10, () => generateCnpj(1)); + + expect(cnpj).toBe("00000000000191"); + expect(isValidCnpj(cnpj)).toBe(true); + }); + + test("should compute the alphanumeric check digits the same way when the remainder is 2", () => { + const cnpj = generateWithForcedDraws(REMAINDER_TWO_DRAWS, 36, () => generateCnpj(2)); + + expect(cnpj).toBe("00000000000191"); + expect(isValidCnpj(cnpj, { version: 2 })).toBe(true); + }); + test("should generate different numeric CNPJs on multiple calls, retrying more draws on the rare chance of a collision", () => { const cnpj1 = generateCnpj(1); const cnpj2 = generateCnpj(1); diff --git a/src/generate-cnpj/generate-cnpj.ts b/src/generate-cnpj/generate-cnpj.ts index 99abff57..3d2af809 100644 --- a/src/generate-cnpj/generate-cnpj.ts +++ b/src/generate-cnpj/generate-cnpj.ts @@ -74,8 +74,5 @@ const generateAlphanumericCnpj = (): string => { * * @see Official: https://www.gov.br/receitafederal/pt-br/assuntos/orientacao-tributaria/cadastros/cnpj */ -export const generateCnpj = (version?: 1 | 2): string => { - const versionToUse = version ?? 1; - if (versionToUse === 1) return generateNumericCnpj(); - return generateAlphanumericCnpj(); -}; +export const generateCnpj = (version: 1 | 2 = 1): string => + version === 1 ? generateNumericCnpj() : generateAlphanumericCnpj(); diff --git a/src/generate-cpf/constants.ts b/src/generate-cpf/constants.ts index 0a52f8c9..515f044e 100644 --- a/src/generate-cpf/constants.ts +++ b/src/generate-cpf/constants.ts @@ -1,4 +1,4 @@ -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; export const BASE_LENGTH = 8; diff --git a/src/generate-cpf/generate-cpf.ts b/src/generate-cpf/generate-cpf.ts index 9b1cb840..b394e204 100644 --- a/src/generate-cpf/generate-cpf.ts +++ b/src/generate-cpf/generate-cpf.ts @@ -1,4 +1,4 @@ -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; import { generateChecksum } from "../_internals/generate-checksum/generate-checksum"; import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; import { isRepeatedDigits } from "../_internals/is-repeated-digits/is-repeated-digits"; diff --git a/src/generate-license-plate/generate-license-plate.ts b/src/generate-license-plate/generate-license-plate.ts index 723c3fdf..946ad6e2 100644 --- a/src/generate-license-plate/generate-license-plate.ts +++ b/src/generate-license-plate/generate-license-plate.ts @@ -1,9 +1,10 @@ -import type { LicensePlateFormat } from "../get-format-license-plate/get-format-license-plate"; +import { type LicensePlateFormat } from "../get-format-license-plate/get-format-license-plate"; const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const DEFAULT_FORMAT = "LLLNLNN"; +/** The license plate formats `generateLicensePlate` can generate. */ export type GenerateLicensePlateFormat = LicensePlateFormat; const randomLetter = (): string => LETTERS.charAt(Math.floor(Math.random() * LETTERS.length)); diff --git a/src/generate-phone/generate-phone.ts b/src/generate-phone/generate-phone.ts index e65401fe..7e00453f 100644 --- a/src/generate-phone/generate-phone.ts +++ b/src/generate-phone/generate-phone.ts @@ -9,6 +9,7 @@ import { } from "../_internals/constants/service-phone"; import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; +/** The kinds of phone number `generatePhone` can generate. */ export type GeneratePhoneType = "mobile" | "landline" | "service"; const randomFrom = (list: readonly Item[]): Item => diff --git a/src/generate-pix-payload/generate-pix-payload.ts b/src/generate-pix-payload/generate-pix-payload.ts index a92f4d56..900f8a65 100644 --- a/src/generate-pix-payload/generate-pix-payload.ts +++ b/src/generate-pix-payload/generate-pix-payload.ts @@ -37,6 +37,7 @@ import { sanitizeToAscii } from "../_internals/sanitize-to-ascii/sanitize-to-asc import { parsePixKey } from "../parse-pix-key/parse-pix-key"; import { AMOUNT_DECIMAL_PLACES, TLV_OVERHEAD, TXID_REGEX } from "./constants"; +/** The parameters `generatePixPayload` takes to build a Pix BR Code. */ export type GeneratePixPayloadParams = { /** The Pix key of the receiver, in any accepted form. Required unless `url` is given. */ key?: string; diff --git a/src/generate-processo-juridico/generate-processo-juridico.ts b/src/generate-processo-juridico/generate-processo-juridico.ts index 390fa9a0..54e38a0f 100644 --- a/src/generate-processo-juridico/generate-processo-juridico.ts +++ b/src/generate-processo-juridico/generate-processo-juridico.ts @@ -1,6 +1,7 @@ import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; import { isNullish } from "../_internals/is-nullish/is-nullish"; +/** Options of `generateProcessoJuridico`. */ export type GenerateProcessoJuridicoOptions = { /** Filing year, from the current year to 9999 (default: the current year). */ year?: number; diff --git a/src/generate-voter-id/generate-voter-id.ts b/src/generate-voter-id/generate-voter-id.ts index 0e6d1f0c..fd32084a 100644 --- a/src/generate-voter-id/generate-voter-id.ts +++ b/src/generate-voter-id/generate-voter-id.ts @@ -1,6 +1,6 @@ import { calculateVoterIdFirstDigit } from "../_internals/calculate-voter-id-first-digit/calculate-voter-id-first-digit"; import { calculateVoterIdSecondDigit } from "../_internals/calculate-voter-id-second-digit/calculate-voter-id-second-digit"; -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; import { generateRandomNumber } from "../_internals/generate-random-number/generate-random-number"; import { UF_TO_VOTER_ID_CODE } from "../is-valid-voter-id/constants"; diff --git a/src/get-address-info-by-cep/get-address-info-by-cep.ts b/src/get-address-info-by-cep/get-address-info-by-cep.ts index e7b8c4df..28deee47 100644 --- a/src/get-address-info-by-cep/get-address-info-by-cep.ts +++ b/src/get-address-info-by-cep/get-address-info-by-cep.ts @@ -2,6 +2,7 @@ import { fetchWithRetry } from "../_internals/fetch-with-retry/fetch-with-retry" import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { isValidCep } from "../is-valid-cep/is-valid-cep"; +/** Base class of every error `getAddressInfoByCep` rejects with. */ export class GetAddressInfoByCepError extends Error { public constructor(message: string) { super(message); @@ -9,6 +10,7 @@ export class GetAddressInfoByCepError extends Error { } } +/** Thrown by `getAddressInfoByCep` when the value given is not a valid CEP. */ export class GetAddressInfoByCepValidationError extends GetAddressInfoByCepError { public constructor(message: string) { super(message); @@ -16,6 +18,7 @@ export class GetAddressInfoByCepValidationError extends GetAddressInfoByCepError } } +/** Thrown by `getAddressInfoByCep` when no CEP service knows the CEP. */ export class GetAddressInfoByCepNotFoundError extends GetAddressInfoByCepError { public constructor(message: string) { super(message); @@ -23,6 +26,7 @@ export class GetAddressInfoByCepNotFoundError extends GetAddressInfoByCepError { } } +/** Thrown by `getAddressInfoByCep` when every CEP service failed to answer. */ export class GetAddressInfoByCepServiceError extends GetAddressInfoByCepError { public constructor(message: string) { super(message); @@ -30,6 +34,7 @@ export class GetAddressInfoByCepServiceError extends GetAddressInfoByCepError { } } +/** The address `getAddressInfoByCep` returns for a CEP. */ export type AddressInfo = { /** The 8 digit CEP, no mask. */ cep: string; @@ -43,8 +48,10 @@ export type AddressInfo = { street: string; }; +/** The CEP services `getAddressInfoByCep` can query. */ export type CepProvider = "viacep" | "widenet" | "brasilapi"; +/** Options of `getAddressInfoByCep`. */ export type GetAddressInfoByCepOptions = { /** Which CEP services to race, in the order given (default: all of them). */ providers?: CepProvider[]; @@ -210,12 +217,14 @@ export const getAddressInfoByCep = async ( } let notFound = false; - const providerPromises = providersToUse.map((provider) => - providerMap[provider](cepString).catch((error: unknown) => { + const providerPromises = providersToUse.map(async (provider) => { + try { + return await providerMap[provider](cepString); + } catch (error) { if (error instanceof GetAddressInfoByCepNotFoundError) notFound = true; throw error; - }), - ); + } + }); try { return await Promise.any(providerPromises); diff --git a/src/get-area-code-info/get-area-code-info.ts b/src/get-area-code-info/get-area-code-info.ts index 94c35308..7d732ded 100644 --- a/src/get-area-code-info/get-area-code-info.ts +++ b/src/get-area-code-info/get-area-code-info.ts @@ -3,6 +3,7 @@ import { DATA, type State, type StateCode, type StateName } from "../_internals/ import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +/** The state, and the region it belongs to, that `getAreaCodeInfo` returns for a DDD. */ export type AreaCodeInfo = { /** The DDD (area code) as a number, e.g. `11`. */ areaCode: number; diff --git a/src/get-boleto-info/get-boleto-info.ts b/src/get-boleto-info/get-boleto-info.ts index a06d5fed..b8147ccb 100644 --- a/src/get-boleto-info/get-boleto-info.ts +++ b/src/get-boleto-info/get-boleto-info.ts @@ -12,6 +12,7 @@ import { RANGE_BEFORE, } from "./constants"; +/** The fields `getBoletoInfo` reads out of a bank slip (boleto). */ export type BoletoInfo = { /** Amount in cents. */ amount: number; @@ -69,6 +70,7 @@ const getExpirationDate = (factor: number, referenceDate: Date): Date | null => return dateFromBase(closest); }; +/** Options of `getBoletoInfo`. */ export type GetBoletoInfoOptions = { /** Date used to resolve the 9000 day "fator de vencimento" cycle (default: now). */ referenceDate?: Date; diff --git a/src/get-cep-info-by-address/get-cep-info-by-address.ts b/src/get-cep-info-by-address/get-cep-info-by-address.ts index 2c483d59..18ba5362 100644 --- a/src/get-cep-info-by-address/get-cep-info-by-address.ts +++ b/src/get-cep-info-by-address/get-cep-info-by-address.ts @@ -2,6 +2,7 @@ import { DATA as STATES, type StateCode } from "../_internals/constants/states"; import { fetchWithRetry } from "../_internals/fetch-with-retry/fetch-with-retry"; import { removeAccents } from "../remove-accents/remove-accents"; +/** Base class of every error `getCepInfoByAddress` rejects with. */ export class GetCepInfoByAddressError extends Error { public constructor(message: string) { super(message); @@ -9,6 +10,7 @@ export class GetCepInfoByAddressError extends Error { } } +/** Thrown by `getCepInfoByAddress` when the state, city or street given is missing or invalid. */ export class GetCepInfoByAddressValidationError extends GetCepInfoByAddressError { public constructor(message: string) { super(message); @@ -16,6 +18,7 @@ export class GetCepInfoByAddressValidationError extends GetCepInfoByAddressError } } +/** Thrown by `getCepInfoByAddress` when no address matches the query. */ export class GetCepInfoByAddressNotFoundError extends GetCepInfoByAddressError { public constructor(message: string) { super(message); @@ -23,6 +26,7 @@ export class GetCepInfoByAddressNotFoundError extends GetCepInfoByAddressError { } } +/** One address returned by `getCepInfoByAddress`, under the field names ViaCEP itself uses. */ export type CepAddressInfo = { /** The CEP, masked as "00000-000" the way ViaCEP returns it. */ cep: string; @@ -46,6 +50,7 @@ export type CepAddressInfo = { siafi?: string; }; +/** The address `getCepInfoByAddress` looks up. */ export type GetCepInfoByAddressOptions = { /** Two letter state code, e.g. "SP". */ federalUnit: string; diff --git a/src/get-cities/get-cities.ts b/src/get-cities/get-cities.ts index 35c63719..81402207 100644 --- a/src/get-cities/get-cities.ts +++ b/src/get-cities/get-cities.ts @@ -1,5 +1,7 @@ import { DATA as CITIES_DATA } from "../_internals/constants/cities"; -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; + +let allCitiesCache: string[] | undefined; /** * Returns a list of city names for a given Brazilian state, or all cities if no state is specified. @@ -10,8 +12,8 @@ import type { StateCode } from "../_internals/constants/states"; * expects them (the combined, sorted list is computed once and cached; every call returns * a fresh copy). * - * @param state - The code of the Brazilian state to filter cities by. Optional. - * @returns An array of city names, sorted alphabetically. Returns an empty array if the state is not found. + * @param {StateCode} [state] - The code of the Brazilian state to filter cities by. Optional. + * @returns {string[]} An array of city names, sorted alphabetically. Returns an empty array if the state is not found. * * @example * ```typescript @@ -21,8 +23,6 @@ import type { StateCode } from "../_internals/constants/states"; * * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades */ -let allCitiesCache: string[] | undefined; - export const getCities = (state?: StateCode): string[] => { if (!state) { allCitiesCache ??= Object.values(CITIES_DATA) diff --git a/src/get-format-license-plate/get-format-license-plate.ts b/src/get-format-license-plate/get-format-license-plate.ts index 11579d35..9c76c3e9 100644 --- a/src/get-format-license-plate/get-format-license-plate.ts +++ b/src/get-format-license-plate/get-format-license-plate.ts @@ -3,6 +3,7 @@ import { LENGTH } from "../parse-license-plate/constants"; import { parseLicensePlate } from "../parse-license-plate/parse-license-plate"; import { MERCOSUL_REGEX, OLD_FORMAT_REGEX } from "./constants"; +/** The Brazilian license plate formats `getFormatLicensePlate` can identify: the old `LLLNNNN` and the Mercosul `LLLNLNN`. */ export type LicensePlateFormat = "LLLNNNN" | "LLLNLNN"; /** diff --git a/src/get-holidays/constants.ts b/src/get-holidays/constants.ts index 838bd257..8a9a812b 100644 --- a/src/get-holidays/constants.ts +++ b/src/get-holidays/constants.ts @@ -1,5 +1,5 @@ -import type { StateCode } from "../_internals/constants/states"; -import type { HolidayType } from "./get-holidays"; +import { type StateCode } from "../_internals/constants/states"; +import { type HolidayType } from "./get-holidays"; export type StateHolidayEntry = { name: string; diff --git a/src/get-holidays/get-holidays.ts b/src/get-holidays/get-holidays.ts index 1758675a..81b20d0e 100644 --- a/src/get-holidays/get-holidays.ts +++ b/src/get-holidays/get-holidays.ts @@ -1,5 +1,5 @@ import { HOLIDAYS_MAX_YEAR, HOLIDAYS_MIN_YEAR } from "../_internals/constants/holidays"; -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { CONSCIENCIA_NEGRA_HOLIDAY_NAME, @@ -9,8 +9,10 @@ import { STATE_HOLIDAYS, } from "./constants"; +/** How a holiday returned by `getHolidays` is observed. */ export type HolidayType = "national" | "state" | "optional" | "religious"; +/** One holiday returned by `getHolidays`. */ export type Holiday = { /** The holiday name in Brazilian Portuguese, e.g. `"Sexta-feira Santa"`. */ name: string; @@ -20,6 +22,7 @@ export type Holiday = { type: HolidayType; }; +/** The options form `getHolidays` accepts, naming the year to list and, optionally, the state whose holidays are added. */ export type GetHolidaysOptions = { /** The four digit year to list holidays for. Must be an integer between 1900 and 2099. */ year: number; @@ -180,6 +183,13 @@ const computeHolidays = (year: number, stateCode: StateCode | undefined): Holida * some state holidays where no official law text was located (see constants.ts for which). */ export function getHolidays(year: number): Holiday[]; +/** + * Retrieves all Brazilian holidays for a given year, optionally including the holidays of a + * state. See the overload taking a year for the full documentation. + * + * @param {GetHolidaysOptions} options - The year to list holidays for and, optionally, the state whose holidays are added + * @returns {Holiday[]} An array of holidays sorted by date + */ export function getHolidays(options: GetHolidaysOptions): Holiday[]; export function getHolidays(yearOrOptions: number | GetHolidaysOptions): Holiday[] { let year: number; diff --git a/src/get-municipalities/get-municipalities.test.ts b/src/get-municipalities/get-municipalities.test.ts index f385b651..b50d17d9 100644 --- a/src/get-municipalities/get-municipalities.test.ts +++ b/src/get-municipalities/get-municipalities.test.ts @@ -1,11 +1,12 @@ import { DATA } from "../_internals/constants/cities"; +import { type StateCode } from "../_internals/constants/states"; import { describe, expect, it } from "../_internals/test/runtime"; import { getStates } from "../get-states/get-states"; import { getMunicipalities } from "./get-municipalities"; const NUMBER_OF_BRAZILIAN_MUNICIPALITIES = 5571; -const KNOWN_STATE_MUNICIPALITY_COUNTS: Record = { +const KNOWN_STATE_MUNICIPALITY_COUNTS: Partial> = { MG: 853, MT: 142, RS: 497, @@ -35,7 +36,7 @@ describe("getMunicipalities", () => { it("should filter municipalities by state", () => { for (const [stateCode, expectedCount] of Object.entries(KNOWN_STATE_MUNICIPALITY_COUNTS)) { - expect(getMunicipalities(stateCode).length).toBe(expectedCount); + expect(getMunicipalities(stateCode as StateCode).length).toBe(expectedCount); } }); @@ -50,11 +51,14 @@ describe("getMunicipalities", () => { }); it("should return an empty array for an unknown state", () => { + // @ts-expect-error: intentionally invalid input expect(getMunicipalities("ZZ")).toEqual([]); }); it("should return an empty array for inherited Object property names instead of throwing", () => { + // @ts-expect-error: intentionally invalid input expect(getMunicipalities("toString")).toEqual([]); + // @ts-expect-error: intentionally invalid input expect(getMunicipalities("constructor")).toEqual([]); }); diff --git a/src/get-municipalities/get-municipalities.ts b/src/get-municipalities/get-municipalities.ts index 0e7272ad..82132108 100644 --- a/src/get-municipalities/get-municipalities.ts +++ b/src/get-municipalities/get-municipalities.ts @@ -1,5 +1,5 @@ import { DATA as CITIES_DATA, type Municipality } from "../_internals/constants/cities"; -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; import { getStates } from "../get-states/get-states"; const buildMunicipalities = (stateCode: StateCode): Municipality[] => @@ -12,7 +12,7 @@ const buildMunicipalities = (stateCode: StateCode): Municipality[] => * omitted, every municipality of every state is returned, sorted with `localeCompare` in the * "pt-BR" locale so accented names land where a Brazilian reader expects them. * - * @param {string} [stateCode] - The two letter code of the Brazilian state to filter by. + * @param {StateCode} [stateCode] - The two letter code of the Brazilian state to filter by. * @returns {Municipality[]} A fresh array of fresh `Municipality` objects. Empty when * `stateCode` is not a known state. * @@ -25,7 +25,7 @@ const buildMunicipalities = (stateCode: StateCode): Municipality[] => * * @see Official: https://servicodados.ibge.gov.br/api/docs/localidades */ -export const getMunicipalities = (stateCode?: string): Municipality[] => { +export const getMunicipalities = (stateCode?: StateCode): Municipality[] => { if (stateCode === undefined) { return getStates() .flatMap((state) => buildMunicipalities(state.code)) diff --git a/src/get-municipality/get-municipality.test.ts b/src/get-municipality/get-municipality.test.ts index e7aebc8a..6c0c20a3 100644 --- a/src/get-municipality/get-municipality.test.ts +++ b/src/get-municipality/get-municipality.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "../_internals/test/runtime"; -import type { GetMunicipalityByNameOptions } from "./get-municipality"; -import { getMunicipality } from "./get-municipality"; +import { type GetMunicipalityByNameOptions, getMunicipality } from "./get-municipality"; describe("getMunicipality", () => { it("should get municipality code by name", async () => { diff --git a/src/get-municipality/get-municipality.ts b/src/get-municipality/get-municipality.ts index dfec7319..5ea90a8b 100644 --- a/src/get-municipality/get-municipality.ts +++ b/src/get-municipality/get-municipality.ts @@ -2,11 +2,13 @@ import { DATA as CITIES_DATA } from "../_internals/constants/cities"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { removeAccents } from "../remove-accents/remove-accents"; +/** The `getMunicipality` query by IBGE municipality code. */ export type GetMunicipalityByCodeOptions = { /** The 7 digit IBGE municipality code. */ code: string; }; +/** The `getMunicipality` query by municipality name and state code. */ export type GetMunicipalityByNameOptions = { /** The municipality name, accents and casing ignored. */ municipalityName: string; @@ -14,6 +16,7 @@ export type GetMunicipalityByNameOptions = { uf: string; }; +/** The two ways `getMunicipality` can be queried: by IBGE code, or by municipality name and state code. */ export type GetMunicipalityOptions = GetMunicipalityByCodeOptions | GetMunicipalityByNameOptions; let codeIndex: Map | undefined; diff --git a/src/index.test.ts b/src/index.test.ts index fc0734df..7dfa4e38 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,81 +1,81 @@ import { describe, expect, test } from "./_internals/test/runtime"; -import type { - AddBusinessDaysParams, - AddressInfo, - AreaCodeInfo, - Bank, - BoletoInfo, - CapitalizeOptions, - Cbo, - CepAddressInfo, - CepProvider, - Certidao, - CertidaoType, - Cfop, - Cnae, - ConvertCurrencyToWordsOptions, - ConvertDateToWordsOptions, - ConvertNumberToWordsOptions, - DifferenceInBusinessDaysParams, - FormatBoletoOptions, - FormatCaepfOptions, - FormatCeiOptions, - FormatCepOptions, - FormatCertidaoOptions, - FormatCnhOptions, - FormatCnoOptions, - FormatCnpjOptions, - FormatCnsOptions, - FormatCpfOptions, - FormatCurrencyOptions, - FormatPhoneOptions, - FormatPisOptions, - FormatProcessoJuridicoOptions, - GenerateBoletoOptions, - GenerateLicensePlateFormat, - GeneratePhoneType, - GeneratePixPayloadParams, - GenerateProcessoJuridicoOptions, - GetAddressInfoByCepOptions, - GetBoletoInfoOptions, - GetCepInfoByAddressOptions, - GetHolidaysOptions, - GetMunicipalityByCodeOptions, - GetMunicipalityByNameOptions, - GetMunicipalityOptions, - Holiday, - HolidayType, - Iban, - IsBusinessDayOptions, - IsHolidayOptions, - IsValidBankAccountOptions, - IsValidBankAccountParams, - IsValidCertidaoOptions, - IsValidCnpjOptions, - IsValidCstOptions, - IsValidMobilePhoneOptions, - IsValidPhoneOptions, - IsValidPixKeyOptions, - IsValidRegistroProfissionalOptions, - LegalNature, - LicensePlateFormat, - Municipality, - NfeKey, - NumberToWordsGender, - ParseCnpjOptions, - ParseCurrencyOptions, - PhoneMask, - PhoneType, - PhoneVersion, - PixKey, - PixKeyType, - PixPayload, - PixPointOfInitiation, - RegistroProfissionalCouncil, - State, - StateCode, - StateName, - WordsCase, +import { + type AddBusinessDaysParams, + type AddressInfo, + type AreaCodeInfo, + type Bank, + type BoletoInfo, + type CapitalizeOptions, + type Cbo, + type CepAddressInfo, + type CepProvider, + type Certidao, + type CertidaoType, + type Cfop, + type Cnae, + type ConvertCurrencyToWordsOptions, + type ConvertDateToWordsOptions, + type ConvertNumberToWordsOptions, + type DifferenceInBusinessDaysParams, + type FormatBoletoOptions, + type FormatCaepfOptions, + type FormatCeiOptions, + type FormatCepOptions, + type FormatCertidaoOptions, + type FormatCnhOptions, + type FormatCnoOptions, + type FormatCnpjOptions, + type FormatCnsOptions, + type FormatCpfOptions, + type FormatCurrencyOptions, + type FormatPhoneOptions, + type FormatPisOptions, + type FormatProcessoJuridicoOptions, + type GenerateBoletoOptions, + type GenerateLicensePlateFormat, + type GeneratePhoneType, + type GeneratePixPayloadParams, + type GenerateProcessoJuridicoOptions, + type GetAddressInfoByCepOptions, + type GetBoletoInfoOptions, + type GetCepInfoByAddressOptions, + type GetHolidaysOptions, + type GetMunicipalityByCodeOptions, + type GetMunicipalityByNameOptions, + type GetMunicipalityOptions, + type Holiday, + type HolidayType, + type Iban, + type IsBusinessDayOptions, + type IsHolidayOptions, + type IsValidBankAccountOptions, + type IsValidBankAccountParams, + type IsValidCertidaoOptions, + type IsValidCnpjOptions, + type IsValidCstOptions, + type IsValidMobilePhoneOptions, + type IsValidPhoneOptions, + type IsValidPixKeyOptions, + type IsValidRegistroProfissionalOptions, + type LegalNature, + type LicensePlateFormat, + type Municipality, + type NfeKey, + type NumberToWordsGender, + type ParseCnpjOptions, + type ParseCurrencyOptions, + type PhoneMask, + type PhoneType, + type PhoneVersion, + type PixKey, + type PixKeyType, + type PixPayload, + type PixPointOfInitiation, + type RegistroProfissionalCouncil, + type State, + type StateCode, + type StateName, + type WordsCase, } from "./index"; import * as brazilianUtils from "./index"; diff --git a/src/index.ts b/src/index.ts index 9bd03141..1729cb11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -192,7 +192,7 @@ export { type ParseCurrencyOptions, parseCurrency } from "./parse-currency/parse export { type Iban, parseIban } from "./parse-iban/parse-iban"; export { parseLegalNature } from "./parse-legal-nature/parse-legal-nature"; export { parseLicensePlate } from "./parse-license-plate/parse-license-plate"; -export { type NfeKey, parseNfeKey } from "./parse-nfe-key/parse-nfe-key"; +export { type NfeKey, type NfeKeyModel, parseNfeKey } from "./parse-nfe-key/parse-nfe-key"; export { parsePassport } from "./parse-passport/parse-passport"; export { parsePhone } from "./parse-phone/parse-phone"; export { parsePis } from "./parse-pis/parse-pis"; @@ -206,7 +206,12 @@ export { parseProcessoJuridico } from "./parse-processo-juridico/parse-processo- export { parseVoterId } from "./parse-voter-id/parse-voter-id"; export { removeAccents } from "./remove-accents/remove-accents"; -/** @deprecated Use `IsValidBankAccountOptions` instead. */ +/** + * The bank account `isValidBankAccount` checks: the bank, the agency and the account with its + * check digit. + * + * @deprecated Use `IsValidBankAccountOptions` instead. + */ export type { IsValidBankAccountParams } from "./is-valid-bank-account/is-valid-bank-account"; /** @deprecated Use `formatCep` instead. */ export { formatCep as formatCEP } from "./format-cep/format-cep"; diff --git a/src/is-business-day/is-business-day.test.ts b/src/is-business-day/is-business-day.test.ts index 67f6559b..44b50ac4 100644 --- a/src/is-business-day/is-business-day.test.ts +++ b/src/is-business-day/is-business-day.test.ts @@ -94,7 +94,7 @@ describe("isBusinessDay", () => { it("should not mutate the input Date", () => { const value = new Date(2024, 0, 6, 12); - const original = new Date(value.getTime()); + const original = new Date(value); isBusinessDay(value, { stateCode: "SP" }); diff --git a/src/is-business-day/is-business-day.ts b/src/is-business-day/is-business-day.ts index cbee9172..b94e0edf 100644 --- a/src/is-business-day/is-business-day.ts +++ b/src/is-business-day/is-business-day.ts @@ -1,7 +1,8 @@ import { HOLIDAYS_MAX_YEAR, HOLIDAYS_MIN_YEAR } from "../_internals/constants/holidays"; -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; import { getHolidays } from "../get-holidays/get-holidays"; +/** Options of `isBusinessDay`. */ export type IsBusinessDayOptions = { /** Two letter state code whose state holidays are also treated as non-business days (default: national holidays only). */ stateCode?: StateCode; diff --git a/src/is-holiday/is-holiday.ts b/src/is-holiday/is-holiday.ts index 053336f3..d971f96a 100644 --- a/src/is-holiday/is-holiday.ts +++ b/src/is-holiday/is-holiday.ts @@ -1,7 +1,8 @@ -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { getHolidays } from "../get-holidays/get-holidays"; +/** The options `isHoliday` takes: the date to check and, optionally, the state whose holidays also count. */ export type IsHolidayOptions = { /** The date to check, read by its local calendar day. */ targetDate: Date; @@ -54,10 +55,9 @@ export const isHoliday = (options?: IsHolidayOptions): boolean => { } const year = targetDate.getFullYear(); - return getHolidays({ year, stateCode }).some((holiday) => { - return ( + return getHolidays({ year, stateCode }).some( + (holiday) => holiday.date.getMonth() === targetDate.getMonth() && - holiday.date.getDate() === targetDate.getDate() - ); - }); + holiday.date.getDate() === targetDate.getDate(), + ); }; diff --git a/src/is-valid-bank-account/is-valid-bank-account.ts b/src/is-valid-bank-account/is-valid-bank-account.ts index 8b3aa4cb..ffc99227 100644 --- a/src/is-valid-bank-account/is-valid-bank-account.ts +++ b/src/is-valid-bank-account/is-valid-bank-account.ts @@ -15,6 +15,7 @@ import { VERHOEFF_PERMUTATION, } from "./constants"; +/** The bank account `isValidBankAccount` checks: the bank, the agency and the account with its check digit. */ export type IsValidBankAccountOptions = { /** Three digit bank code (COMPE), e.g. "001" for Banco do Brasil. */ bankCode: string; @@ -26,7 +27,12 @@ export type IsValidBankAccountOptions = { digit: string; }; -/** @deprecated Use `IsValidBankAccountOptions` instead. */ +/** + * The bank account `isValidBankAccount` checks: the bank, the agency and the account with its + * check digit. + * + * @deprecated Use `IsValidBankAccountOptions` instead. + */ export type IsValidBankAccountParams = IsValidBankAccountOptions; type BankAccountDigits = (agency: string, account: string) => string[]; diff --git a/src/is-valid-cbo/is-valid-cbo.ts b/src/is-valid-cbo/is-valid-cbo.ts index e2c8da86..7b008a70 100644 --- a/src/is-valid-cbo/is-valid-cbo.ts +++ b/src/is-valid-cbo/is-valid-cbo.ts @@ -1,6 +1,4 @@ -import { CBO_TITLES } from "../_internals/constants/cbo"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; -import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { getCbo } from "../get-cbo/get-cbo"; /** * Validates if a CBO (Classificação Brasileira de Ocupações) code exists in the official @@ -15,6 +13,7 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * isValidCbo("2124-05"); // true * isValidCbo("212405"); // true * isValidCbo(212405); // true + * isValidCbo(10205); // true (a number is padded to 6 digits, so this is "010205") * isValidCbo("999999"); // false * ``` * @@ -22,10 +21,4 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * @see Based on: https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json * Community mirror of the official table used to build `CBO_TITLES`. */ -export const isValidCbo = (value: string | number): boolean => { - if (isNullish(value)) return false; - - const digits = sanitizeToDigits(value); - - return digits in CBO_TITLES; -}; +export const isValidCbo = (value: string | number): boolean => getCbo(value) !== null; diff --git a/src/is-valid-certidao/is-valid-certidao.ts b/src/is-valid-certidao/is-valid-certidao.ts index e1d88b28..2dd2dd22 100644 --- a/src/is-valid-certidao/is-valid-certidao.ts +++ b/src/is-valid-certidao/is-valid-certidao.ts @@ -5,8 +5,9 @@ import { } from "../_internals/constants/certidao"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { CERTIDAO_TYPES } from "../parse-certidao/constants"; -import type { CertidaoType } from "../parse-certidao/parse-certidao"; +import { type CertidaoType } from "../parse-certidao/parse-certidao"; +/** Options of `isValidCertidao`. */ export type IsValidCertidaoOptions = { /** Kinds of certidão (book types) that count as valid (default: all of them). */ accept?: CertidaoType[]; @@ -18,7 +19,7 @@ const getCheckDigit = (value: string): number => { for (let i = 0; i < value.length; i++) { sum += (value.charCodeAt(i) - 48) * weight; - weight = weight + 1; + weight += 1; } const remainder = sum % 11; diff --git a/src/is-valid-cnae/is-valid-cnae.ts b/src/is-valid-cnae/is-valid-cnae.ts index 948c8231..ae052d77 100644 --- a/src/is-valid-cnae/is-valid-cnae.ts +++ b/src/is-valid-cnae/is-valid-cnae.ts @@ -1,6 +1,4 @@ -import { CNAE_SUBCLASSES } from "../_internals/constants/cnae"; -import { isNullish } from "../_internals/is-nullish/is-nullish"; -import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; +import { getCnae } from "../get-cnae/get-cnae"; /** * Validates if a CNAE (Classificação Nacional de Atividades Econômicas) subclass code @@ -15,15 +13,10 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d * isValidCnae("6201-5/01"); // true * isValidCnae("6201501"); // true * isValidCnae(6201501); // true + * isValidCnae(111301); // true (a number is padded to 7 digits, so this is "0111301") * isValidCnae("0000000"); // false * ``` * * @see Official: https://servicodados.ibge.gov.br/api/v2/cnae/subclasses */ -export const isValidCnae = (value: string | number): boolean => { - if (isNullish(value)) return false; - - const digits = sanitizeToDigits(value); - - return digits in CNAE_SUBCLASSES; -}; +export const isValidCnae = (value: string | number): boolean => getCnae(value) !== null; diff --git a/src/is-valid-cnpj/is-valid-cnpj.ts b/src/is-valid-cnpj/is-valid-cnpj.ts index 9740ae48..034b5dfa 100644 --- a/src/is-valid-cnpj/is-valid-cnpj.ts +++ b/src/is-valid-cnpj/is-valid-cnpj.ts @@ -6,6 +6,7 @@ import { import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { RESERVED_NUMBERS } from "./constants"; +/** Options of `isValidCnpj`. */ export type IsValidCnpjOptions = { /** Which CNPJ format to accept: `1` numeric only, `2` alphanumeric (default: `1`). */ version?: 1 | 2; diff --git a/src/is-valid-ie/is-valid-ie.ts b/src/is-valid-ie/is-valid-ie.ts index e1713b84..a77542bd 100644 --- a/src/is-valid-ie/is-valid-ie.ts +++ b/src/is-valid-ie/is-valid-ie.ts @@ -1,4 +1,4 @@ -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { @@ -23,7 +23,7 @@ const checkLength = (ie: string, length: number | number[]): boolean => { }; const startsWithAny = (ie: string, prefixes: readonly string[]): boolean => - prefixes.some((prefix) => ie.slice(0, prefix.length) === prefix); + prefixes.some((prefix) => ie.startsWith(prefix)); const startsWith = (ie: string, prefix: string): boolean => startsWithAny(ie, [prefix]); @@ -169,7 +169,7 @@ const validateBA: IeValidator = (ie: string) => { const charAt = Number.parseInt(ie.slice(pos, pos + 1), 10); const mod = BA_MOD_10_DIGITS.includes(charAt) ? 10 : 11; - const body = ie.slice(0, ie.length - 2); + const body = ie.slice(0, -2); const firstSum = calcWeightedSum({ source: ie, length: body.length, diff --git a/src/is-valid-mobile-phone/is-valid-mobile-phone.ts b/src/is-valid-mobile-phone/is-valid-mobile-phone.ts index 70754423..55e446a5 100644 --- a/src/is-valid-mobile-phone/is-valid-mobile-phone.ts +++ b/src/is-valid-mobile-phone/is-valid-mobile-phone.ts @@ -1,11 +1,12 @@ import { PHONE_NATIONAL_MAX_LENGTH } from "../_internals/constants/phone"; import { isValidDDD } from "../_internals/is-valid-ddd/is-valid-ddd"; import { normalizePhone } from "../_internals/normalize-phone/normalize-phone"; -import type { PhoneVersion } from "../is-valid-phone/is-valid-phone"; +import { type PhoneVersion } from "../is-valid-phone/is-valid-phone"; import { MOBILE_VALID_FIRST_NUMBERS_V1, MOBILE_VALID_FIRST_NUMBERS_V2 } from "./constants"; export type { PhoneVersion } from "../is-valid-phone/is-valid-phone"; +/** Options of `isValidMobilePhone`. */ export type IsValidMobilePhoneOptions = { /** Numbering rule to enforce: `1` the pre-2016 8 digit rule, `2` the 9 digit one (default: `2`). */ version?: PhoneVersion; diff --git a/src/is-valid-phone/constants.ts b/src/is-valid-phone/constants.ts index 1d58a6ea..6e562530 100644 --- a/src/is-valid-phone/constants.ts +++ b/src/is-valid-phone/constants.ts @@ -1,3 +1,3 @@ -import type { PhoneType } from "./is-valid-phone"; +import { type PhoneType } from "./is-valid-phone"; export const DEFAULT_ACCEPT: PhoneType[] = ["mobile", "landline"]; diff --git a/src/is-valid-phone/is-valid-phone.ts b/src/is-valid-phone/is-valid-phone.ts index 9dc40d4c..928ac32d 100644 --- a/src/is-valid-phone/is-valid-phone.ts +++ b/src/is-valid-phone/is-valid-phone.ts @@ -9,10 +9,13 @@ import { isValidMobilePhone } from "../is-valid-mobile-phone/is-valid-mobile-pho import { isValidServicePhone } from "../is-valid-service-phone/is-valid-service-phone"; import { DEFAULT_ACCEPT } from "./constants"; +/** The Brazilian mobile numbering rule to enforce: `1` the pre-2016 8 digit one, `2` the current 9 digit one. */ export type PhoneVersion = 1 | 2; +/** The kinds of Brazilian phone number `isValidPhone` can accept. */ export type PhoneType = "mobile" | "landline" | "service"; +/** Options of `isValidPhone`. */ export type IsValidPhoneOptions = { /** Mobile numbering rule to enforce, see `isValidMobilePhone` (default: `2`). */ version?: PhoneVersion; diff --git a/src/is-valid-pix-key/is-valid-pix-key.ts b/src/is-valid-pix-key/is-valid-pix-key.ts index 78cfef87..41da0881 100644 --- a/src/is-valid-pix-key/is-valid-pix-key.ts +++ b/src/is-valid-pix-key/is-valid-pix-key.ts @@ -1,5 +1,6 @@ import { type PixKeyType, parsePixKey } from "../parse-pix-key/parse-pix-key"; +/** Options of `isValidPixKey`. */ export type IsValidPixKeyOptions = { /** Kinds of Pix key that count as valid (default: all of them). */ accept?: PixKeyType[]; diff --git a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts index 255f9321..aab89bf9 100644 --- a/src/is-valid-registro-profissional/is-valid-registro-profissional.ts +++ b/src/is-valid-registro-profissional/is-valid-registro-profissional.ts @@ -9,6 +9,7 @@ import { type RegistroProfissionalCouncil, } from "./constants"; +/** The options `isValidRegistroProfissional` takes: the professional council and, optionally, the UF the registration must belong to. */ export type IsValidRegistroProfissionalOptions = { /** The professional council that issued the registration number. */ council: RegistroProfissionalCouncil; diff --git a/src/is-valid-voter-id/constants.ts b/src/is-valid-voter-id/constants.ts index 75c112a6..14ceef66 100644 --- a/src/is-valid-voter-id/constants.ts +++ b/src/is-valid-voter-id/constants.ts @@ -1,4 +1,4 @@ -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; export const UF_TO_VOTER_ID_CODE: Record = { SP: "01", diff --git a/src/parse-certidao/parse-certidao.ts b/src/parse-certidao/parse-certidao.ts index 64a09332..4eb517d3 100644 --- a/src/parse-certidao/parse-certidao.ts +++ b/src/parse-certidao/parse-certidao.ts @@ -3,8 +3,23 @@ import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-d import { isValidCertidao } from "../is-valid-certidao/is-valid-certidao"; import { CERTIDAO_TYPES } from "./constants"; -export type CertidaoType = (typeof CERTIDAO_TYPES)[number]; +/** + * The nine books (tipo do livro) a matrícula de registro civil can point to, in the order of the + * codes 1 to 9. `parseCertidao` names the book of a matrícula with one of these, and + * `isValidCertidao` accepts a list of them. + */ +export type CertidaoType = + | "birth" + | "marriage" + | "religious-marriage" + | "death" + | "stillbirth" + | "banns" + | "other" + | "emancipation" + | "interdiction"; +/** The fields `parseCertidao` reads out of the matrícula of a certidão de registro civil. */ export type Certidao = { /** The 6 digit CNS (Código Nacional de Serventia) of the serventia that issued the act. */ registryCns: string; diff --git a/src/parse-cnpj/parse-cnpj.ts b/src/parse-cnpj/parse-cnpj.ts index 55b39048..e7eabb50 100644 --- a/src/parse-cnpj/parse-cnpj.ts +++ b/src/parse-cnpj/parse-cnpj.ts @@ -2,8 +2,9 @@ import { CNPJ_LENGTH } from "../_internals/constants/cnpj"; import { isNullish } from "../_internals/is-nullish/is-nullish"; import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; -import type { FormatCnpjOptions } from "../format-cnpj/format-cnpj"; +import { type FormatCnpjOptions } from "../format-cnpj/format-cnpj"; +/** Options of `parseCnpj`. */ export type ParseCnpjOptions = Pick; const sanitize = (value: string | number, version?: FormatCnpjOptions["version"]): string => { diff --git a/src/parse-currency/parse-currency.ts b/src/parse-currency/parse-currency.ts index d5c0ac8c..b115adac 100644 --- a/src/parse-currency/parse-currency.ts +++ b/src/parse-currency/parse-currency.ts @@ -1,6 +1,7 @@ import { DEFAULT_PRECISION, clampPrecision } from "../_internals/clamp-precision/clamp-precision"; import { parseDecimal } from "../_internals/parse-decimal/parse-decimal"; +/** Options of `parseCurrency`. */ export type ParseCurrencyOptions = { /** Number of decimal places used as the minor unit scale. Fractions accept up to two digits, or `precision` digits when it is greater. Defaults to 2, clamped to 0-20. */ precision?: number; diff --git a/src/parse-iban/parse-iban.ts b/src/parse-iban/parse-iban.ts index 3970dc91..787e866b 100644 --- a/src/parse-iban/parse-iban.ts +++ b/src/parse-iban/parse-iban.ts @@ -1,6 +1,7 @@ import { sanitizeToAlphanumeric } from "../_internals/sanitize-to-alphanumeric/sanitize-to-alphanumeric"; import { isValidIban } from "../is-valid-iban/is-valid-iban"; +/** The fields `parseIban` reads out of a Brazilian IBAN. */ export type Iban = { /** ISO 3166-1 alpha-2 country code. Always `"BR"`, the only country this parser supports. */ countryCode: "BR"; diff --git a/src/parse-nfe-key/parse-nfe-key.ts b/src/parse-nfe-key/parse-nfe-key.ts index e2030de5..6a6de14c 100644 --- a/src/parse-nfe-key/parse-nfe-key.ts +++ b/src/parse-nfe-key/parse-nfe-key.ts @@ -1,12 +1,14 @@ import { IBGE_UF_CODES } from "../_internals/constants/ibge-uf-codes"; import { NFE_KEY_LENGTH } from "../_internals/constants/nfe-key"; -import type { StateCode } from "../_internals/constants/states"; +import { type StateCode } from "../_internals/constants/states"; import { mod11 } from "../_internals/mod11/mod11"; import { sanitizeToDigits } from "../_internals/sanitize-to-digits/sanitize-to-digits"; import { ABSENT_NUMBER, FORMAT_REGEX, NUMBER_END, NUMBER_START, VALID_MODELS } from "./constants"; -export type NfeKeyModel = (typeof VALID_MODELS)[number]; +/** The document models a DF-e access key can carry: `"55"` NF-e, `"57"` CT-e, `"58"` MDF-e and `"65"` NFC-e. */ +export type NfeKeyModel = "55" | "57" | "58" | "65"; +/** The fields `parseNfeKey` reads out of a DF-e access key (chave de acesso). */ export type NfeKey = { /** Two letter code of the issuing state, read from the IBGE UF code. */ state: StateCode; diff --git a/src/parse-pix-key/parse-pix-key.ts b/src/parse-pix-key/parse-pix-key.ts index d82c654c..ecf713a7 100644 --- a/src/parse-pix-key/parse-pix-key.ts +++ b/src/parse-pix-key/parse-pix-key.ts @@ -9,8 +9,10 @@ import { isValidPhone } from "../is-valid-phone/is-valid-phone"; import { parseCnpj } from "../parse-cnpj/parse-cnpj"; import { EMAIL_MAX_LENGTH, EVP_REGEX, PHONE_HINT_REGEX } from "./constants"; +/** The kinds of Pix key `parsePixKey` recognizes. */ export type PixKeyType = "cpf" | "cnpj" | "email" | "phone" | "evp"; +/** A Pix key recognized by `parsePixKey`, normalized to the canonical DICT form of its kind. */ export type PixKey = { /** Which kind of Pix key the value was recognized as. */ type: PixKeyType; diff --git a/src/parse-pix-payload/parse-pix-payload.test.ts b/src/parse-pix-payload/parse-pix-payload.test.ts index 39f4ae16..42a6e8e3 100644 --- a/src/parse-pix-payload/parse-pix-payload.test.ts +++ b/src/parse-pix-payload/parse-pix-payload.test.ts @@ -26,16 +26,17 @@ const tlv = (id: string, value: string): string => `${id}${value.length.toString().padStart(2, "0")}${value}`; const buildPayload = (merchantAccountInformation: string, additionalData?: string): string => { - const withoutCrc = - tlv("00", "01") + - tlv("26", merchantAccountInformation) + - tlv("52", "0000") + - tlv("53", "986") + - tlv("58", "BR") + - tlv("59", "Fulano de Tal") + - tlv("60", "BRASILIA") + - (additionalData === undefined ? "" : tlv("62", additionalData)) + - "6304"; + const withoutCrc = [ + tlv("00", "01"), + tlv("26", merchantAccountInformation), + tlv("52", "0000"), + tlv("53", "986"), + tlv("58", "BR"), + tlv("59", "Fulano de Tal"), + tlv("60", "BRASILIA"), + additionalData === undefined ? "" : tlv("62", additionalData), + "6304", + ].join(""); return withoutCrc + crc16Ccitt(withoutCrc); }; @@ -43,28 +44,30 @@ const buildPayload = (merchantAccountInformation: string, additionalData?: strin const MERCHANT_ACCOUNT_INFORMATION = tlv("00", "br.gov.bcb.pix") + tlv("01", "12345678909"); const buildPayloadWithMerchantAccountInformationTag = (tag: string): string => { - const withoutCrc = - tlv("00", "01") + - tlv(tag, MERCHANT_ACCOUNT_INFORMATION) + - tlv("52", "0000") + - tlv("53", "986") + - tlv("58", "BR") + - tlv("59", "Fulano de Tal") + - tlv("60", "BRASILIA") + - "6304"; + const withoutCrc = [ + tlv("00", "01"), + tlv(tag, MERCHANT_ACCOUNT_INFORMATION), + tlv("52", "0000"), + tlv("53", "986"), + tlv("58", "BR"), + tlv("59", "Fulano de Tal"), + tlv("60", "BRASILIA"), + "6304", + ].join(""); return withoutCrc + crc16Ccitt(withoutCrc); }; const buildPayloadWithoutCountryCode = (): string => { - const withoutCrc = - tlv("00", "01") + - tlv("26", MERCHANT_ACCOUNT_INFORMATION) + - tlv("52", "0000") + - tlv("53", "986") + - tlv("59", "Fulano de Tal") + - tlv("60", "BRASILIA") + - "6304"; + const withoutCrc = [ + tlv("00", "01"), + tlv("26", MERCHANT_ACCOUNT_INFORMATION), + tlv("52", "0000"), + tlv("53", "986"), + tlv("59", "Fulano de Tal"), + tlv("60", "BRASILIA"), + "6304", + ].join(""); return withoutCrc + crc16Ccitt(withoutCrc); }; @@ -86,30 +89,32 @@ const buildPayloadWithCrcTag = (crcTag: string): string => { }; const buildPayloadWithAmount = (amount: string): string => { - const withoutCrc = - tlv("00", "01") + - tlv("26", MERCHANT_ACCOUNT_INFORMATION) + - tlv("52", "0000") + - tlv("53", "986") + - tlv("54", amount) + - tlv("58", "BR") + - tlv("59", "Fulano de Tal") + - tlv("60", "BRASILIA") + - "6304"; + const withoutCrc = [ + tlv("00", "01"), + tlv("26", MERCHANT_ACCOUNT_INFORMATION), + tlv("52", "0000"), + tlv("53", "986"), + tlv("54", amount), + tlv("58", "BR"), + tlv("59", "Fulano de Tal"), + tlv("60", "BRASILIA"), + "6304", + ].join(""); return withoutCrc + crc16Ccitt(withoutCrc); }; const buildPayloadWithMerchantName = (merchantName: string): string => { - const withoutCrc = - tlv("00", "01") + - tlv("26", MERCHANT_ACCOUNT_INFORMATION) + - tlv("52", "0000") + - tlv("53", "986") + - tlv("58", "BR") + - tlv("59", merchantName) + - tlv("60", "BRASILIA") + - "6304"; + const withoutCrc = [ + tlv("00", "01"), + tlv("26", MERCHANT_ACCOUNT_INFORMATION), + tlv("52", "0000"), + tlv("53", "986"), + tlv("58", "BR"), + tlv("59", merchantName), + tlv("60", "BRASILIA"), + "6304", + ].join(""); return withoutCrc + crc16Ccitt(withoutCrc); }; diff --git a/src/parse-pix-payload/parse-pix-payload.ts b/src/parse-pix-payload/parse-pix-payload.ts index e982acdc..7e53a7c5 100644 --- a/src/parse-pix-payload/parse-pix-payload.ts +++ b/src/parse-pix-payload/parse-pix-payload.ts @@ -30,8 +30,10 @@ import { crc16Ccitt } from "../_internals/crc16-ccitt/crc16-ccitt"; import { isValidPixUrl } from "../_internals/is-valid-pix-url/is-valid-pix-url"; import { type TlvFields, parseTlv } from "../_internals/parse-tlv/parse-tlv"; +/** Whether a Pix BR Code may be paid many times (`"static"`) or only once (`"dynamic"`). */ export type PixPointOfInitiation = "static" | "dynamic"; +/** The fields `parsePixPayload` reads out of a Pix BR Code. */ export type PixPayload = { /** The Pix key of the receiver, present in a static payload. */ key?: string; diff --git a/vite.config.ts b/vite.config.ts index 964d849c..39097a4d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,7 +3,7 @@ import { resolve } from "node:path"; import { transform } from "esbuild"; import { defineConfig } from "vite-plus"; -import type { PackUserConfig } from "vite-plus/pack"; +import { type PackUserConfig } from "vite-plus/pack"; import { webdriverio } from "vite-plus/test/browser-webdriverio"; const rootDir = import.meta.dirname; @@ -136,7 +136,7 @@ const sharedPack = { export default defineConfig({ fmt: { - ignorePatterns: ["dist", "coverage", "docs", "api", ".claude"], + ignorePatterns: ["dist", "coverage", "docs", ".claude"], singleQuote: false, sortImports: true, useTabs: true, @@ -197,8 +197,216 @@ export default defineConfig({ "unicorn/prefer-export-from": "error", "unicorn/prefer-spread": "error", "unicorn/switch-case-braces": "error", + "eslint/max-params": ["error", { max: 4 }], + "eslint/prefer-template": "error", + "eslint/prefer-regex-literals": "error", + "eslint/no-duplicate-imports": "error", + "eslint/operator-assignment": "error", + "eslint/arrow-body-style": "error", + "eslint/no-multi-assign": "error", + "eslint/prefer-spread": "error", + "eslint/no-else-return": "error", + "eslint/no-lonely-if": "error", + "eslint/no-unneeded-ternary": "error", + "eslint/no-useless-return": "error", + "eslint/no-useless-concat": "error", + "eslint/no-useless-computed-key": "error", + "eslint/no-useless-rename": "error", + "eslint/prefer-exponentiation-operator": "error", + "eslint/prefer-numeric-literals": "error", + "eslint/prefer-object-has-own": "error", + "eslint/prefer-promise-reject-errors": "error", + "eslint/require-await": "error", + "eslint/yoda": "error", + "eslint/eqeqeq": "error", + "eslint/no-negated-condition": "error", + "eslint/default-case-last": "error", + "eslint/grouped-accessor-pairs": "error", + "eslint/no-label-var": "error", + "eslint/no-labels": "error", + "eslint/no-sequences": "error", + "eslint/no-throw-literal": "error", + "eslint/radix": "error", + "eslint/symbol-description": "error", + "eslint/no-implicit-coercion": "error", + "typescript/prefer-for-of": "error", + "typescript/method-signature-style": "error", + "typescript/prefer-optional-chain": "error", + "typescript/prefer-nullish-coalescing": "error", + "typescript/prefer-readonly": "error", + "typescript/prefer-string-starts-ends-with": "error", + "typescript/prefer-includes": "error", + "typescript/prefer-regexp-exec": "error", + "typescript/no-unnecessary-type-arguments": "error", + "typescript/no-unnecessary-boolean-literal-compare": "error", + "typescript/switch-exhaustiveness-check": "error", + "typescript/no-inferrable-types": "error", + "typescript/require-array-sort-compare": "error", + "typescript/no-confusing-void-expression": "error", + "typescript/no-meaningless-void-operator": "error", + "typescript/no-redundant-type-constituents": "error", + "typescript/no-useless-empty-export": "error", + "typescript/prefer-reduce-type-parameter": "error", + "typescript/no-empty-object-type": "error", + "unicorn/prefer-negative-index": "error", + "unicorn/consistent-existence-index-check": "error", + "unicorn/prefer-default-parameters": "error", + "unicorn/no-unreadable-array-destructuring": "error", + "unicorn/prefer-string-raw": "error", + "unicorn/consistent-date-clone": "error", + "unicorn/no-await-expression-member": "error", + "unicorn/no-array-reduce": "error", + "unicorn/filename-case": ["error", { case: "kebabCase" }], + "unicorn/catch-error-name": "error", + "unicorn/prefer-array-flat-map": "error", + "unicorn/prefer-array-flat": "error", + "unicorn/prefer-array-some": "error", + "unicorn/prefer-array-find": "error", + "unicorn/prefer-array-index-of": "error", + "unicorn/prefer-includes": "error", + "unicorn/prefer-string-slice": "error", + "unicorn/prefer-string-trim-start-end": "error", + "unicorn/prefer-regexp-test": "error", + "unicorn/prefer-optional-catch-binding": "error", + "unicorn/prefer-set-has": "error", + "unicorn/prefer-set-size": "error", + "unicorn/prefer-logical-operator-over-ternary": "error", + "unicorn/prefer-date-now": "error", + "unicorn/prefer-math-min-max": "error", + "unicorn/prefer-modern-math-apis": "error", + "unicorn/prefer-math-trunc": "error", + "unicorn/prefer-native-coercion-functions": "error", + "unicorn/prefer-number-properties": "error", + "unicorn/prefer-object-from-entries": "error", + "unicorn/prefer-structured-clone": "error", + "unicorn/prefer-type-error": "error", + "unicorn/prefer-global-this": "error", + "unicorn/prefer-node-protocol": "error", + "unicorn/prefer-at": "error", + "unicorn/require-array-join-separator": "error", + "unicorn/require-number-to-fixed-digits-argument": "error", + "unicorn/throw-new-error": "error", + "unicorn/error-message": "error", + "unicorn/no-instanceof-builtins": "error", + "unicorn/no-typeof-undefined": "error", + "unicorn/no-negation-in-equality-check": "error", + "unicorn/no-thenable": "error", + "unicorn/no-unnecessary-await": "error", + "unicorn/no-useless-promise-resolve-reject": "error", + "unicorn/no-useless-spread": "error", + "unicorn/no-useless-fallback-in-spread": "error", + "unicorn/no-useless-length-check": "error", + "unicorn/no-useless-switch-case": "error", + "unicorn/no-lonely-if": "error", + "unicorn/no-negated-condition": "error", + "unicorn/no-object-as-default-parameter": "error", + "unicorn/no-static-only-class": "error", + "unicorn/no-empty-file": "error", + "unicorn/no-hex-escape": "error", + "unicorn/escape-case": "error", + "unicorn/no-new-array": "error", + "unicorn/no-array-method-this-argument": "error", + "unicorn/no-array-callback-reference": "error", + "unicorn/no-length-as-slice-end": "error", + "unicorn/no-magic-array-flat-depth": "error", + "unicorn/no-single-promise-in-promise-methods": "error", + "unicorn/no-await-in-promise-methods": "error", + "unicorn/no-unreadable-iife": "error", + "unicorn/no-anonymous-default-export": "error", + "unicorn/no-abusive-eslint-disable": "error", + "unicorn/explicit-length-check": "error", + "unicorn/consistent-empty-array-spread": "error", + "unicorn/text-encoding-identifier-case": "error", + "unicorn/prefer-import-meta-properties": "error", + "unicorn/prefer-prototype-methods": "error", + "unicorn/prefer-reflect-apply": "error", + "unicorn/no-console-spaces": "error", + "unicorn/no-instanceof-array": "error", + "unicorn/relative-url-style": "error", + "promise/prefer-await-to-then": "error", + "promise/param-names": "error", + "promise/no-return-wrap": "error", + "promise/no-nesting": "error", + "promise/no-promise-in-callback": "error", + "promise/no-callback-in-promise": "error", + "promise/valid-params": "error", + "promise/no-new-statics": "error", + "promise/no-multiple-resolved": "error", + "promise/catch-or-return": "error", + "promise/always-return": "error", + "promise/no-return-in-finally": "error", + "promise/spec-only": "error", + "node/no-exports-assign": "error", + "node/no-new-require": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-param-name": "error", + "jsdoc/require-property": "error", + "jsdoc/require-property-description": "error", + "jsdoc/require-property-name": "error", + "jsdoc/require-property-type": "error", + "jsdoc/require-yields": "error", + "jsdoc/check-access": "error", + "jsdoc/empty-tags": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/no-defaults": "error", + "jsdoc/check-property-names": "error", + "vitest/prefer-describe-function-title": "error", + "vitest/prefer-to-be": "error", + "vitest/prefer-to-have-length": "error", + "vitest/prefer-to-be-object": "error", + "vitest/prefer-strict-equal": "error", + "vitest/prefer-equality-matcher": "error", + "vitest/prefer-comparison-matcher": "error", + "vitest/prefer-called-with": "error", + "vitest/prefer-hooks-in-order": "error", + "vitest/prefer-hooks-on-top": "error", + "vitest/prefer-mock-promise-shorthand": "error", + "vitest/no-alias-methods": "error", + "vitest/no-commented-out-tests": "error", + "vitest/no-duplicate-hooks": "error", + "vitest/no-identical-title": "error", + "vitest/no-interpolation-in-snapshots": "error", + "vitest/no-large-snapshots": "error", + "vitest/no-mocks-import": "error", + "vitest/no-restricted-matchers": "error", + "vitest/no-standalone-expect": "error", + "vitest/no-test-prefixes": "error", + "vitest/no-test-return-statement": "error", + "vitest/consistent-test-it": ["error", { fn: "test" }], + "vitest/consistent-vitest-vi": "error", + "vitest/expect-expect": "error", + "vitest/max-nested-describe": "error", + "vitest/require-to-throw-message": "error", + "vitest/require-top-level-describe": "error", + "vitest/valid-describe-callback": "error", + "vitest/valid-expect": "error", + "vitest/valid-expect-in-promise": "error", + "vitest/no-focused-tests": "error", + "import/consistent-type-specifier-style": ["error", "prefer-inline"], + "import/first": "error", + "import/newline-after-import": "error", + "import/no-absolute-path": "error", + "import/no-amd": "error", + "import/no-commonjs": "error", + "import/no-dynamic-require": "error", + "import/no-empty-named-blocks": "error", + "import/no-mutable-exports": "error", + "import/no-named-default": "error", + "import/no-self-import": "error", + "import/no-webpack-loader-syntax": "error", + "import/no-anonymous-default-export": "error", }, overrides: [ + { + files: ["src/_internals/test/**"], + rules: { + "typescript/consistent-type-definitions": "off", + "vitest/no-disabled-tests": "off", + "vitest/valid-title": "off", + "vitest/warn-todo": "off", + }, + }, { files: ["src/index.ts", "src/index.test.ts"], rules: {