#!/usr/bin/env bun // Vendors icon files, copied byte for byte from their packages, plus the licences: // site/img/icons/ Phosphor (@phosphor-icons/core, MIT, "regular" weight) for the site's icons and the // App Store / Google Play marks from simple-icons (CC0 data) // apps/desktop/src/ui/store-marks the same two store marks for the desktop app (the other desktop icons come from // @phosphor-icons/react) // media/teaser/icons Phosphor icons for the teaser video (the mouse cursor, the inbox, finding and fix icons) // templates/web the two store marks for the landing page `storebound web init --landing` writes // Both show them as CSS masks (site/style.css .ic-*, desktop styles.css .store-mark), so they take the text colour in // both themes and nothing is loaded from outside. The store marks only name the store next to its name; they are // trademarks of Apple Inc. and Google LLC (docs/desktop.md, the site footer). // bun tools/site-icons.ts copy (after a package bump or a new icon in the lists below) // bun tools/site-icons.ts --check fail when a vendored file differs from its package (CI, when installed) import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; export const SITE_ICONS = ['caret-right', 'play', 'user', 'squares-four', 'briefcase'] as const; export const STORE_MARKS = ['appstore', 'googleplay'] as const; /** Teaser icons: [Phosphor weight, icon name], vendored as -.svg (the package's own file name). */ export const TEASER_ICONS = [['fill', 'navigation-arrow'], ['bold', 'check'], ['fill', 'x-circle'], ['fill', 'check-circle'], ['bold', 'tray'], ['fill', 'warning-octagon'], ['fill', 'paper-plane-tilt'], ['bold', 'terminal-window']] as const; const root = new URL('..', import.meta.url).pathname; const phosphor = join(root, 'node_modules/@phosphor-icons/core'); const simple = join(root, 'node_modules/simple-icons'); export const ICON_DIR = join(root, 'site/img/icons'); export const MARK_DIR = join(root, 'apps/desktop/src/ui/store-marks'); export const TEASER_DIR = join(root, 'media/teaser/icons'); export const WEB_DIR = join(root, 'templates/web'); /** [vendored dir, file name, package file] for every vendored icon. */ function plan(): [string, string, string][] { const marks = STORE_MARKS.map(n => [`${n}.svg`, join(simple, 'icons', `${n}.svg`)] as const); return [ ...SITE_ICONS.map(n => [ICON_DIR, `${n}.svg`, join(phosphor, 'assets/regular', `${n}.svg`)] as [string, string, string]), ...marks.map(([f, src]) => [ICON_DIR, f, src] as [string, string, string]), ...marks.map(([f, src]) => [MARK_DIR, f, src] as [string, string, string]), ...marks.map(([f, src]) => [WEB_DIR, f, src] as [string, string, string]), ...TEASER_ICONS.map(([w, n]) => [TEASER_DIR, `${n}-${w}.svg`, join(phosphor, 'assets', w, `${n}-${w}.svg`)] as [string, string, string]), ]; } const rel = (p: string) => p.slice(root.length); /** Problems of the vendored folders against the packages ([] when they match, or when the packages are not installed). */ export function checkSiteIcons(): string[] { if (!existsSync(phosphor) || !existsSync(simple)) return []; const out: string[] = []; const p = plan(); for (const [dir, f, src] of p) { const at = join(dir, f); if (!existsSync(at)) out.push(`${rel(at)} missing (bun tools/site-icons.ts)`); else if (readFileSync(at, 'utf8') !== readFileSync(src, 'utf8')) out.push(`${rel(at)} is not ${rel(src).replace('node_modules/', '')}`); } for (const dir of [ICON_DIR, MARK_DIR, TEASER_DIR, WEB_DIR]) { if (!existsSync(dir)) continue; for (const f of readdirSync(dir)) if (f.endsWith('.svg') && !p.some(([d, n]) => d === dir && n === f)) out.push(`${rel(join(dir, f))} is not a vendored icon`); } return out; } const version = (dir: string) => JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version as string; const simpleNote = () => `App Store and Google Play marks: simple-icons ${version(simple)} (https://simpleicons.org), copied unchanged from\nthe simple-icons package. The icon data is CC0; the marks are trademarks of Apple Inc. and Google LLC and are used\nonly to name the store next to its name. storebound is not affiliated with Apple or Google.\n\n${readFileSync(join(simple, 'LICENSE.md'), 'utf8')}`; if (import.meta.main) { if (!existsSync(phosphor) || !existsSync(simple)) { console.error('site-icons: @phosphor-icons/core or simple-icons is not installed (bun install)'); process.exit(1); } if (process.argv.includes('--check')) { const bad = checkSiteIcons(); for (const b of bad) console.error(`site-icons: ${b}`); process.exit(bad.length ? 1 : 0); } for (const dir of [ICON_DIR, MARK_DIR, TEASER_DIR, WEB_DIR]) mkdirSync(dir, { recursive: true }); for (const [dir, f, src] of plan()) writeFileSync(join(dir, f), readFileSync(src)); writeFileSync(join(ICON_DIR, 'LICENSE.txt'), `Icons in this folder, copied unchanged by tools/site-icons.ts:\n\nPhosphor ${version(phosphor)} (https://phosphoricons.com), regular weight, from the @phosphor-icons/core\npackage.\n\n${readFileSync(join(phosphor, 'LICENSE'), 'utf8')}\n\n${simpleNote()}`); writeFileSync(join(TEASER_DIR, 'LICENSE.txt'), `Icons in this folder, copied unchanged by tools/site-icons.ts:\n\nPhosphor ${version(phosphor)} (https://phosphoricons.com), from the @phosphor-icons/core package.\n\n${readFileSync(join(phosphor, 'LICENSE'), 'utf8')}`); writeFileSync(join(MARK_DIR, 'LICENSE.txt'), `Store marks in this folder, copied unchanged by tools/site-icons.ts.\n\n${simpleNote()}`); console.log(`site-icons: ${SITE_ICONS.length} Phosphor icons ${version(phosphor)} and ${STORE_MARKS.length} simple-icons ${version(simple)} store marks and ${TEASER_ICONS.length} teaser icons -> site/img/icons/, apps/desktop/src/ui/store-marks/, media/teaser/icons/, templates/web/`); }