/** * Decide whether a given file is "generated" (regenerated by a build step, * unsafe to write variants into) or "source" (safe to edit, changes persist). * * Why this matters: when the user picks an element on a page whose underlying * file is regenerated by a build step (e.g. `scripts/build-sub-pages.js` * rewriting `public/docs/*.html`), writing variants or accepted changes into * that file is silent data loss — the next build wipes them. * * Signals, in order of reliability: * 1. Git check-ignore: gitignored files are assumed generated. * 2. File-header markers ("GENERATED", "DO NOT EDIT", "AUTO-GENERATED") * within the first ~300 characters — catches non-git projects. */ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; const HEADER_SCAN_BYTES = 300; const HEADER_MARKERS = [ /@generated\b/i, /\bGENERATED\s+FILE\b/, /\bAUTO-?GENERATED\b/i, /\bDO\s+NOT\s+EDIT\b/i, ]; /** * @param {string} filePath - absolute or cwd-relative path * @param {object} [options] * @param {string} [options.cwd] - project root (defaults to process.cwd()) */ export function isGeneratedFile(filePath, options = {}) { const cwd = options.cwd || process.cwd(); const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath); if (isGitIgnored(absPath, cwd)) return true; if (hasGeneratedHeader(absPath)) return true; return false; } function isGitIgnored(absPath, cwd) { try { execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { cwd, stdio: 'ignore', }); return true; // exit 0 = ignored } catch (err) { // Exit code 1 = not ignored. Exit code 128 = not a git repo or other error. // In both cases, treat as "not known to be ignored." return false; } } function hasGeneratedHeader(absPath) { let fd; try { fd = fs.openSync(absPath, 'r'); const buf = Buffer.alloc(HEADER_SCAN_BYTES); const bytesRead = fs.readSync(fd, buf, 0, HEADER_SCAN_BYTES, 0); const head = buf.slice(0, bytesRead).toString('utf-8'); return HEADER_MARKERS.some((re) => re.test(head)); } catch { return false; } finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch {} } } }