import { readFile, writeFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import sharp from 'sharp'; const ROOT = fileURLToPath(new URL('..', import.meta.url)); const OUTPUT = join(ROOT, 'src', 'brand', 'chickpea-mark.generated.ts'); const MARK_MASTER = join(ROOT, 'assets', 'chickpea-mark.png'); const WORDMARK_MASTER = join(ROOT, 'assets', 'chickpea-wordmark-mask.png'); const PNG_OPTIONS = { compressionLevel: 9, adaptiveFiltering: true, palette: false }; const DERIVATIVES = [ { source: MARK_MASTER, file: 'chickpea-mark-128.png', width: 128, height: 128 }, { source: MARK_MASTER, file: 'chickpea-favicon-32.png', width: 32, height: 32 }, { source: WORDMARK_MASTER, file: 'chickpea-wordmark-512.png', width: 512, height: 126 }, // White-on-dark counterpart per BRAND.md (cocoa on light surfaces, white on // dark); the README's lockup swaps to it under dark color schemes. { source: WORDMARK_MASTER, file: 'chickpea-wordmark-512-dark.png', width: 512, height: 126, tint: '#ffffff' }, ]; const ASSETS = [ { exportName: 'CHICKPEA_MARK_URL', file: 'chickpea-mark-128.png', width: 128, height: 128 }, { exportName: 'CHICKPEA_FAVICON_URL', file: 'chickpea-favicon-32.png', width: 32, height: 32 }, { exportName: 'CHICKPEA_WORDMARK_URL', file: 'chickpea-wordmark-512.png', width: 512, height: 126 }, ]; async function validateMaster(file, width, height, label) { const image = sharp(file); const metadata = await image.metadata(); if (metadata.format !== 'png' || metadata.width !== width || metadata.height !== height) { throw new Error( `${label} must be a ${width}x${height} PNG, got ${metadata.width}x${metadata.height} ${metadata.format}`, ); } return image; } await validateMaster(MARK_MASTER, 512, 512, 'chickpea-mark.png'); const wordmark = await validateMaster( WORDMARK_MASTER, 2106, 518, 'chickpea-wordmark-mask.png', ); const wordmarkStats = await wordmark.ensureAlpha().stats(); const wordmarkAlpha = wordmarkStats.channels[3]; if (wordmarkAlpha.min !== 0 || wordmarkAlpha.max !== 255) { throw new Error('chickpea-wordmark-mask.png must contain transparent and opaque pixels'); } for (const derivative of DERIVATIVES) { const output = join(ROOT, 'assets', derivative.file); if (derivative.tint) { const alpha = await sharp(derivative.source) .resize(derivative.width, derivative.height, { fit: 'fill' }) .ensureAlpha() .extractChannel(3) .toBuffer(); await sharp({ create: { width: derivative.width, height: derivative.height, channels: 3, background: derivative.tint, }, }) .joinChannel(alpha) .png(PNG_OPTIONS) .toFile(output); continue; } await sharp(derivative.source) .resize(derivative.width, derivative.height, { fit: 'fill' }) .png(PNG_OPTIONS) .toFile(output); } const exports = []; for (const asset of ASSETS) { const bytes = await readFile(join(ROOT, 'assets', asset.file)); await validateMaster(join(ROOT, 'assets', asset.file), asset.width, asset.height, asset.file); const version = createHash('sha256').update(bytes).digest('hex').slice(0, 12); exports.push( `export const ${asset.exportName} = ${JSON.stringify(`/${asset.file}?v=${version}`)};`, ); } const source = '// Generated by scripts/build-chickpea-product-mark.mjs. Do not edit by hand.\n' + `${exports.join('\n\n')}\n`; await writeFile(OUTPUT, source); console.log(`Wrote ${ASSETS.length} Chickpea product-mark assets to ${OUTPUT}`);