#!/usr/bin/env node import fs from 'fs'; import os from 'os'; import path from 'path'; import { execFileSync } from 'child_process'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(__dirname, '..'); const iconsDir = path.join(projectRoot, 'build', 'icons'); const publicAssetsDir = path.join(projectRoot, 'public', 'assets'); const sourcePngPath = path.join(publicAssetsDir, 'vlcode-lite-source.png'); const svgPath = path.join(iconsDir, 'vlcode-lite.svg'); const icnsPath = path.join(iconsDir, 'vlcode-lite.icns'); const publicSvgPath = path.join(publicAssetsDir, 'vlcode-lite-icon.svg'); const publicPngPath = path.join(publicAssetsDir, 'vlcode-lite-icon.png'); const publicFavicon32Path = path.join(publicAssetsDir, 'vlcode-lite-favicon-32.png'); const publicFavicon16Path = path.join(publicAssetsDir, 'vlcode-lite-favicon-16.png'); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vlcode-icon-')); const iconsetDir = path.join(tempDir, 'vlcode-lite.iconset'); const iconSizes = [ { size: 16, scale: 1 }, { size: 16, scale: 2 }, { size: 32, scale: 1 }, { size: 32, scale: 2 }, { size: 128, scale: 1 }, { size: 128, scale: 2 }, { size: 256, scale: 1 }, { size: 256, scale: 2 }, { size: 512, scale: 1 }, { size: 512, scale: 2 }, ]; function run(command, args) { execFileSync(command, args, { stdio: 'inherit' }); } function readImageSize(filePath) { const output = execFileSync('sips', ['-g', 'pixelWidth', '-g', 'pixelHeight', filePath], { encoding: 'utf8' }); const width = Number(output.match(/pixelWidth:\s*(\d+)/)?.[1] || 0); const height = Number(output.match(/pixelHeight:\s*(\d+)/)?.[1] || 0); if (!width || !height) { throw new Error(`Could not determine image dimensions for ${filePath}`); } return { width, height }; } function buildSvgFromPng(pngPath) { const canvasSize = 1024; const panelInset = 44; const panelSize = canvasSize - panelInset * 2; const panelRadius = 232; const pngBase64 = fs.readFileSync(pngPath).toString('base64'); const { width, height } = readImageSize(pngPath); const maxWidth = 900; const maxHeight = 672; const scale = Math.min(maxWidth / width, maxHeight / height); const drawWidth = Math.round(width * scale); const drawHeight = Math.round(height * scale); const x = Math.round((canvasSize - drawWidth) / 2); const y = Math.round((canvasSize - drawHeight) / 2); return ` `; } function findRenderedPng(outputDir) { const matches = fs.readdirSync(outputDir) .filter((entry) => entry.endsWith('.png')) .map((entry) => path.join(outputDir, entry)); if (matches.length === 0) { throw new Error('Quick Look did not produce a PNG preview for the SVG icon'); } return matches[0]; } try { fs.mkdirSync(iconsDir, { recursive: true }); fs.mkdirSync(publicAssetsDir, { recursive: true }); if (fs.existsSync(sourcePngPath)) { fs.writeFileSync(svgPath, buildSvgFromPng(sourcePngPath), 'utf8'); } if (!fs.existsSync(svgPath)) { throw new Error(`Missing source SVG: ${svgPath}`); } fs.copyFileSync(svgPath, publicSvgPath); fs.mkdirSync(iconsetDir, { recursive: true }); run('qlmanage', ['-t', '-s', '1024', '-o', tempDir, svgPath]); const basePng = findRenderedPng(tempDir); fs.copyFileSync(basePng, publicPngPath); run('sips', ['-z', '32', '32', basePng, '--out', publicFavicon32Path]); run('sips', ['-z', '16', '16', basePng, '--out', publicFavicon16Path]); for (const icon of iconSizes) { const pixels = icon.size * icon.scale; const suffix = icon.scale === 2 ? '@2x' : ''; const outputPath = path.join(iconsetDir, `icon_${icon.size}x${icon.size}${suffix}.png`); run('sips', ['-z', String(pixels), String(pixels), basePng, '--out', outputPath]); } fs.rmSync(icnsPath, { force: true }); run('iconutil', ['-c', 'icns', iconsetDir, '-o', icnsPath]); console.log(`[build-icon] Wrote ${svgPath}`); console.log(`[build-icon] Wrote ${publicSvgPath}`); console.log(`[build-icon] Wrote ${publicPngPath}`); console.log(`[build-icon] Wrote ${publicFavicon32Path}`); console.log(`[build-icon] Wrote ${publicFavicon16Path}`); console.log(`[build-icon] Wrote ${icnsPath}`); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); }