import { createHash } from 'node:crypto'; import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { extname, join, relative, resolve } from 'node:path'; const projectRoot = resolve(import.meta.dirname, '..'); const pageFiles = ['vip/fami.html', 'vip/fami.css', 'vip/fami.js']; const assetRoot = join(projectRoot, 'vip/assets/fami'); const manifestPath = join(assetRoot, 'manifest.json'); const forbiddenSourceNodes = new Set([ '3639:15063', // unactivated root frame '3639:15467', // activated root frame '3639:15387', // complete carousel '3639:15791', // complete carousel '3639:15451', // complete bottom bar '3639:15855', // complete bottom bar ]); const forbiddenAssetName = /(?:full[-_ ]?page|whole[-_ ]?page|page[-_ ]?shot|screenshot|figma-vip-(?:full|no-status|content-no-status))/i; const exactSourceNodeSizes = new Map([ ['3639:15349', [30.33, 27.854637]], ['3639:15355', [30.33, 27.030392]], ['3639:15376', [33, 33]], ]); const failures = []; const fail = (message) => failures.push(message); const sha256 = (filePath) => createHash('sha256').update(readFileSync(filePath)).digest('hex'); for (const file of pageFiles) { const absolute = join(projectRoot, file); if (!existsSync(absolute)) { fail(`${file}: page source is missing`); continue; } const source = readFileSync(absolute, 'utf8'); if (forbiddenAssetName.test(source)) { fail(`${file}: references a forbidden whole-page screenshot asset`); } } if (!existsSync(manifestPath)) { fail('vip/assets/fami/manifest.json: layer manifest is missing'); } else { const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); const files = new Set(); for (const asset of manifest.assets ?? []) { const label = asset.file || ''; const absolute = join(assetRoot, label); if (!asset.file || files.has(asset.file)) { fail(`${label}: manifest file path is missing or duplicated`); } files.add(asset.file); if (forbiddenSourceNodes.has(asset.sourceNode)) { fail( `${label}: exported from forbidden aggregate node ${asset.sourceNode}` ); } if (forbiddenAssetName.test(label)) { fail(`${label}: filename indicates a whole-page screenshot`); } if ( asset.containsText !== false || asset.containsControls !== false || asset.containsDynamicMedia !== false ) { fail( `${label}: asset embeds text, controls, or dynamic business media` ); } const exactSourceNodeSize = exactSourceNodeSizes.get(asset.sourceNode); if ( exactSourceNodeSize && (Math.abs( Number(asset.intrinsicSize?.width) - exactSourceNodeSize[0] ) > 0.001 || Math.abs( Number(asset.intrinsicSize?.height) - exactSourceNodeSize[1] ) > 0.001) ) { fail( `${label}: intrinsicSize does not match the Figma source node` ); } if (!existsSync(absolute)) { fail(`${label}: manifest asset is missing`); } else if (sha256(absolute) !== asset.sha256) { fail(`${label}: sha256 does not match the audited export`); } else if (asset.format === 'svg') { const source = readFileSync(absolute, 'utf8'); const viewBox = source.match( /viewBox=["']\s*[-\d.]+\s+[-\d.]+\s+([\d.]+)\s+([\d.]+)\s*["']/i ); const width = Number(viewBox?.[1]); const height = Number(viewBox?.[2]); if ( !viewBox || Math.abs(width - Number(asset.exportViewBox?.width)) > 0.001 || Math.abs(height - Number(asset.exportViewBox?.height)) > 0.001 ) { fail(`${label}: exportViewBox does not match the SVG file`); } } } const exportedFiles = readdirSync(assetRoot, { recursive: true }) .filter((entry) => ['.png', '.svg', '.webp', '.jpg', '.jpeg'].includes(extname(entry)) ) .map((entry) => entry.replaceAll('\\', '/')); for (const file of exportedFiles) { if (!files.has(file)) { fail( `${relative(projectRoot, join(assetRoot, file))}: exported asset is not audited in manifest` ); } if (forbiddenAssetName.test(file)) { fail( `${file}: whole-page screenshot naming is permanently forbidden` ); } } } if (failures.length > 0) { console.error('Fami layer audit failed:\n- ' + failures.join('\n- ')); process.exit(1); } console.log( 'Fami layer audit passed: only audited, independent visual layers are used.' );