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
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,30 @@ request: the report is still posted, but the check no longer fails. Run `node sc
or `node scripts/tree-shaking.ts --json before.json` before a change and
`node scripts/tree-shaking.ts --compare before.json` after it to preview the same diff.

## Lint and type strictness

`npm check` runs oxlint through Vite+ with the `correctness`, `suspicious`, `perf` and `pedantic`
categories as errors, the `import`, `jsdoc` and `promise` plugins, and a curated set of
`restriction`/`style` rules on top (see `lint.rules` in `vite.config.ts`): explicit return types
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<T>`, 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.

`tsconfig.json` is `strict` plus `noImplicitOverride`, `noUnusedLocals`, `noUnusedParameters` and
`noPropertyAccessFromIndexSignature`. `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`
stay off on purpose: the lookup tables are indexed by digits the code has already validated, so
those flags only add unreachable fallbacks, and every unreachable branch shows up as missing
coverage and as an equivalent mutant. Fix a type error with a real check that returns the same
value the code returned before, never with `!` or `as`.

Two pedantic rules stay off on purpose: `require-unicode-regexp` (the `u` flag changes what a
few escapes mean) and `prefer-code-point`/`prefer-number-coercion` (the digit arithmetic on
`charCodeAt` and `parseInt` is deliberate, and `codePointAt` would add a nullable branch to every
check-digit loop).

## Code quality gates

Three extra gates run in CI next to lint, types and coverage; run them locally before opening a
Expand Down
55 changes: 37 additions & 18 deletions scripts/banks.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
#!/usr/bin/env node

import { readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";

import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts";

const scriptsDir = dirname(fileURLToPath(import.meta.url));
const scriptsDir = import.meta.dirname;

const BACEN_CSV_URL =
"https://www.bcb.gov.br/content/estabilidadefinanceira/str1/ParticipantesSTR.csv";
Expand All @@ -26,6 +25,14 @@ type BrasilApiBank = {
fullName?: string;
};

const isBrasilApiBank = (value: unknown): value is BrasilApiBank =>
typeof value === "object" &&
value !== null &&
(!("ispb" in value) || typeof value.ispb === "string") &&
(!("code" in value) || typeof value.code === "number") &&
(!("name" in value) || typeof value.name === "string") &&
(!("fullName" in value) || typeof value.fullName === "string");

const parseCsvLine = (line: string): string[] => {
const fields: string[] = [];
let current = "";
Expand Down Expand Up @@ -73,7 +80,17 @@ const fetchFromBacen = async (): Promise<BankRow[]> => {
for (const row of rows) {
const [ispb, , code, , , name] = parseCsvLine(row);

if (!ispb || !code || !name || !/^\d{1,3}$/.test(code)) continue;
if (
ispb === undefined ||
ispb === "" ||
code === undefined ||
code === "" ||
name === undefined ||
name === "" ||
!/^\d{1,3}$/.test(code)
) {
continue;
}

banks.push({ code: code.padStart(3, "0"), ispb, name: name.trim() });
}
Expand All @@ -88,31 +105,33 @@ const fetchFromBrasilApi = async (): Promise<BankRow[]> => {
throw new Error(`BrasilAPI banks request failed with status ${response.status}`);
}

const json: BrasilApiBank[] = await response.json();
const json: unknown = await response.json();

if (!Array.isArray(json)) {
throw new TypeError("BrasilAPI banks payload is not an array");
}

const banks: BankRow[] = [];

for (const bank of json) {
if (
typeof bank.code !== "number" ||
!Number.isInteger(bank.code) ||
bank.code < 0 ||
bank.code > 999 ||
!bank.ispb
)
continue;
for (const entry of json) {
if (!isBrasilApiBank(entry) || typeof entry.code !== "number") continue;
if (!Number.isInteger(entry.code) || entry.code < 0 || entry.code > 999) continue;

const ispb = entry.ispb;

if (ispb === undefined || ispb === "") continue;

const name = (bank.fullName ?? bank.name ?? "").trim();
const name = (entry.fullName ?? entry.name ?? "").trim();

if (!name) continue;
if (name === "") continue;

banks.push({ code: String(bank.code).padStart(3, "0"), ispb: bank.ispb, name });
banks.push({ code: String(entry.code).padStart(3, "0"), ispb, name });
}

return banks;
};

const main = async () => {
const main = async (): Promise<void> => {
let banks: BankRow[];
let source: string;

Expand Down
21 changes: 16 additions & 5 deletions scripts/cbo.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
#!/usr/bin/env node

import { writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";

import { fetchSortedRecord } from "../src/_internals/fetch-sorted-record/fetch-sorted-record.ts";

const scriptsDir = dirname(fileURLToPath(import.meta.url));
const scriptsDir = import.meta.dirname;

type CboEntry = {
cbo: string;
descricao: string;
};

const main = async () => {
const isCboEntry = (value: unknown): value is CboEntry =>
typeof value === "object" &&
value !== null &&
"cbo" in value &&
typeof value.cbo === "string" &&
"descricao" in value &&
typeof value.descricao === "string";

const main = async (): Promise<void> => {
const sorted = await fetchSortedRecord(
"https://raw.githubusercontent.com/lucaashoff/lista-cbo-json/main/cbos.json",
"CBO mirror",
async (response) => {
const json: CboEntry[] = await response.json();
const json: unknown = await response.json();

if (!Array.isArray(json) || !json.every((entry) => isCboEntry(entry))) {
throw new Error("CBO mirror payload is not an array of cbo and descricao entries");
}

const data: Record<string, string> = {};

Expand Down
16 changes: 10 additions & 6 deletions scripts/cfop.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
#!/usr/bin/env node

import { writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";

import { fetchSortedRecord } from "../src/_internals/fetch-sorted-record/fetch-sorted-record.ts";

const scriptsDir = dirname(fileURLToPath(import.meta.url));
const scriptsDir = import.meta.dirname;

const EMBEDDED_ENTRY_REGEX = /\s+(\d)\.(\d{3})\s+-\s+/g;

/**
* Some rows of the mirror glue the next code into the description, e.g.
* `1305;"... energia elétrica 1.306 - Aquisição de serviço ..."`, which both corrupts the
* `1305` description and drops `1306`. Splits such a row into one entry per code.
* @param {string} code - The CFOP code the row started with.
* @param {string} description - The row description, possibly containing embedded codes.
* @returns {[string, string][]} One `[code, description]` entry per code found in the row.
*/
const splitEmbeddedEntries = (code: string, description: string): [string, string][] => {
const entries: [string, string][] = [];
Expand All @@ -23,18 +25,18 @@ const splitEmbeddedEntries = (code: string, description: string): [string, strin
for (const match of description.matchAll(EMBEDDED_ENTRY_REGEX)) {
entries.push([
currentCode,
description.slice(lastIndex, match.index).replace(/\s+/g, " ").trim(),
description.slice(lastIndex, match.index).replaceAll(/\s+/g, " ").trim(),
]);
currentCode = `${match[1]}${match[2]}`;
lastIndex = match.index + match[0].length;
}

entries.push([currentCode, description.slice(lastIndex).replace(/\s+/g, " ").trim()]);
entries.push([currentCode, description.slice(lastIndex).replaceAll(/\s+/g, " ").trim()]);

return entries;
};

const main = async () => {
const main = async (): Promise<void> => {
const sorted = await fetchSortedRecord(
"https://raw.githubusercontent.com/jansenfelipe/cfop/master/cfop.csv",
"CFOP mirror",
Expand All @@ -50,6 +52,8 @@ const main = async () => {

const [, code, description] = match;

if (code === undefined || description === undefined) continue;

for (const [entryCode, entryDescription] of splitEmbeddedEntries(code, description)) {
if (entryCode.endsWith("00")) continue;

Expand Down
37 changes: 24 additions & 13 deletions scripts/cities.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
#!/usr/bin/env node

import { writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";

import { DATA as STATES } from "../src/_internals/constants/states.ts";
import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts";

const scriptsDir = dirname(fileURLToPath(import.meta.url));
const scriptsDir = import.meta.dirname;

const STATE_CODES = STATES.map((state) => state.code);

Expand Down Expand Up @@ -47,7 +46,17 @@ type City = {
};
};

const main = async () => {
const isCity = (value: unknown): value is City =>
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "number" &&
"nome" in value &&
typeof value.nome === "string" &&
"microrregiao" in value &&
"regiao-imediata" in value;

const main = async (): Promise<void> => {
const response = await fetchWithRetry(
"https://servicodados.ibge.gov.br/api/v1/localidades/municipios",
);
Expand All @@ -56,21 +65,23 @@ const main = async () => {
throw new Error(`IBGE municipalities request failed with status ${response.status}`);
}

const json: City[] = await response.json();
const json: unknown = await response.json();

if (!Array.isArray(json) || !json.every((entry) => isCity(entry))) {
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;
city.microrregiao?.mesorregiao?.UF?.sigla ??
city["regiao-imediata"]?.["regiao-intermediaria"]?.UF?.sigla;

if (!stateInitials) return acc;
if (stateInitials === undefined || stateInitials === "") return acc;

if (!acc[stateInitials]) {
acc[stateInitials] = [];
}
const cityNames = (acc[stateInitials] ??= []);

acc[stateInitials].push([city.nome, String(city.id)]);
cityNames.push([city.nome, String(city.id)]);

return acc;
},
Expand All @@ -91,7 +102,7 @@ const main = async () => {
})
.join("\n");

const missingStates = STATE_CODES.filter((code) => !byState[code]);
const missingStates = STATE_CODES.filter((code) => !Object.hasOwn(byState, code));

if (missingStates.length > 0) {
throw new Error(`IBGE response is missing municipalities for: ${missingStates.join(", ")}`);
Expand Down
21 changes: 16 additions & 5 deletions scripts/cnae.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
#!/usr/bin/env node

import { writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";

import { fetchWithRetry } from "../src/_internals/fetch-with-retry/fetch-with-retry.ts";

const scriptsDir = dirname(fileURLToPath(import.meta.url));
const scriptsDir = import.meta.dirname;

type CnaeSubclass = {
id: string;
descricao: string;
};

const main = async () => {
const isCnaeSubclass = (value: unknown): value is CnaeSubclass =>
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof value.id === "string" &&
"descricao" in value &&
typeof value.descricao === "string";

const main = async (): Promise<void> => {
const response = await fetchWithRetry("https://servicodados.ibge.gov.br/api/v2/cnae/subclasses");

if (!response.ok) {
throw new Error(`IBGE CNAE request failed with status ${response.status}`);
}

const json: CnaeSubclass[] = await response.json();
const json: unknown = await response.json();

if (!Array.isArray(json) || !json.every((entry) => isCnaeSubclass(entry))) {
throw new Error("IBGE CNAE payload is not an array of subclass entries");
}

const entries = json
.filter((subclass) => /^\d{7}$/.test(subclass.id))
Expand Down
13 changes: 8 additions & 5 deletions scripts/data.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
#!/usr/bin/env node

import { spawn } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";

const scriptsDir = dirname(fileURLToPath(import.meta.url));
const scriptsDir = import.meta.dirname;

const run = (command: string, args: string[]): Promise<number | null> =>
new Promise((resolveExit) => {
const child = spawn(command, args, {
stdio: "inherit",
});

child.on("close", (code) => resolveExit(code));
child.on("error", () => resolveExit(1));
child.on("close", (code) => {
resolveExit(code);
});
child.on("error", () => {
resolveExit(1);
});
});

const generators = [
Expand Down
Loading
Loading