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);
}
|