open-design/scripts/sync-design-systems.ts
Zakaria a46764fb1b
Some checks failed
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
first-commit
2026-05-04 14:58:14 -04:00

155 lines
5.5 KiB
JavaScript

#!/usr/bin/env node
// Sync design-systems/* from the upstream `getdesign` npm package.
//
// Usage:
// 1) curl -sL $(npm view getdesign dist.tarball) -o /tmp/getdesign.tgz
// tar -xzf /tmp/getdesign.tgz -C /tmp
// 2) node --experimental-strip-types scripts/sync-design-systems.ts [/tmp/package/templates]
//
// The script re-creates each brand's design-systems/<slug>/DESIGN.md with a
// `> Category: <name>` line inserted after the H1, mapped from the
// awesome-design-md README. Hand-authored systems (default, warm-editorial)
// are left untouched.
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
interface ManifestEntry {
brand: string;
file: string;
description: string;
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const SRC = process.argv[2] || '/tmp/package/templates';
const CATEGORY = {
// AI & LLM
claude: 'AI & LLM', cohere: 'AI & LLM', elevenlabs: 'AI & LLM',
minimax: 'AI & LLM', 'mistral.ai': 'AI & LLM', ollama: 'AI & LLM',
'opencode.ai': 'AI & LLM', replicate: 'AI & LLM', runwayml: 'AI & LLM',
'together.ai': 'AI & LLM', voltagent: 'AI & LLM', 'x.ai': 'AI & LLM',
// Developer Tools
cursor: 'Developer Tools', expo: 'Developer Tools', lovable: 'Developer Tools',
raycast: 'Developer Tools', superhuman: 'Developer Tools',
vercel: 'Developer Tools', warp: 'Developer Tools',
// Backend & Data
clickhouse: 'Backend & Data', composio: 'Backend & Data',
hashicorp: 'Backend & Data', mongodb: 'Backend & Data',
posthog: 'Backend & Data', sanity: 'Backend & Data',
sentry: 'Backend & Data', supabase: 'Backend & Data',
// Productivity & SaaS
cal: 'Productivity & SaaS', intercom: 'Productivity & SaaS',
'linear.app': 'Productivity & SaaS', mintlify: 'Productivity & SaaS',
notion: 'Productivity & SaaS', resend: 'Productivity & SaaS',
zapier: 'Productivity & SaaS',
// Design & Creative
airtable: 'Design & Creative', clay: 'Design & Creative',
figma: 'Design & Creative', framer: 'Design & Creative',
miro: 'Design & Creative', webflow: 'Design & Creative',
// Fintech & Crypto
binance: 'Fintech & Crypto', coinbase: 'Fintech & Crypto',
kraken: 'Fintech & Crypto', mastercard: 'Fintech & Crypto',
revolut: 'Fintech & Crypto', stripe: 'Fintech & Crypto', wise: 'Fintech & Crypto',
// E-Commerce & Retail
airbnb: 'E-Commerce & Retail', meta: 'E-Commerce & Retail',
nike: 'E-Commerce & Retail', shopify: 'E-Commerce & Retail',
starbucks: 'E-Commerce & Retail',
// Media & Consumer
apple: 'Media & Consumer', ibm: 'Media & Consumer',
nvidia: 'Media & Consumer', pinterest: 'Media & Consumer',
playstation: 'Media & Consumer', spacex: 'Media & Consumer',
spotify: 'Media & Consumer', theverge: 'Media & Consumer',
uber: 'Media & Consumer', vodafone: 'Media & Consumer', wired: 'Media & Consumer',
// Automotive
bmw: 'Automotive', bugatti: 'Automotive', ferrari: 'Automotive',
lamborghini: 'Automotive', renault: 'Automotive', tesla: 'Automotive',
} as const;
type Brand = keyof typeof CATEGORY;
const slugOf = (brand: string): string => brand.replace(/\./g, '-');
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function readManifest(): ManifestEntry[] {
const raw = readFileSync(path.join(SRC, 'manifest.json'), 'utf8');
const parsed: unknown = JSON.parse(raw);
if (!Array.isArray(parsed)) {
throw new Error('manifest.json must contain an array');
}
return parsed.map((entry) => {
if (
typeof entry === 'object' &&
entry !== null &&
'brand' in entry &&
'file' in entry &&
'description' in entry &&
typeof entry.brand === 'string' &&
typeof entry.file === 'string' &&
typeof entry.description === 'string'
) {
return entry;
}
throw new Error('manifest.json contains an invalid entry');
});
}
function main(): void {
let manifest: ManifestEntry[];
try {
manifest = readManifest();
} catch (error) {
console.error(`Could not read manifest.json under ${SRC}: ${errorMessage(error)}`);
console.error('Did you extract the getdesign tarball? See scripts/sync-design-systems.ts header.');
process.exit(1);
}
const written: string[] = [];
const skipped: string[] = [];
for (const entry of manifest) {
const { brand, file, description } = entry;
const cat = CATEGORY[brand as Brand];
if (!cat) { skipped.push(`${brand} (unmapped category)`); continue; }
const slug = slugOf(brand);
let raw: string;
try {
raw = readFileSync(path.join(SRC, file), 'utf8');
} catch (error) {
skipped.push(`${brand} (${errorMessage(error)})`);
continue;
}
const lines = raw.split(/\r?\n/);
const h1 = lines.findIndex((line) => /^#\s+/.test(line));
if (h1 < 0) { skipped.push(`${brand} (no H1)`); continue; }
const head = lines.slice(0, h1 + 1);
const tail = lines.slice(h1 + 1);
while (tail[0] === '') tail.shift();
const body = [
...head,
'',
`> Category: ${cat}`,
`> ${description}`,
'',
...tail,
].join('\n');
const dir = path.join(ROOT, 'design-systems', slug);
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, 'DESIGN.md'), body);
written.push(slug);
}
console.log(`wrote ${written.length} design systems → design-systems/`);
if (skipped.length) {
console.log('skipped:');
for (const entry of skipped) console.log(` - ${entry}`);
}
}
main();