Skip to content
Closed
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ but the few rules below hold everywhere:
ViaCEP and the alternatives listed in their docs) over HTTPS through
`src/_internals/fetch-with-retry`, treat every response as untrusted input and never send anything
but the CEP or address being looked up.
- `src/_cli/` is the `brazilian-utils` command (the `bin` of `package.json`), laid out like
`src/_internals/`: one function per folder. It is a generic dispatcher over `src/index.ts`, so a
new utility is a new command with no wiring. `run-cli` is the dispatcher as a pure function
(arguments and stdin in, output and exit code out), `run-bin` connects it to a process, and
`bin/bin.ts` is the three-line Node.js entry, built into `dist/cli.js` by its own pack config in
`vite.config.ts` with the library left external. No library entry point imports it, so it never
reaches a consumer's bundle. Arguments are strings unless `src/_cli/constants.ts` says otherwise:
`CLI_OPTION_KINDS` and `CLI_POSITIONAL_KINDS` list the options and positional arguments that are
a number, a boolean, a list or a date, and `CLI_PARAMS_UTILITIES` the utilities whose first
argument is an options object, which therefore never read stdin on their own. All three types
are derived from the public signatures, so `npm run check` fails when a new non-string option or
argument, or a new params-only utility, is missing from them.
- There are no runtime dependencies (see [Zero runtime dependencies](#zero-runtime-dependencies)),
so the trust boundary of the published package is this repository, its build toolchain and the
npm registry; [MAINTAINERS.md](MAINTAINERS.md) lists who can change what, and
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ The [utilities reference](https://brazilian-utils.com.br/utilities) lists every
- Using an AI coding assistant? The docs are indexed on Context7 as [`/brazilian-utils/javascript`](https://context7.com/brazilian-utils/javascript), and [llms.txt](https://brazilian-utils.com.br/llms.txt) lists every util for other tools. See [AI assistants](https://brazilian-utils.com.br/getting-started?id=ai-assistants).
- The package is tree-shakeable. Every util is also available as its own subpath (e.g. `@brazilian-utils/brazilian-utils/get-cities`) so you can lazy-load the few heavy ones. See [Bundle size](https://brazilian-utils.com.br/getting-started?id=bundle-size).

## Command line

Every utility also runs from a terminal or a shell script, with no install needed:

```bash
npx @brazilian-utils/brazilian-utils isValidCpf 12345678909 # true (exit code 0; 1 for a negative answer)
npx @brazilian-utils/brazilian-utils formatCnpj 12345678000195 --obfuscate # **.345.678/0001-**
npx @brazilian-utils/brazilian-utils getBankByCode 001 # { "code": "001", ... } as JSON
echo 01001000 | npx @brazilian-utils/brazilian-utils getAddressInfoByCep # reads stdin, awaits the lookup
```

The first argument is the name of a utility, the positional values are its arguments and `--key value`, `--flag` or `--json '<object>'` become its options object. `list` prints every utility and `--help` the full usage. See [Command line](https://brazilian-utils.com.br/getting-started?id=command-line).

## Development

This repository uses Vite+ as the local toolchain; it is installed as a dependency, so nothing has
Expand Down
43 changes: 43 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,49 @@ isValidCpf('1232454233345'); // false

The [utilities reference](utilities.md) lists every function, grouped by family, with its options and examples. The [guides](guides/document-field.md) show a CPF field in React, Angular, Vue and plain JavaScript.

## Command line

The package ships a `brazilian-utils` command that runs any utility from a terminal or a shell script, with no install needed:

```bash
npx @brazilian-utils/brazilian-utils isValidCpf 12345678909 # true
npx @brazilian-utils/brazilian-utils generateCpf # 45654643304
npx @brazilian-utils/brazilian-utils formatCnpj 12345678000195 --obfuscate # **.345.678/0001-**
npx @brazilian-utils/brazilian-utils getBankByCode 001 # { "code": "001", "ispb": "00000000", ... }
```

`bunx @brazilian-utils/brazilian-utils` runs the same command, and Deno runs it as `deno run npm:@brazilian-utils/brazilian-utils`, asking for the permissions it needs (only the two CEP lookups reach the network). Once the package is installed in a project it is `npx brazilian-utils`, or plain `brazilian-utils` in a `package.json` script.

The command is a generic dispatcher over the public API: the first argument is the name of a utility, exactly as it is exported, and the rest maps to its arguments.

| You write | The utility receives |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `isValidIe SP 110042490114` | Positional values, in order: `isValidIe("SP", "110042490114")` |
| `--key value`, `--key=value` | An entry of the options (or params) object; `--state-code` and `--stateCode` are the same key |
| `--flag`, `--no-flag` | A boolean option set to `true` or to `false`: `formatCurrency 10 --symbol`, `isBusinessDay 2026-02-17 --no-include-optional` |
| `--json '<object>'` | The options (or params) object as JSON; options given next to it win |
| `-`, or no value in a pipe | The value is read from stdin: `echo 12345678909 \| brazilian-utils formatCpf --obfuscate` |
| `--` | Ends the options, so that a value starting with `--` is read as a value |

A utility whose first argument is an options object, such as `isHoliday`, `getHolidays` or `isValidBankAccount`, never takes a value from stdin on its own: it reads stdin only where you write `-`. So `brazilian-utils isHoliday --target-date 2026-09-07` answers the same inside a script whose own stdin is a file or a pipe.

Values stay strings (so `001` keeps its zeros), except where the utility expects a number, a list (comma separated: `--accept cpf,cnpj`) or a date. Dates are written `YYYY-MM-DD`, mean that local calendar day, and are printed the same way:

```bash
brazilian-utils addBusinessDays 2026-09-04 1 # 2026-09-08
brazilian-utils getHolidays --year 2026 --state-code SP # [{ "name": "Ano novo", "date": "2026-01-01", ... }]
brazilian-utils isValidBankAccount --json '{"bankCode":"001","agency":"1234","account":"12345678","digit":"9"}'
brazilian-utils getAddressInfoByCep 01001000 # awaits the lookup, then prints the address
```

Strings and numbers are printed as they are, a date as its `YYYY-MM-DD` local day (inside JSON too), anything else as JSON. The exit code is `0` on success, `1` when the answer is negative (`false`, `null`, or the empty string a formatter answers with when it cannot read its value) or the utility throws (the error goes to stderr), and `2` when the command line itself is wrong, so a validator works as a shell condition:

```bash
if brazilian-utils isValidCnpj "$CNPJ" > /dev/null; then echo "ok"; fi
```

`brazilian-utils list` prints the name of every utility, `--help` the usage and `--version` the version of the package. The command is a separate file that no entry point of the library imports, so it adds nothing to your bundle.

## AI assistants

The documentation is indexed on Context7 as [`/brazilian-utils/javascript`](https://context7.com/brazilian-utils/javascript). In a coding agent connected to the Context7 MCP server, name the library in the prompt and the agent skips the library search:
Expand Down
43 changes: 43 additions & 0 deletions docs/pt-br/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,49 @@ isValidCpf('1232454233345'); // false

A [referência de utilitários](pt-br/utilities.md) lista todas as funções, agrupadas por família, com opções e exemplos. Os [guias](pt-br/guides/document-field.md) mostram um campo de CPF em React, Angular, Vue e JavaScript puro.

## Linha de comando

O pacote traz o comando `brazilian-utils`, que executa qualquer utilitário a partir do terminal ou de um shell script, sem precisar instalar nada:

```bash
npx @brazilian-utils/brazilian-utils isValidCpf 12345678909 # true
npx @brazilian-utils/brazilian-utils generateCpf # 45654643304
npx @brazilian-utils/brazilian-utils formatCnpj 12345678000195 --obfuscate # **.345.678/0001-**
npx @brazilian-utils/brazilian-utils getBankByCode 001 # { "code": "001", "ispb": "00000000", ... }
```

`bunx @brazilian-utils/brazilian-utils` executa o mesmo comando, e no Deno ele é `deno run npm:@brazilian-utils/brazilian-utils`, que pede as permissões necessárias (só as duas consultas de CEP acessam a rede). Com o pacote instalado no projeto ele vira `npx brazilian-utils`, ou só `brazilian-utils` em um script do `package.json`.

O comando é um despachante genérico sobre a API pública: o primeiro argumento é o nome de um utilitário, exatamente como ele é exportado, e o restante vira os argumentos dele.

| Você escreve | O utilitário recebe |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `isValidIe SP 110042490114` | Valores posicionais, em ordem: `isValidIe("SP", "110042490114")` |
| `--key value`, `--key=value` | Uma entrada do objeto de opções (ou de parâmetros); `--state-code` e `--stateCode` são a mesma chave |
| `--flag`, `--no-flag` | Uma opção booleana com valor `true` ou `false`: `formatCurrency 10 --symbol`, `isBusinessDay 2026-02-17 --no-include-optional` |
| `--json '<objeto>'` | O objeto de opções (ou de parâmetros) em JSON; as opções passadas junto com ele têm prioridade |
| `-`, ou nenhum valor em pipe | O valor é lido da stdin: `echo 12345678909 \| brazilian-utils formatCpf --obfuscate` |
| `--` | Encerra as opções, para que um valor que começa com `--` seja lido como valor |

Um utilitário cujo primeiro argumento é um objeto de opções, como `isHoliday`, `getHolidays` ou `isValidBankAccount`, nunca pega um valor da stdin por conta própria: ele só lê a stdin onde você escreve `-`. Assim `brazilian-utils isHoliday --target-date 2026-09-07` responde a mesma coisa dentro de um script cuja stdin é um arquivo ou um pipe.

Os valores continuam sendo strings (assim `001` mantém os zeros), exceto onde o utilitário espera um número, uma lista (separada por vírgulas: `--accept cpf,cnpj`) ou uma data. Datas são escritas como `YYYY-MM-DD`, representam esse dia do calendário local e são impressas do mesmo jeito:

```bash
brazilian-utils addBusinessDays 2026-09-04 1 # 2026-09-08
brazilian-utils getHolidays --year 2026 --state-code SP # [{ "name": "Ano novo", "date": "2026-01-01", ... }]
brazilian-utils isValidBankAccount --json '{"bankCode":"001","agency":"1234","account":"12345678","digit":"9"}'
brazilian-utils getAddressInfoByCep 01001000 # aguarda a consulta e imprime o endereço
```

Strings e números são impressos como estão, uma data como o seu dia local `YYYY-MM-DD` (também dentro do JSON), e todo o resto como JSON. O código de saída é `0` em caso de sucesso, `1` quando a resposta é negativa (`false`, `null` ou a string vazia que um formatador devolve quando não consegue ler o valor) ou quando o utilitário lança um erro (que vai para a stderr), e `2` quando a própria linha de comando está errada, então um validador funciona como condição no shell:

```bash
if brazilian-utils isValidCnpj "$CNPJ" > /dev/null; then echo "ok"; fi
```

`brazilian-utils list` imprime o nome de todos os utilitários, `--help` o modo de uso e `--version` a versão do pacote. O comando é um arquivo separado que nenhum ponto de entrada da biblioteca importa, então não acrescenta nada ao seu bundle.

## Assistentes de IA

A documentação está indexada no Context7 como [`/brazilian-utils/javascript`](https://context7.com/brazilian-utils/javascript). Em um agente de código conectado ao servidor MCP do Context7, cite a biblioteca no prompt e o agente pula a busca pela biblioteca:
Expand Down
8 changes: 7 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"entry": ["src/index.ts", "src/*/*.ts!", "scripts/*.ts", "stryker.config.json"],
"entry": [
"src/index.ts",
"src/*/*.ts!",
"src/_cli/bin/bin.ts!",
"scripts/*.ts",
"stryker.config.json"
],
"project": ["src/**/*.ts", "scripts/**/*.ts"],
"ignore": ["src/_internals/test/runtime-deno.ts"],
"ignoreDependencies": ["@arethetypeswrong/core", "publint"]
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
"type": "git",
"url": "git+https://github.com/brazilian-utils/javascript.git"
},
"bin": {
"brazilian-utils": "./dist/cli.js"
},
"files": [
"./CHANGELOG.md",
"./brazilian-utils.cdx.json",
Expand Down Expand Up @@ -84,6 +87,7 @@
"default": "./dist/brazilian-utils.umd.cjs"
}
},
"./cli": null,
"./*": {
"import": {
"types": "./dist/*.d.ts",
Expand Down
18 changes: 18 additions & 0 deletions src/_cli/bin/bin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env node
import { fstatSync } from "node:fs";
import process from "node:process";

import packageJson from "../../../package.json" with { type: "json" };
import * as api from "../../index";
import { runBin } from "../run-bin/run-bin";

await runBin({
api,
version: packageJson.version,
process,
isStdinPiped: () => {
const stats = fstatSync(process.stdin.fd);

return stats.isFIFO() || stats.isFile();
},
});
157 changes: 157 additions & 0 deletions src/_cli/coerce-cli-value/coerce-cli-value.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import * as fc from "fast-check";

import { anyText, anyValue } from "../../_internals/test/arbitraries";
import { expectNeverThrows } from "../../_internals/test/properties";
import { describe, expect, expectTypeOf, test } from "../../_internals/test/runtime";
import { type CliValueKind } from "../constants";
import { coerceCliValue } from "./coerce-cli-value";

const KINDS: (CliValueKind | "text" | undefined)[] = [
"boolean",
"date",
"list",
"number",
"text",
undefined,
];

describe("coerceCliValue", () => {
test("should keep text with no kind as it is, leading zeros included", () => {
expect(coerceCliValue("001")).toBe("001");
expect(coerceCliValue("001", "text")).toBe("001");
expect(coerceCliValue("")).toBe("");
expect(coerceCliValue("true")).toBe("true");
});

test("should turn the text of a number into that number", () => {
expect(coerceCliValue("2", "number")).toBe(2);
expect(coerceCliValue("-5", "number")).toBe(-5);
expect(coerceCliValue("1234.56", "number")).toBe(1234.56);
expect(coerceCliValue(" 7 ", "number")).toBe(7);
});

test("should turn text that is not a number into NaN, a blank text included", () => {
expect(Number.isNaN(coerceCliValue("abc", "number"))).toBe(true);
expect(Number.isNaN(coerceCliValue("", "number"))).toBe(true);
expect(Number.isNaN(coerceCliValue(" ", "number"))).toBe(true);
});

test("should read a YYYY-MM-DD date as that local calendar day", () => {
const date = coerceCliValue("2024-12-25", "date") as Date;

expect(date instanceof Date).toBe(true);
expect(date.getFullYear()).toBe(2024);
expect(date.getMonth()).toBe(11);
expect(date.getDate()).toBe(25);
expect(date.getHours()).toBe(0);
});

test("should read a year below 100 as written, not as 19xx", () => {
const date = coerceCliValue("0050-03-04", "date") as Date;

expect(date.getFullYear()).toBe(50);
expect(date.getMonth()).toBe(2);
expect(date.getDate()).toBe(4);
expect(date.getHours()).toBe(0);
expect(date.getMinutes()).toBe(0);
expect(date.getSeconds()).toBe(0);
expect(date.getMilliseconds()).toBe(0);
});

test("should read the last day of a month and a leap day", () => {
const leap = coerceCliValue("2024-02-29", "date") as Date;
const last = coerceCliValue("2024-12-31", "date") as Date;

expect([leap.getFullYear(), leap.getMonth(), leap.getDate()]).toEqual([2024, 1, 29]);
expect([last.getFullYear(), last.getMonth(), last.getDate()]).toEqual([2024, 11, 31]);
});

test("should read a leap day of a year below 100 that 19xx does not have", () => {
const date = coerceCliValue("0000-02-29", "date") as Date;

expect([date.getFullYear(), date.getMonth(), date.getDate()]).toEqual([0, 1, 29]);
expect(date.getHours()).toBe(0);
expect(Number.isNaN((coerceCliValue("0001-02-29", "date") as Date).getTime())).toBe(true);
});

test("should turn anything that is not a real YYYY-MM-DD day into an invalid date", () => {
for (const text of [
"2024-02-30",
"2024-13-01",
"2024-00-10",
"2024-01-00",
"2024-12-32",
"2023-02-29",
"25/12/2024",
"2024-12-25T10:00:00",
" 2024-12-25",
"2024-12-25 ",
"today",
"",
]) {
const date = coerceCliValue(text, "date") as Date;

expect(date instanceof Date).toBe(true);
expect(Number.isNaN(date.getTime())).toBe(true);
}
});

test("should split a list on commas and trim every item", () => {
expect(coerceCliValue("cpf", "list")).toEqual(["cpf"]);
expect(coerceCliValue("cpf,cnpj", "list")).toEqual(["cpf", "cnpj"]);
expect(coerceCliValue(" de , da ,do", "list")).toEqual(["de", "da", "do"]);
});

test("should read true and false as booleans and keep any other text", () => {
expect(coerceCliValue("true", "boolean")).toBe(true);
expect(coerceCliValue("false", "boolean")).toBe(false);
expect(coerceCliValue("yes", "boolean")).toBe("yes");
expect(coerceCliValue("", "boolean")).toBe("");
});

test("should hand back a value that is not text untouched, whatever the kind", () => {
const list = ["cpf"];
const date = new Date(2024, 0, 1);

expect(coerceCliValue(true, "boolean")).toBe(true);
expect(coerceCliValue(false, "number")).toBe(false);
expect(coerceCliValue(3, "number")).toBe(3);
expect(coerceCliValue(3, "date")).toBe(3);
expect(coerceCliValue(list, "list")).toBe(list);
expect(coerceCliValue(date, "date")).toBe(date);
expect(coerceCliValue(null, "list")).toBeNull();
expect((coerceCliValue as unknown as () => unknown)()).toBeUndefined();
});

describe("properties", () => {
test("should never throw, whatever the value and the kind", () => {
for (const kind of KINDS) {
expectNeverThrows((value: unknown) => coerceCliValue(value, kind), anyValue);
}
});

test("should keep any text with no kind unchanged", () => {
fc.assert(
fc.property(anyText, (text) => {
expect(coerceCliValue(text)).toBe(text);
}),
);
});

test("should read back any integer written as text", () => {
fc.assert(
fc.property(fc.integer(), (value) => {
expect(coerceCliValue(String(value), "number")).toBe(value);
}),
);
});
});
});

describe("coerceCliValue types", () => {
test("should take any value and a kind, and return an unknown value", () => {
expectTypeOf(coerceCliValue).parameter(0).toEqualTypeOf<unknown>();
expectTypeOf(coerceCliValue).parameter(1).toEqualTypeOf<CliValueKind | "text" | undefined>();
expectTypeOf(coerceCliValue).returns.toEqualTypeOf<unknown>();
});
});
Loading
Loading