All files / packages/design-system-2-site/src/lib/zeroheight manifest.js

94.11% Statements 16/17
87.5% Branches 7/8
100% Functions 8/8
100% Lines 15/15

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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104                                                                                                3x       12x               1x 1x                 3x 117x 110x 16x     113x 105x     94x   3x                   1x 1x               1x    
/**
 * Read and write `zeroheight-manifest.json`: one entry per Zeroheight page,
 * keyed by page `uid` (numeric ids change per release; uids look stable).
 *
 * Hand-edited fields survive every run:
 *   - `decision`: `port` | `skip` | `on-hold` (hidden content defaults to `skip`:
 *     unpublished, outdated, or test pages)
 *   - `matchedBy: "manual"` with `localFile` / `tabs[].localFile`: pins a mapping
 *   - `notes`: free text
 *   - top-level `siteOnly`: path prefixes of site-only content (no Zeroheight source)
 */
 
import fs from 'node:fs';
import prettier from 'prettier';
 
/**
 * @typedef {Object} ManifestTab
 * @property {string} name
 * @property {string | null} uid
 * @property {number | null} order
 * @property {boolean} hidden
 * @property {string} [decision]       hidden tabs only
 * @property {string | null} localFile
 * @property {string | null} zhHash
 * @property {string | null} localHash
 *
 * @typedef {Object} ManifestEntry
 * @property {number} id
 * @property {string} name
 * @property {string} category
 * @property {boolean} hidden
 * @property {string} decision
 * @property {'nav' | 'filename' | 'manual' | null} matchedBy
 * @property {string | null} localFile
 * @property {string | null} zhHash
 * @property {string | null} localHash
 * @property {'mcp' | 'api'} hashSource
 * @property {string} lastSynced
 * @property {ManifestTab[]} tabs
 * @property {string} [notes]
 *
 * @typedef {Object} Manifest
 * @property {number | null} releaseId
 * @property {string[]} siteOnly
 * @property {Record<string, ManifestEntry>} pages
 */
 
/** Content written for the site, never in Zeroheight. */
export const DEFAULT_SITE_ONLY = ['content/home.md'];
 
/** @returns {Manifest} */
export function emptyManifest() {
  return { releaseId: null, siteOnly: [...DEFAULT_SITE_ONLY], pages: {} };
}
 
/**
 * @param {string} file
 * @returns {Manifest}
 */
export function readManifest(file) {
  Iif (!fs.existsSync(file)) return emptyManifest();
  return { ...emptyManifest(), ...JSON.parse(fs.readFileSync(file, 'utf8')) };
}
 
/**
 * JSON with object keys sorted (arrays keep their order) so reruns diff cleanly.
 * @param {unknown} value
 */
export function stableStringify(value) {
  /** @param {any} v @returns {any} */
  const sort = (v) => {
    if (Array.isArray(v)) return v.map(sort);
    if (v && typeof v === 'object') {
      return Object.fromEntries(
        Object.keys(v)
          .sort()
          .filter((k) => v[k] !== undefined)
          .map((k) => [k, sort(v[k])]),
      );
    }
    return v;
  };
  return `${JSON.stringify(sort(value), null, 2)}\n`;
}
 
/**
 * Format with the repo's Prettier config so `format:check` passes on the
 * generated file without an ignore entry.
 * @param {string} file
 * @param {unknown} value
 */
export async function writeJson(file, value) {
  const options = await prettier.resolveConfig(file);
  fs.writeFileSync(file, await prettier.format(stableStringify(value), { ...options, filepath: file }));
}
 
/**
 * @param {string} file
 * @param {Manifest} manifest
 */
export function writeManifest(file, manifest) {
  return writeJson(file, manifest);
}