All files / src/plugins/icons check-icons.ts

0% Statements 0/22
0% Branches 0/8
0% Functions 0/3
0% Lines 0/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71                                                                                                                                             
/**
 * Checks that every face's appinfo media block and committed icon PNGs match what the icons generator
 * would write from icons.json.
 *
 * An icon added to icons.json without a `paf gen <face> icons` run leaves the C side with no RESOURCE_ID for it,
 * and the face fails to build. A size changed without one leaves the old PNG, which draws at the old
 * size. A face that declares icons but has no media array to write them into is reported as well,
 * rather than being skipped.
 *
 * It reads the media helpers from media.ts rather than the rasterizer, so it compares JSON and PNG
 * headers without loading sharp.
 *
 * paf check runs this from the unit when it lists the icons plugin.
 */
import fs from 'node:fs';
import path from 'node:path';
import { appinfoPath, faceDir, listFaceNames } from '../../tools/shared/faces.ts';
import { checkFace, runCheck } from '../../tools/shared/checks.ts';
import { buildMedia, iconPngPath, iconSizeProblems, iconsManifestPath, mediaOf, readPngHeader, replaceMediaArray } from './media.ts';
import type { IconManifest } from './media.ts';
import type { MediaEntry } from '../../tools/manifest/build-manifests.ts';
import { readText } from '../../tools/shared/files.ts';
import { isMainScript } from '../../tools/shared/entry.ts';
 
/**
 * Every face whose media block is stale or missing, or whose icon PNGs are missing or the wrong size.
 *
 * @return One line per problem, empty when every face is current.
 */
export async function checkIcons(): Promise<string[]> {
  const problems: string[] = [];
 
  for (const face of listFaceNames()) {
    await checkFace(face, problems, () => {
      const manifestFile = iconsManifestPath(face);
 
      if (!fs.existsSync(manifestFile)) {
        return;
      }
 
      const raw = readText(appinfoPath(face));
      const media = mediaOf(raw);
 
      if (!Array.isArray(media)) {
        problems.push(`${face}: declares icons in resources/icons.json, but its appinfo has no media array to hold them`);
        return;
      }
 
      const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8')) as IconManifest;
 
      // a stale block and the PNGs it would miss have the one fix, so the PNGs wait for the next run
      if (replaceMediaArray(raw, buildMedia(media as MediaEntry[], manifest)) !== raw) {
        problems.push(`${face}: the appinfo media block is stale, run paf gen ${face} icons`);
        return;
      }
 
      const readPng = (name: string): Buffer | null => readPngHeader(path.join(faceDir(face), iconPngPath(name)));
 
      for (const problem of iconSizeProblems(manifest, readPng)) {
        problems.push(`${face}: ${problem}, run paf gen ${face} icons`);
      }
    });
  }
 
  return problems;
}
 
if (isMainScript(import.meta)) {
  runCheck('icons', checkIcons);
}