#!/usr/bin/env node // Fetches GKM Interactive's company details into a local JSON file, so that an // address is edited in one place instead of in every repository. // // Copy this file into a project unchanged, as scripts/sync-company.mjs, and run // it before the build. Do not add project-specific checks to it — keep those in // a separate script, so this one stays identical everywhere and can be replaced // by re-copying it. // // curl -sfL https://company.gkminteractive.com/sync-company.mjs \ // -o scripts/sync-company.mjs // // Configure with environment variables, not by editing: // // COMPANY_DATA_URL where to fetch from // (default https://company.gkminteractive.com/company.json) // COMPANY_DATA_OUT where to write it, relative to the project root // (default app/data/company.json) // // Commit the written file. It is the snapshot this script falls back to when the // fetch fails — an offline laptop, the Pages project down, a typo in the URL. // Building with slightly stale but valid company details beats failing the // deployment. Building with none at all is not an option, so a failed fetch with // no snapshot present is a hard error. // // Source: https://github.com/GKM-Interactive/company-data import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const URL_ = process.env.COMPANY_DATA_URL ?? "https://company.gkminteractive.com/company.json"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const target = resolve( root, process.env.COMPANY_DATA_OUT ?? join("app", "data", "company.json"), ); // A truncated or misrouted response can still be valid JSON. Overwriting a good // snapshot with it would be worse than keeping the snapshot, so require the // top-level keys before trusting what came back. const REQUIRED = [ "updated", "legalName", "shortName", "legalForm", "address", "register", "vatId", "managingDirector", "contact", "web", "supervisoryAuthority", ]; function usable(data) { return REQUIRED.every((key) => data?.[key] != null); } function keepSnapshot(reason) { if (!existsSync(target)) { console.error( `sync-company: ${reason}, and there is no snapshot at ` + `${target.slice(root.length + 1)} to fall back to.`, ); process.exit(1); } console.warn(`sync-company: ${reason} — keeping the committed snapshot.`); process.exit(0); } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10_000); let response; try { response = await fetch(URL_, { signal: controller.signal }); } catch (error) { clearTimeout(timeout); keepSnapshot(`could not reach ${URL_} (${error.name})`); } clearTimeout(timeout); if (!response.ok) keepSnapshot(`${URL_} returned ${response.status}`); let data; try { data = await response.json(); } catch { keepSnapshot(`${URL_} did not return JSON`); } if (!usable(data)) { keepSnapshot(`${URL_} returned JSON without the expected fields`); } const next = JSON.stringify(data, null, 2) + "\n"; const current = existsSync(target) ? readFileSync(target, "utf8") : null; if (next === current) { console.log(`sync-company: already up to date (updated ${data.updated}).`); process.exit(0); } mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, next, "utf8"); console.log( `sync-company: wrote ${target.slice(root.length + 1)} from ${URL_} ` + `(updated ${data.updated}).`, );