Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .bestpractices.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"osps_sa_01_01_status": "Met",
"osps_sa_01_01_justification": "https://github.com/brazilian-utils/javascript/blob/main/CONTRIBUTING.md#architecture",
"osps_sa_02_01_status": "Met",
"osps_sa_02_01_justification": "The public API is documented at https://brazilian-utils.com.br/utilities and tracked by an API Extractor report: https://github.com/brazilian-utils/javascript/blob/main/reports/api/brazilian-utils.api.md",
"osps_sa_02_01_justification": "The public API is documented at https://brazilian-utils.com.br/utilities and checked on every pull request against the last npm release, which fails on a breaking change and lists every added, removed and changed declaration: https://github.com/brazilian-utils/javascript/blob/main/CONTRIBUTING.md#public-api-validation",
"osps_sa_03_01_status": "Met",
"osps_sa_03_01_justification": "https://github.com/brazilian-utils/javascript/blob/main/SECURITY.md#threat-model",
"osps_vm_01_01_status": "Met",
Expand Down Expand Up @@ -144,7 +144,7 @@
"documentation_basics_status": "Met",
"documentation_basics_justification": "https://brazilian-utils.com.br/getting-started",
"documentation_interface_status": "Met",
"documentation_interface_justification": "https://brazilian-utils.com.br/utilities documents every exported function; the API report is https://github.com/brazilian-utils/javascript/blob/main/reports/api/brazilian-utils.api.md",
"documentation_interface_justification": "https://brazilian-utils.com.br/utilities documents every exported function; API Extractor fails CI on an undocumented export: https://github.com/brazilian-utils/javascript/blob/main/CONTRIBUTING.md#public-api-validation",
"sites_https_status": "Met",
"sites_https_justification": "The site, the repository and the npm package are HTTPS only.",
"discussion_status": "Met",
Expand Down Expand Up @@ -282,7 +282,7 @@
"documentation_quick_start_status": "Met",
"documentation_quick_start_justification": "https://brazilian-utils.com.br/getting-started",
"documentation_current_status": "Met",
"documentation_current_justification": "The docs are checked in CI: llms.txt, the site shells and the API report fail the Check workflow when stale, and a pull request that changes a utility must update both language versions.",
"documentation_current_justification": "The docs are checked in CI: llms.txt and the site shells fail the Check workflow when stale, an undocumented export fails the public API check, and a pull request that changes a utility must update both language versions.",
"documentation_achievements_status": "Met",
"documentation_achievements_justification": "The README links the OpenSSF Best Practices and Scorecard badges: https://github.com/brazilian-utils/javascript#readme",
"accessibility_best_practices_status": "Unmet",
Expand Down
9 changes: 2 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,8 @@ coverage
.env.*
# The SBOM is generated by the release workflow right before publishing.
*.cdx.json
# Every report is generated (stryker, coverage, jscpd) except the API Extractor baseline, which is
# committed so that `npm run check:api` compares the public API against the reviewed one instead of
# writing a new file on every run.
reports/*
!reports/api/
reports/api/*
!reports/api/brazilian-utils.api.md
# Generated reports (Stryker writes its mutation reports there).
reports
# Deno writes a lockfile next to jsr.json on every `deno test`/`deno publish`; the npm lockfile is
# the one that pins this project's dependencies.
deno.lock
Expand Down
72 changes: 58 additions & 14 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ and is invoked through the `npm` scripts below, so you don't need to install any
| `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/<util>/<util>.ts` for one file. |
| `npm run bench` | Runs the `describe("… benchmarks")` blocks with vitest in benchmark mode (`vp test bench --run`); they register nothing in test mode. |
| `npm run check:api` | Builds the package and runs API Extractor over `dist/brazilian-utils.d.ts`: a public type without a doc comment, a type the API refers to without exporting, or a public signature that differs from the committed baseline `reports/api/brazilian-utils.api.md`, fails. |
| `npm run check:api:update` | Rewrites the committed API Extractor baseline `reports/api/brazilian-utils.api.md` from the current build; run it when a public signature changes on purpose and commit the new report. |
| `npm run check:api` | Builds the package and runs `scripts/api.ts`: API Extractor fails on an undocumented public declaration or an unexported type the API refers to, a breaking change against the last release on npm fails, and the API diff is shown ([details](#public-api-validation)). |
| `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). |
| `npm run check:vex` | Checks that every advisory suppressed for `audit-ci` and OSV-Scanner has a `not_affected` statement in `openvex.json`, and nothing else does. |
Expand Down Expand Up @@ -302,16 +301,56 @@ pull request so the CI result is not a surprise.

## Public API validation

[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. It
also compares the public API against the reviewed baseline committed at
`reports/api/brazilian-utils.api.md` (the only file of the ignored `reports/` folder that is
committed) and fails when the two differ, so a change to a public signature has to be reviewed in
the diff of that report: run `npm run check:api:update` to write the new baseline and commit it
along with the change. On top of that, the public signatures are pinned by the
`describe("<name> types")` blocks in the tests, and the `src/index.test.ts` export map catches an
export that goes missing.
`npm run check:api` builds the package and runs `scripts/api.ts`, which CI runs on every pull
request and push to `main`. Nothing it writes lands in the repository: every report goes to a
temporary directory that is deleted at the end, so there is no file to regenerate or commit.

1. [API Extractor](https://api-extractor.com) runs over the bundled `dist/brazilian-utils.d.ts`
with the settings of `api-extractor.json`. It fails when a type the public API refers to is not
itself exported (`ae-forgotten-export`: a consumer could not name it), when an exported
function, type or class has no doc comment (`ae-undocumented`), and on any compiler error in the
bundled declarations.
2. The script downloads the last release (`npm pack @brazilian-utils/brazilian-utils@latest`) and
generates a TypeScript file that only compiles when this build can replace it, type-checked with
the repository's `tsc`. A breaking change fails the job:
- an export of the release (value or type, at the root or in a subpath entry point such as
`@brazilian-utils/brazilian-utils/is-valid-cpf`) that is no longer exported, or a subpath
whose `.js`, `.cjs`, `.d.ts` or `.d.cts` file is no longer built;
- a value that is not assignable to the released one (`const _: typeof Old.x = New.x`): a
parameter that became required or narrower, a new required parameter, a return type that
widened (`boolean` to `boolean | null`), an overload that went away;
- a function whose return type narrowed (`Bank | null` to `Bank`) or whose returned object gained
a required property: a consumer that stored the result in an inferred variable
(`let bank = getBankByCode(code)`) and later assigns the old type to it (`bank = null`) stops
compiling. The returns of all overloads (up to four) are compared as one union;
- an exported type that rejects a value the released one accepts (a removed union member, a
property that became required or narrower, a new required property);
- an exported type that consumers get back (it appears in a return type, a thrown class, or a
type reached from one) or that no function uses, and that now accepts a value the released one
rejects (a new union member, a removed or widened property): code that reads it, such as an
exhaustive `switch`, does not handle the new value. A type that only appears in parameters (an
`*Options` or `*Params` type) may widen freely, and a deprecated alias
(`type GetHolidaysOptions = GetHolidaysParams`) follows the type it renames.

What stays allowed: new exports, new optional parameters, new optional properties, parameters
and input types that accept more values. When `package.json` is already on a higher major
version than the release, breaking changes are listed but do not fail. The check needs network
access and exits with code 2 when the registry cannot be reached; a package that was never
published skips it.

3. The declarations added, removed and changed since the release, taken from the API Extractor
reports of both builds, are printed as a diff and, in CI, added to the job summary of the
`Check` workflow, which is where a reviewer reads the API changes of a pull request. This part
never fails.

The limits of a type-level check: it proves that code which compiled against the release still
compiles, not that it behaves the same (a changed default, a different result for the same input or
a new exception are caught only by the tests), and it assumes consumers use `strict` TypeScript.
Roles are inferred from where a type is written in the signatures, so a type that only appears
inside a callback parameter is treated as an input although it flows out. Generic types (none
today) cannot be named without type arguments and only show up in the diff. On top of that, the
public signatures are pinned by the `describe("<name> types")` blocks in the tests, and the
`src/index.test.ts` export map catches an export that goes missing.

## Supply chain

Expand Down Expand Up @@ -443,7 +482,9 @@ This library is used in production by many projects, so please do not introduce
(renamed/removed exports, changed function signatures, changed default behavior) without first
opening an issue or discussion to align on the approach with maintainers. If a breaking change is
unavoidable, call it out explicitly in the PR description (and use a `feat!`/`fix!` or
`BREAKING CHANGE:` footer in the commit, per Conventional Commits).
`BREAKING CHANGE:` footer in the commit, per Conventional Commits). `npm run check:api` fails on a
breaking change against the last release ([Public API validation](#public-api-validation)) until
`package.json` is on the next major version, so such a pull request is merged deliberately.

## Releasing

Expand Down Expand Up @@ -504,7 +545,7 @@ to what no tool can judge.

**What CI already decided.** A reviewer does not re-check these; a red check is a "not yet":
formatting, lint and types (`vp check`), the tests on every runtime, 100% coverage and mutation
score, duplicated code (jscpd), unused files and exports (knip), the public API report, bundle size
score, duplicated code (jscpd), unused files and exports (knip), the public API check, bundle size
per export (the tree-shaking report), the lockfile, known vulnerabilities (`audit-ci`,
OSV-Scanner), CodeQL, the workflow linters, stale generated files (`llms.txt`, the site shells) and
the commit messages.
Expand All @@ -521,6 +562,9 @@ the commit messages.
`false`, `format*`/`parse*` return `""`, getters return `null` or `[]`, and there is a property
test that says so. No renamed or removed export, no changed default, no narrower accepted input
([Breaking changes](#breaking-changes)); a rename keeps the old name as a `@deprecated` alias.
The job summary of the `Check` workflow lists every public declaration the pull request adds,
removes or changes against the last release; read it to confirm the API change is the intended
one.
3. **Does it fit the project?** One utility per folder named after it, shared logic in
`src/_internals/` instead of a copy, `XxxOptions`/`XxxParams` naming, datasets generated by a
script and never edited by hand, and nothing in the change the pull request does not need.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Brazilian Utils is a zero-dependency library of small utilities for the day-to-d
- **Zero runtime dependencies.** Nothing else lands in your `node_modules` or in your bundle.
- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.4 KB minified (0.8 KB gzipped). Every util is also its own subpath entry, so the heavy ones can be lazy-loaded.
- **Runs everywhere.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno and evergreen browsers, all tested in CI.
- **Written in TypeScript.** Types ship with the package, and an API report tracks the public API so nothing changes silently.
- **Written in TypeScript.** Types ship with the package, and every pull request is checked against the last release so the public API never changes silently.
- **Validated against the official rules.** Every validator cites the specification, law or dataset it implements, and the test suite is mutation-tested, not just covered.
- **Documented in English and Portuguese**, with an `llms.txt` for AI assistants.

Expand Down
4 changes: 2 additions & 2 deletions api-extractor.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"newlineKind": "lf",
"apiReport": {
"enabled": true,
"reportFolder": "<projectFolder>/reports/api/",
"reportTempFolder": "<projectFolder>/reports/api/temp/",
"reportFolder": "<projectFolder>/node_modules/.cache/api-extractor/",
"reportTempFolder": "<projectFolder>/node_modules/.cache/api-extractor/temp/",
"reportFileName": "brazilian-utils.api.md"
},
"docModel": {
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Brazilian Utils is a zero-dependency library of small utilities for the day-to-d
- **Zero runtime dependencies.** Nothing else lands in your `node_modules` or in your bundle.
- **Tree-shakeable, down to the function.** `import { isValidCpf }` costs about 1.4 KB minified (0.8 KB gzipped). Every util is also its own subpath entry, so the heavy ones can be lazy-loaded.
- **Runs everywhere.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno and evergreen browsers, all tested in CI.
- **Written in TypeScript.** Types ship with the package, and an API report tracks the public API so nothing changes silently.
- **Written in TypeScript.** Types ship with the package, and every pull request is checked against the last release so the public API never changes silently.
- **Validated against the official rules.** Every validator cites the specification, law or dataset it implements, and the test suite is mutation-tested, not just covered.
- **Documented in English and Portuguese**, with an `llms.txt` for AI assistants.

Expand Down
2 changes: 1 addition & 1 deletion docs/pt-br/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Brazilian Utils é uma biblioteca de utilitários, sem dependências, para os pr
- **Zero dependências de runtime.** Nada além da biblioteca entra no seu `node_modules` ou no seu bundle.
- **Tree-shakeable até a função.** `import { isValidCpf }` custa cerca de 1,4 KB minificado (0,8 KB com gzip). Cada utilitário também é um subpath próprio, então os pesados podem ser carregados sob demanda.
- **Roda em qualquer lugar.** Node.js `^20.19.0 || >=22.12.0`, Bun, Deno e navegadores modernos, todos testados no CI.
- **Escrita em TypeScript.** Os tipos vêm no pacote, e um relatório de API acompanha a API pública para que nada mude em silêncio.
- **Escrita em TypeScript.** Os tipos vêm no pacote, e todo pull request é comparado com a última versão publicada para que a API pública nunca mude em silêncio.
- **Validada contra as regras oficiais.** Cada validador cita a especificação, lei ou base de dados que implementa, e a suíte de testes passa por mutation testing, não só por cobertura.
- **Documentada em inglês e português**, com um `llms.txt` para assistentes de IA.

Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@
"check:vex": "node ./scripts/vex.ts",
"test:mutation": "stryker run",
"bench": "vp test bench --run",
"check:api": "npm run build && api-extractor run --verbose",
"check:api:update": "npm run build && api-extractor run --local --verbose",
"check:api": "npm run build && node ./scripts/api.ts",
"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",
Expand Down
Loading
Loading