105 lines
6.4 KiB
TypeScript
105 lines
6.4 KiB
TypeScript
// Owner rule: no hand-written SVG. Charts come from visx, icons from Phosphor, the store marks from simple-icons, the
|
|
// journey diagram from Mermaid, the fixture art from tools/fixture-art.ts. This guard fails on inline SVG drawing in
|
|
// the desktop webview, the site and the teaser video (media/teaser), and on any SVG file that is not a known asset.
|
|
// The brand mark is a PNG (tools/brand-assets.py from brand/master/logo-mark.png): no inline brand SVG anywhere.
|
|
// Allowed: the <svg> root a visx chart renders into (files that import @visx/*), the vendored icon
|
|
// files (site/img/icons: Phosphor and simple-icons; apps/desktop/src/ui/store-marks and templates/web: simple-icons;
|
|
// media/teaser/icons: Phosphor), each byte-equal to
|
|
// its package (tools/site-icons.ts), and the Mermaid output (docs/img, site/img journey*.svg).
|
|
import { describe, expect, test } from 'bun:test';
|
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
import { join, relative } from 'node:path';
|
|
import { checkSiteIcons, ICON_DIR, MARK_DIR, SITE_ICONS, STORE_MARKS, TEASER_DIR, TEASER_ICONS, WEB_DIR } from '../tools/site-icons';
|
|
|
|
const ROOT = join(import.meta.dir, '..');
|
|
const walk = (dir: string, keep: (f: string) => boolean): string[] => readdirSync(join(ROOT, dir)).flatMap(n => {
|
|
const rel = join(dir, n);
|
|
if (n === 'node_modules' || n === 'dist') return [];
|
|
return statSync(join(ROOT, rel)).isDirectory() ? walk(rel, keep) : keep(n) ? [rel] : [];
|
|
});
|
|
const read = (f: string) => readFileSync(join(ROOT, f), 'utf8');
|
|
|
|
/** SVG drawing elements and inline SVG images. */
|
|
const SHAPES = /<(path|circle|rect|ellipse|line|polyline|polygon)[\s/>]|data:image\/svg\+xml|<svg[\s>]/g;
|
|
|
|
/** Every hand-drawn hit in a source text: "file:line <tag". */
|
|
export function svgHits(file: string, text: string, opts: { visxRoot?: boolean } = {}): string[] {
|
|
const out: string[] = [];
|
|
for (const m of text.matchAll(SHAPES)) {
|
|
if (opts.visxRoot && /^<svg/.test(m[0])) continue;
|
|
out.push(`${file}:${text.slice(0, m.index).split('\n').length} ${m[0].trim()}`);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe('no hand-written SVG', () => {
|
|
test('desktop webview: charts through visx, icons through Phosphor, no inline SVG at all (the brand mark is a PNG)', () => {
|
|
const files = walk('apps/desktop/src', n => /\.(tsx?|css|html)$/.test(n));
|
|
expect(files.length).toBeGreaterThan(30);
|
|
const hits = files.flatMap(f => {
|
|
const s = read(f);
|
|
return svgHits(f, s, { visxRoot: /from '@visx\//.test(s) });
|
|
});
|
|
expect(hits).toEqual([]);
|
|
// icons only through Phosphor's per-icon modules (tree-shaken), never the lucide set or the whole Phosphor index
|
|
const bad = files.filter(f => /from '(lucide-react|@phosphor-icons\/react)'/.test(read(f)));
|
|
expect(bad).toEqual([]);
|
|
});
|
|
|
|
test('the guard itself catches a drawn icon and an inline SVG image', () => {
|
|
expect(svgHits('x.tsx', 'const a = <svg viewBox="0 0 24 24"><path d="M0 0h1" /></svg>;')).toHaveLength(2);
|
|
expect(svgHits('x.ts', "const u = 'data:image/svg+xml;base64,AAAA';")).toHaveLength(1);
|
|
expect(svgHits('x.tsx', '<svg width={w}><LinePath data={d} /></svg>', { visxRoot: true })).toEqual([]);
|
|
expect(svgHits('x.tsx', '<svg width={w}><circle r={2} /></svg>', { visxRoot: true })).toHaveLength(1);
|
|
});
|
|
|
|
test('site pages: icons are vendored Phosphor / simple-icons files; no inline SVG, the brand mark is a PNG', () => {
|
|
const pages = walk('site', n => n.endsWith('.html'));
|
|
expect(pages.length).toBeGreaterThanOrEqual(2);
|
|
const hits = pages.flatMap(f => svgHits(f, read(f)));
|
|
expect(hits).toEqual([]);
|
|
expect(checkSiteIcons()).toEqual([]);
|
|
for (const f of ['site/style.css', 'site/main.js']) expect(svgHits(f, read(f)), f).toEqual([]);
|
|
});
|
|
|
|
test('teaser video (media/teaser): icons and the mouse cursor are vendored Phosphor files, no inline SVG', () => {
|
|
const files = walk('media/teaser', n => /\.(html|js|ts|css)$/.test(n)).filter(f => !f.startsWith('media/teaser/out/'));
|
|
expect(files.length).toBeGreaterThanOrEqual(5);
|
|
expect(files.flatMap(f => svgHits(f, read(f)))).toEqual([]);
|
|
// every icon the teaser references is one of the vendored Phosphor files
|
|
const used = files.flatMap(f => [...read(f).matchAll(/icons\/([\w-]+\.svg)/g)].map(m => m[1]));
|
|
expect(used.length).toBeGreaterThanOrEqual(TEASER_ICONS.length);
|
|
const vendored: string[] = TEASER_ICONS.map(([w, i]) => `${i}-${w}.svg`);
|
|
for (const n of used) expect(vendored, n).toContain(n ?? '');
|
|
});
|
|
|
|
test('templates (the web init landing page): store marks are the vendored simple-icons files, no inline SVG', () => {
|
|
const files = walk('templates', n => /\.(html|css|js)$/.test(n));
|
|
expect(files).toContain('templates/web/landing.html');
|
|
expect(files.flatMap(f => svgHits(f, read(f)))).toEqual([]);
|
|
});
|
|
|
|
test('SVG files: vendored Phosphor / simple-icons files and Mermaid diagrams only', () => {
|
|
const svgs = ['site', 'docs', 'apps/desktop/src', 'media', 'templates'].flatMap(d => walk(d, n => n.endsWith('.svg'))).map(f => relative(ROOT, join(ROOT, f)));
|
|
const vendored = new Set([
|
|
...[...SITE_ICONS, ...STORE_MARKS].map(n => relative(ROOT, join(ICON_DIR, `${n}.svg`))),
|
|
...STORE_MARKS.map(n => relative(ROOT, join(MARK_DIR, `${n}.svg`))),
|
|
...STORE_MARKS.map(n => relative(ROOT, join(WEB_DIR, `${n}.svg`))),
|
|
...TEASER_ICONS.map(([w, n]) => relative(ROOT, join(TEASER_DIR, `${n}-${w}.svg`))),
|
|
]);
|
|
// byte equality with the packages is checked by checkSiteIcons (above); here: no other SVG file sneaks in
|
|
for (const f of svgs) {
|
|
if (vendored.has(f)) continue;
|
|
expect(f, `${f}: not a known asset`).toMatch(/^(docs|site)\/img\/journey(-tall)?\.svg$/);
|
|
expect(read(f), `${f}: render it with bun tools/diagrams.ts`).toContain('aria-roledescription="flowchart-v2"');
|
|
}
|
|
});
|
|
|
|
test('brand: the mark is the PNG set from tools/brand-assets.py, no SVG brand file', () => {
|
|
const all = ['site', 'apps/desktop/src', 'apps/desktop/src-tauri/icons', 'brand'].flatMap(d => walk(d, n => n.endsWith('.svg')));
|
|
expect(all.filter(f => !/^(site\/img\/icons|apps\/desktop\/src\/ui\/store-marks)\//.test(f) && !/journey(-tall)?\.svg$/.test(f))).toEqual([]);
|
|
for (const f of ['brand/master/logo-mark.png', 'site/favicon.ico', 'site/img/logo-mark.png', 'apps/desktop/src/ui/logo-mark.png', 'apps/desktop/src-tauri/icons/icon.icns']) {
|
|
expect(statSync(join(ROOT, f)).size, f).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|