| 1 | /** |
| 2 | * Decide whether a given file is "generated" (regenerated by a build step, |
| 3 | * unsafe to write variants into) or "source" (safe to edit, changes persist). |
| 4 | * |
| 5 | * Why this matters: when the user picks an element on a page whose underlying |
| 6 | * file is regenerated by a build step (e.g. `scripts/build-sub-pages.js` |
| 7 | * rewriting `public/docs/*.html`), writing variants or accepted changes into |
| 8 | * that file is silent data loss — the next build wipes them. |
| 9 | * |
| 10 | * Signals, in order of reliability: |
| 11 | * 1. Git check-ignore: gitignored files are assumed generated. |
| 12 | * 2. File-header markers ("GENERATED", "DO NOT EDIT", "AUTO-GENERATED") |
| 13 | * within the first ~300 characters — catches non-git projects. |
| 14 | */ |
| 15 | |
| 16 | import { execSync } from 'node:child_process'; |
| 17 | import fs from 'node:fs'; |
| 18 | import path from 'node:path'; |
| 19 | |
| 20 | const HEADER_SCAN_BYTES = 300; |
| 21 | const HEADER_MARKERS = [ |
| 22 | /@generated\b/i, |
| 23 | /\bGENERATED\s+FILE\b/, |
| 24 | /\bAUTO-?GENERATED\b/i, |
| 25 | /\bDO\s+NOT\s+EDIT\b/i, |
| 26 | ]; |
| 27 | |
| 28 | /** |
| 29 | * @param {string} filePath - absolute or cwd-relative path |
| 30 | * @param {object} [options] |
| 31 | * @param {string} [options.cwd] - project root (defaults to process.cwd()) |
| 32 | */ |
| 33 | export function isGeneratedFile(filePath, options = {}) { |
| 34 | const cwd = options.cwd || process.cwd(); |
| 35 | const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath); |
| 36 | |
| 37 | if (isGitIgnored(absPath, cwd)) return true; |
| 38 | if (hasGeneratedHeader(absPath)) return true; |
| 39 | return false; |
| 40 | } |
| 41 | |
| 42 | function isGitIgnored(absPath, cwd) { |
| 43 | try { |
| 44 | execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, { |
| 45 | cwd, |
| 46 | stdio: 'ignore', |
| 47 | }); |
| 48 | return true; // exit 0 = ignored |
| 49 | } catch (err) { |
| 50 | // Exit code 1 = not ignored. Exit code 128 = not a git repo or other error. |
| 51 | // In both cases, treat as "not known to be ignored." |
| 52 | return false; |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | function hasGeneratedHeader(absPath) { |
| 57 | let fd; |
| 58 | try { |
| 59 | fd = fs.openSync(absPath, 'r'); |
| 60 | const buf = Buffer.alloc(HEADER_SCAN_BYTES); |
| 61 | const bytesRead = fs.readSync(fd, buf, 0, HEADER_SCAN_BYTES, 0); |
| 62 | const head = buf.slice(0, bytesRead).toString('utf-8'); |
| 63 | return HEADER_MARKERS.some((re) => re.test(head)); |
| 64 | } catch { |
| 65 | return false; |
| 66 | } finally { |
| 67 | if (fd !== undefined) { try { fs.closeSync(fd); } catch {} } |
| 68 | } |
| 69 | } |
| 70 |