Files

122 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
// Generates components/marks.ts — the monochrome mark worn by each party the
// subprocessor list names.
//
// Run: node scripts/marks.mjs
//
// ONE TABLE below is the whole provenance record: every mark id resolves to
// exactly one source, and the only source is simple-icons (CC0-1.0), the
// canonical monochrome brand set. A party with no entry here carries no mark
// and renders the monogram plate instead. That is deliberate and not a gap:
// simple-icons does not carry every party's mark, and a mark it does not carry
// is one we hold no redistribution licence for. Pin the CURRENT release and
// take what it ships — reaching back to an older one to recover a mark its
// owner had removed is not a licence, and drawing something that looks like it
// is worse. A uniform monogram is the honest fallback; a look-alike is not.
//
// This is hanzo.ai/scripts/gen-marks.mjs's policy, applied to a different
// consumer. The OUTPUT differs and the reason is the deployment: that page
// serves files out of public/, and this one is a static export whose enforced
// policy is `img-src 'self' data:` with no origin to fetch from and no wish to
// add one. So the geometry is emitted as a TypeScript module of path bodies
// and inlined — the same bytes, carried a way the page can actually render.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
const root = join(here, "..");
const ICONS = join(root, "node_modules", "simple-icons", "icons");
// mark id -> simple-icons slug. The id is what a party names in
// subprocessors.json; the slug is what the set files it under, and the two are
// not always the same word, which is the reason this is a table and not a
// derivation.
const SOURCES = {
digitalocean: "digitalocean",
cloudflare: "cloudflare",
openrouter: "openrouter",
anthropic: "anthropic",
deepseek: "deepseek",
google: "google",
github: "github",
square: "square",
stripe: "stripe",
googleads: "googleads",
meta: "meta",
};
// Parties we deliberately leave to the monogram, and the reason. Recorded so
// the absence reads as a decision — the next person to wonder why Slack has no
// mark finds the answer here rather than adding one from somewhere else.
const MONOGRAM = {
slack: "simple-icons no longer carries it",
openai: "simple-icons does not carry it",
twilio: "simple-icons does not carry it",
fireworks: "simple-icons does not carry it",
amazon: "the entry is about a party nothing reaches; a mark would dress it up",
};
if (!existsSync(ICONS)) {
console.error(
`marks: ${ICONS} is not there.\n` +
" simple-icons is the one source and it is a devDependency; install first.",
);
process.exit(1);
}
// The outline data, and only that. The set draws every mark as filled paths on
// a 24-unit box, so the `d` attributes ARE the mark — the element around them
// carries a title and a fill this page supplies itself. Emitting the geometry
// rather than the markup means nothing parses XML in a browser to draw a logo.
const outline = (slug) => {
const from = join(ICONS, `${slug}.svg`);
if (!existsSync(from)) throw new Error(`mark "${slug}": source not found — ${from}`);
const svg = readFileSync(from, "utf8");
const d = [...svg.matchAll(/<path\b[^>]*\bd="([^"]+)"/g)].map((m) => m[1]);
if (!d.length) throw new Error(`mark "${slug}": no path geometry in ${from}`);
// Anything but a path would be dropped silently and the mark would render
// wrong rather than not at all, which is the worse failure.
const other = svg.replace(/<title>[\s\S]*?<\/title>/, "").match(/<(circle|rect|polygon|ellipse|line|polyline)\b/);
if (other) throw new Error(`mark "${slug}": carries a ${other[1]}, which this emitter drops`);
return d;
};
const version = JSON.parse(
readFileSync(join(root, "node_modules", "simple-icons", "package.json"), "utf8"),
).version;
const rows = Object.keys(SOURCES)
.sort()
.map((id) => ` ${id}: ${JSON.stringify(outline(SOURCES[id]))},`)
.join("\n");
const out = `// GENERATED by scripts/marks.mjs — do not edit. Run that script to change it.
//
// Canonical geometry, taken verbatim from simple-icons ${version} (CC0-1.0),
// which is the ONE source. Each entry is the mark's path data on a 24-unit
// box, so every mark shares one coordinate space and one optical weight.
//
// A party absent from this table carries no mark and renders the monogram
// plate. That is the licence boundary, not an oversight: the set does not
// carry every party's mark, and one it does not carry is one we may not
// redistribute. Never hand-draw a substitute — a plate that says nothing is
// honest, and a look-alike is a claim about somebody else's brand.
${Object.keys(MONOGRAM)
.sort()
.map((id) => `// ${id.padEnd(13)} monogram — ${MONOGRAM[id]}`)
.join("\n")}
export const MARKS: Record<string, string[]> = {
${rows}
};
`;
mkdirSync(join(root, "components"), { recursive: true });
writeFileSync(join(root, "components", "marks.ts"), out);
console.log(
`wrote components/marks.ts — ${Object.keys(SOURCES).length} marks from simple-icons ${version}, ` +
`${Object.keys(MONOGRAM).length} deliberate monograms`,
);