#!/usr/bin/env node // The rename checklist in `template/README.md`, checked against the tree. // // A reader's first action is to copy `template/` and make it theirs, and the only // thing telling them where the placeholder name is buried is that table. A // checklist nobody verifies is wrong by the second edit to the template — someone // adds a file, mentions the placeholder id in it, and every reader after that // ships a module with a stray `examplegame` in its OpenAPI tags. // // So this asserts the table and the tree agree, in BOTH directions: // // • every file that still mentions the placeholder is listed, and // • every listed file exists and still mentions it. // // The second half is the one that is easy to leave out and is the more valuable: // an entry that has stopped matching is an entry that will be read as instructions // to edit something that is not there. Same rule the identifier check in core's CI // follows about its own exemptions — an exemption that no longer matches fails the // build rather than being quietly tolerated. // // **Why the placeholder is `examplegame` and not `example`.** This is a whole-file // text search, and `example` appears in ordinary English ("for example") all over // prose that is not a rename site at all. A placeholder that cannot occur by // accident is what makes a check like this answerable rather than a source of // false alarms someone eventually learns to ignore. // // Usage: node scripts/checkRenameSites.js (from the repo root) const fs = require('fs') const path = require('path') const ROOT = path.resolve(__dirname, '..') const TEMPLATE = path.join(ROOT, 'template') const CHECKLIST = path.join(TEMPLATE, 'README.md') // Anything a rename has to touch: the id (`examplegame`), the display name // ("Example Game"), and the placeholder world ("Example World"). One pattern // rather than three, because they are one decision. const PLACEHOLDER = /example[ -]?(game|world)/i // Directories with nothing of ours in them. `dist` and `node_modules` are build // output — a chunk full of the placeholder is not a rename site, it is the // consequence of one. const SKIP_DIRS = new Set(['.git', 'node_modules', 'dist']) // The checklist is the one file exempt from the scan: it is a table OF the // placeholder and would trivially list itself. const SELF = 'README.md' /** Every file under `template/`, template-relative, sorted. */ function templateFiles(dir = TEMPLATE, out = []) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.isDirectory()) { if (SKIP_DIRS.has(entry.name)) continue templateFiles(path.join(dir, entry.name), out) } else if (entry.isFile()) { out.push(path.relative(TEMPLATE, path.join(dir, entry.name)).split(path.sep).join('/')) } } return out.sort() } /** * The paths the checklist names, read from between its two markers. * * Delimited by explicit HTML comments rather than by looking for a heading or for * every backticked path in the document: the README quotes plenty of paths in * prose and in its tree diagram, and none of those are checklist entries. An * explicit marker also means the table can be reformatted freely. */ function checklistPaths(markdown) { const start = markdown.indexOf('') const end = markdown.indexOf('') if (start === -1 || end === -1 || end < start) { throw new Error( 'template/README.md has no block. ' + 'That block is the checklist this check exists to verify.', ) } const table = markdown.slice(start, end) const paths = [] for (const line of table.split('\n')) { // A table row whose first cell is a backticked path. const match = /^\|\s*`([^`]+)`\s*\|/.exec(line.trim()) if (match) paths.push(match[1]) } return paths } /** Everything wrong, as sentences. Empty means the checklist is current. */ function problems({ files, listed, contains }) { const out = [] const listedSet = new Set(listed) const duplicates = listed.filter((p, i) => listed.indexOf(p) !== i) for (const p of new Set(duplicates)) out.push(`${p} is listed in the checklist twice.`) for (const file of files) { if (file === SELF) continue if (!contains(file)) continue if (!listedSet.has(file)) { out.push( `${file} still mentions the placeholder and is NOT in the rename checklist. ` + 'Add a row for it, or take the placeholder out of the file.', ) } } const present = new Set(files) for (const file of listed) { if (!present.has(file)) { out.push(`the checklist lists ${file}, which does not exist. Remove the row or restore the file.`) } else if (!contains(file)) { out.push( `the checklist lists ${file}, which no longer mentions the placeholder. ` + 'A row that has stopped matching tells a reader to edit something that is not there.', ) } } return out } module.exports = { PLACEHOLDER, checklistPaths, problems, templateFiles, TEMPLATE } if (require.main !== module) return if (!fs.existsSync(TEMPLATE)) { console.log('checkRenameSites: no template/ yet — nothing to check') process.exit(0) } const files = templateFiles() const listed = checklistPaths(fs.readFileSync(CHECKLIST, 'utf8')) const contains = (file) => PLACEHOLDER.test(fs.readFileSync(path.join(TEMPLATE, file), 'utf8')) const found = problems({ files, listed, contains }) if (found.length) { console.error(`\n${found.length} problem(s) with the rename checklist in template/README.md:\n`) for (const p of found) console.error(` - ${p}`) console.error('') process.exit(1) } console.log(`OK — the rename checklist matches the template (${listed.length} files).`)