All files / src/tools/clay-components check-components.ts

0% Statements 0/24
0% Branches 0/10
0% Functions 0/6
0% Lines 0/22

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 72 73 74 75                                                                                                                                                     
/**
 * Checks that every face's committed Clay components match what the Clay generator would write now.
 *
 * A piece edit without a `paf gen <face> clay` run ships a settings page that ignores the change. Faces are found
 * by their builder manifests, and a face that commits a component the manifests do not account for
 * is reported too, whether it has no manifests or one was renamed, so a lookup that quietly misses a
 * face cannot pass and a dead component cannot ship.
 *
 * paf check runs this from the unit.
 */
import fs from 'node:fs';
import path from 'node:path';
import { faceDir, listFaceNames } from '../shared/faces.ts';
import { checkFace, runCheck } from '../shared/checks.ts';
import { buildComponentSource, findManifests, rootsFor } from './generate-components.ts';
import { readText } from '../shared/files.ts';
import { isMainScript } from '../shared/entry.ts';
 
/** Every bundled component a face commits, by its full path, found by its output rather than its manifests. */
function committedComponents(face: string): string[] {
  const clayDir = path.join(faceDir(face), 'src', 'pkjs', 'clay');
 
  if (!fs.existsSync(clayDir)) {
    return [];
  }
 
  return fs.readdirSync(clayDir).filter((name) => name.endsWith('-component.g.js')).map((name) => path.resolve(clayDir, name));
}
 
/** A path inside a face, from the face's folder and with forward slashes, for a problem line. */
function faceRel(face: string, file: string): string {
  return path.relative(faceDir(face), file).split(path.sep).join('/');
}
 
/**
 * Every stale or unaccounted-for Clay component across the unit's faces.
 *
 * @return One line per problem, empty when every face is current.
 */
export async function checkComponents(): Promise<string[]> {
  const problems: string[] = [];
 
  for (const face of listFaceNames()) {
    await checkFace(face, problems, async () => {
      const roots = rootsFor(face);
      const written = new Set<string>();
 
      for (const manifestPath of findManifests(roots)) {
        const built = await buildComponentSource(manifestPath, roots);
 
        written.add(path.resolve(built.output));
        const committed = fs.existsSync(built.output) ? readText(built.output) : null;
 
        if (committed !== built.source) {
          problems.push(`${face}: ${faceRel(face, built.output)} is stale, run paf gen ${face} clay`);
        }
      }
 
      // the pkjs build copies every *.g.js under src/pkjs, so a component no manifest writes any
      // more still ships. a renamed or dropped manifest leaves one behind
      for (const file of committedComponents(face)) {
        if (!written.has(file)) {
          problems.push(`${face}: ${faceRel(face, file)} is written by no builder manifest, so delete it or bring its manifest back`);
        }
      }
    });
  }
 
  return problems;
}
 
if (isMainScript(import.meta)) {
  runCheck('Clay components', checkComponents);
}