From 36486007ccada8c31d30e8f528d21233cabce32c Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Sun, 26 Jul 2026 15:01:02 -0300 Subject: Initial import --- .gitignore | 5 + .prettierignore | 3 + .prettierrc.json | 3 + LICENSE | 15 ++ Makefile | 39 ++++ README.md | 232 +++++++++++++++++++ demo.html | 160 ++++++++++++++ package.json | 49 ++++ scripts/size.js | 15 ++ src/global.js | 32 +++ src/index.js | 614 +++++++++++++++++++++++++++++++++++++++++++++++++++ test/browser.test.js | 336 ++++++++++++++++++++++++++++ test/fixture.html | 62 ++++++ test/server.js | 171 ++++++++++++++ 14 files changed, 1736 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 demo.html create mode 100644 package.json create mode 100644 scripts/size.js create mode 100644 src/global.js create mode 100644 src/index.js create mode 100644 test/browser.test.js create mode 100644 test/fixture.html create mode 100644 test/server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26be789 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.log +.DS_Store +bun.lock diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..190ddbf --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +bun.lock diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..de753c5 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 100 +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e9104d --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2026 Murilo Ijanc + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3545904 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +BUN ?= bun + +.DEFAULT_GOAL := help +.PHONY: help install test lint format build check clean + +help: + @echo 'Targets:' + @echo ' install install the development dependencies' + @echo ' test run the browser test suite (bun test + Chromium)' + @echo ' lint prettier --check + a bundle syntax check (read-only)' + @echo ' format apply prettier (writes)' + @echo ' build bundle to dist/ and report the gzipped size' + @echo ' check lint + test + build, in that order' + @echo ' clean remove build artefacts' + +node_modules: package.json + $(BUN) install + @touch node_modules + +install: node_modules + +test: node_modules + $(BUN) test + +lint: node_modules + $(BUN) x prettier --check . + $(BUN) build ./src/global.js --target browser --format iife > /dev/null + +format: node_modules + $(BUN) x prettier --write . + +build: node_modules + rm -rf dist + $(BUN) run build + +check: lint test build + +clean: + rm -rf dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..242859a --- /dev/null +++ b/README.md @@ -0,0 +1,232 @@ +# cepis-autofill + +Drop-in address autofill for Brazilian checkouts, on top of the +[public CEP API at cep.is](https://cep.is). The shopper types a CEP; the +widget fills the street, neighbourhood, city and state fields you point it +at. Official Correios data (DNE). + +One ` + +``` + +Nothing about any particular e-commerce platform is baked in: which field +is which is configuration. See +[Mapping your platform's fields](#mapping-your-platforms-fields). + +## Install + +Copy `dist/cepis-autofill.min.js` next to your theme's other assets and load +it with a ` + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..798ab4b --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "cepis-autofill", + "version": "0.1.0", + "description": "Drop-in address autofill widget for Brazilian checkouts, on top of the CEP API at cep.is", + "keywords": [ + "cep", + "correios", + "checkout", + "autofill", + "address", + "brasil", + "brazil", + "widget" + ], + "homepage": "https://cep.is", + "license": "ISC", + "author": "Murilo Ijanc ", + "type": "module", + "sideEffects": [ + "./src/global.js" + ], + "main": "./dist/cepis-autofill.esm.js", + "module": "./dist/cepis-autofill.esm.js", + "unpkg": "./dist/cepis-autofill.min.js", + "jsdelivr": "./dist/cepis-autofill.min.js", + "exports": { + ".": "./dist/cepis-autofill.esm.js", + "./package.json": "./package.json" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "scripts": { + "test": "bun test", + "lint": "prettier --check .", + "format": "prettier --write .", + "build": "bun run build:iife && bun run build:min && bun run build:esm && bun run size", + "build:iife": "bun build ./src/global.js --target browser --format iife --outfile dist/cepis-autofill.js", + "build:min": "bun build ./src/global.js --target browser --format iife --minify --outfile dist/cepis-autofill.min.js", + "build:esm": "bun build ./src/index.js --target browser --format esm --outfile dist/cepis-autofill.esm.js", + "size": "bun run scripts/size.js" + }, + "devDependencies": { + "playwright-core": "^1.62.0", + "prettier": "^3.9.6" + } +} diff --git a/scripts/size.js b/scripts/size.js new file mode 100644 index 0000000..7583e86 --- /dev/null +++ b/scripts/size.js @@ -0,0 +1,15 @@ +import { gzipSync } from "bun"; + +const files = [ + "dist/cepis-autofill.js", + "dist/cepis-autofill.min.js", + "dist/cepis-autofill.esm.js", +]; + +for (const path of files) { + const bytes = await Bun.file(path).bytes(); + const gzipped = gzipSync(bytes).length; + console.log( + `${path.padEnd(28)} ${String(bytes.length).padStart(6)} B ${String(gzipped).padStart(5)} B gzipped`, + ); +} diff --git a/src/global.js b/src/global.js new file mode 100644 index 0000000..217a9a0 --- /dev/null +++ b/src/global.js @@ -0,0 +1,32 @@ +/** + * Entry point for the ` + + + diff --git a/test/server.js b/test/server.js new file mode 100644 index 0000000..720f4a0 --- /dev/null +++ b/test/server.js @@ -0,0 +1,171 @@ +/** + * A fake cep.is, stood up with `Bun.serve` so the browser tests never touch the + * network. Same shapes and status codes as the real API, plus the counters the + * tests assert on. + * + * @module + */ + +const ADDRESSES = { + "01001000": { + cep: "01001000", + logradouro: "Praça da Sé", + complemento: "lado ímpar", + unidade: "", + bairro: "Sé", + localidade: "São Paulo", + uf: "SP", + estado: "São Paulo", + regiao: "Sudeste", + ibge: "3550308", + gia: "1004", + ddd: "11", + siafi: "7107", + geo: { lat: -23.567202, lon: -46.649659, raio_m: 1084, precisao: "logradouro" }, + }, + 20040020: { + cep: "20040020", + logradouro: "Avenida Rio Branco", + complemento: "até 0999 - lado ímpar", + unidade: "", + bairro: "Centro", + localidade: "Rio de Janeiro", + uf: "RJ", + estado: "Rio de Janeiro", + regiao: "Sudeste", + ibge: "3304557", + gia: "", + ddd: "21", + siafi: "6001", + }, +}; + +const CORS = { + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json; charset=utf-8", +}; + +/** + * Start the fake API on an ephemeral port. + * + * The returned `state` is live: set `state.delays[cep]` to hold a response + * back, or `state.force` to `{status}` to make every route fail. `requests` + * lists every CEP asked for, and `aborted` counts the requests the browser + * dropped mid-flight. + * + * @returns {{url: string, state: {requests: string[], aborted: number, delays: Record, force: {status: number}|null}, reset: () => void, stop: () => void}} + */ +export function startApi() { + const state = { requests: [], aborted: 0, delays: {}, force: null }; + + const server = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + async fetch(request) { + const path = new URL(request.url).pathname.slice(1); + state.requests.push(path); + request.signal.addEventListener("abort", () => { + state.aborted += 1; + }); + + const delay = state.delays[path] ?? 0; + if (delay > 0) { + await Bun.sleep(delay); + } + if (request.signal.aborted) { + return new Response(null, { status: 499 }); + } + + if (state.force) { + return new Response(JSON.stringify({ erro: "interno", mensagem: "falha interna" }), { + status: state.force.status, + headers: { + ...CORS, + ...(state.force.status === 429 ? { "Retry-After": "3" } : {}), + }, + }); + } + if (!/^\d{8}$/.test(path)) { + return new Response( + JSON.stringify({ erro: "cep_invalido", mensagem: "CEP deve ter 8 dígitos" }), + { status: 400, headers: CORS }, + ); + } + if (path === "88888888") { + return new Response(JSON.stringify({ erro: "interno", mensagem: "falha interna" }), { + status: 500, + headers: CORS, + }); + } + if (path === "77777777") { + return new Response(JSON.stringify({ erro: "limite", mensagem: "muitas consultas" }), { + status: 429, + headers: { ...CORS, "Retry-After": "3" }, + }); + } + const address = ADDRESSES[path]; + if (!address) { + return new Response( + JSON.stringify({ erro: "cep_nao_encontrado", mensagem: "CEP não existe na base do DNE" }), + { status: 404, headers: CORS }, + ); + } + return new Response(JSON.stringify(address), { status: 200, headers: CORS }); + }, + }); + + return { + url: `http://127.0.0.1:${server.port}`, + state, + reset() { + state.requests.length = 0; + state.aborted = 0; + state.delays = {}; + state.force = null; + }, + stop() { + server.stop(true); + }, + }; +} + +/** + * Serve the checkout fixture page together with a freshly built `