mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-10-27 01:52:52 +08:00
165 lines
7.6 KiB
JavaScript
165 lines
7.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/* @mirror — turn duplication you cannot delete into duplication that cannot ROT.
|
|
*
|
|
* Some duplication is forced rather than chosen: CSS cannot share a declaration block across two
|
|
* mutually exclusive guards — the stacked table's card layout is needed under a CLASS (.fs-stacked,
|
|
* the measured data table) AND under a CONTAINER QUERY (the config table, which cannot be measured;
|
|
* see fs-select.js). A script fetched with `curl | sh` and running BEFORE the package exists cannot
|
|
* source a library shipping inside that package, yet has to do exactly what it does.
|
|
*
|
|
* The trap: a structural duplicate detector (tools/css-dup.mjs) matches bodies that are IDENTICAL,
|
|
* so it goes QUIET the moment two copies diverge — exactly when it should shout. Not hypothetical:
|
|
* the fetch helper existed twice and had ALREADY drifted three ways (different backend order, one
|
|
* with no timeout on its first-choice tool, one missing the https redirect pin) and nothing said a
|
|
* word. So: tag every copy, assert they stay byte-identical. */
|
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
import { join, relative } from 'node:path';
|
|
|
|
import { ROOT } from './lib/root.mjs';
|
|
|
|
/* Whole FILES pinned byte-identical — the same argument one level up. The Apache-2.0 text must
|
|
* exist twice and neither copy can go:
|
|
* LICENSE the repo root, the only place GitHub looks
|
|
* luci-theme-footstrap/LICENSE the PACKAGE: PKG_LICENSE_FILES resolves against
|
|
* $(PKG_BUILD_DIR), into which only the package source dirs are
|
|
* copied, so the root file is unreachable from $(CURDIR).
|
|
* A licence text cannot carry an @mirror comment without ceasing to be the licence text. */
|
|
const SAME_FILE = [
|
|
['LICENSE', 'luci-theme-footstrap/LICENSE'],
|
|
];
|
|
|
|
/* Where a mirror may live. Explicit on purpose: a mirror is a decision, and a glob picking up a
|
|
* new tree would let one appear without anybody choosing it. */
|
|
const SEARCH = [
|
|
'luci-theme-footstrap/styles',
|
|
'luci-theme-footstrap/root',
|
|
'luci-theme-footstrap/htdocs/luci-static/resources',
|
|
'luci-theme-footstrap/Makefile',
|
|
/* install.sh is still searched and today holds no pin at all — the shell helpers it once mirrored
|
|
* belonged to a self-update package that is retired and archived. Left in SEARCH so a pin added
|
|
* here later is picked up, rather than removed and forgotten. */
|
|
'install.sh',
|
|
];
|
|
const EXT = /\.(css|js|sh|ut)$|(^|\/)(Makefile|30_luci-theme-footstrap)$/;
|
|
|
|
/* SEARCH mixes directories with single files (Makefile, install.sh), so one isDirectory() stays. */
|
|
const files = SEARCH.flatMap((s) => {
|
|
const p = join(ROOT, s);
|
|
return statSync(p).isDirectory()
|
|
? readdirSync(p, { recursive: true }).map((e) => join(p, e))
|
|
: [p];
|
|
}).filter((f) => EXT.test(f));
|
|
|
|
/* `@mirror name` opens, `@endmirror` closes. The tag may sit inside any comment syntax — we
|
|
* match the token only, and the closing line is not part of the body. */
|
|
const OPEN = /@mirror\s+([A-Za-z0-9_-]+\/[A-Za-z0-9_-]+)/;
|
|
const CLOSE = /@endmirror/;
|
|
|
|
const groups = new Map(); /* "group/role" -> [{file, line, body}] */
|
|
const errors = [];
|
|
|
|
for (const f of files) {
|
|
const lines = readFileSync(f, 'utf8').split('\n');
|
|
let open = null;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const m = lines[i].match(OPEN);
|
|
if (m) {
|
|
if (open) errors.push(`${rel(f)}:${i + 1}: @mirror ${m[1]} opened while ${open.name} is still open`);
|
|
open = { name: m[1], line: i + 1, body: [] };
|
|
continue;
|
|
}
|
|
if (CLOSE.test(lines[i])) {
|
|
if (!open) { errors.push(`${rel(f)}:${i + 1}: @endmirror with no open @mirror`); continue; }
|
|
if (!groups.has(open.name)) groups.set(open.name, []);
|
|
groups.get(open.name).push({ file: rel(f), line: open.line, body: normalise(open.body) });
|
|
open = null;
|
|
continue;
|
|
}
|
|
if (open) open.body.push(lines[i]);
|
|
}
|
|
if (open) errors.push(`${rel(f)}:${open.line}: @mirror ${open.name} is never closed (@endmirror)`);
|
|
}
|
|
|
|
function rel(f) { return relative(ROOT, f); }
|
|
|
|
/* Strip common leading indent and trailing whitespace: a block nested one level deeper in one
|
|
* file is not a divergence. */
|
|
function normalise(body) {
|
|
const kept = body.filter(l => l.trim() !== '');
|
|
if (!kept.length) return '';
|
|
const indent = Math.min(...kept.map(l => l.match(/^[ \t]*/)[0].length));
|
|
return kept.map(l => l.slice(indent).replace(/\s+$/, '')).join('\n');
|
|
}
|
|
|
|
/* whole-file pins (see SAME_FILE) */
|
|
for (const [a, b] of SAME_FILE) {
|
|
let ta, tb;
|
|
try { ta = readFileSync(join(ROOT, a)); } catch { errors.push(`@same-file: ${a} is missing`); continue; }
|
|
try { tb = readFileSync(join(ROOT, b)); } catch { errors.push(`@same-file: ${b} is missing`); continue; }
|
|
if (!ta.equals(tb))
|
|
errors.push(`@same-file: ${a} and ${b} have DRIFTED apart — they must be byte-identical ` +
|
|
`(${ta.length} vs ${tb.length} bytes).`);
|
|
else
|
|
console.log(` ok @same-file ${a} == ${b} (${ta.length} bytes)`);
|
|
}
|
|
|
|
let bad = 0;
|
|
const names = [...groups.keys()].sort();
|
|
for (const name of names) {
|
|
const copies = groups.get(name);
|
|
if (copies.length < 2) {
|
|
errors.push(`@mirror ${name}: only ONE copy (${copies[0].file}:${copies[0].line}). ` +
|
|
`A mirror of one enforces nothing — delete the tag, or restore the copy it was pinning.`);
|
|
bad++;
|
|
continue;
|
|
}
|
|
const first = copies[0];
|
|
const drifted = copies.filter(c => c.body !== first.body);
|
|
if (drifted.length) {
|
|
bad++;
|
|
errors.push(`@mirror ${name}: the copies have DRIFTED apart.`);
|
|
for (const c of copies)
|
|
errors.push(` ${c.file}:${c.line}${c.body === first.body ? '' : ' <-- differs'}`);
|
|
/* show the first differing line, so the failure is actionable */
|
|
const a = first.body.split('\n'), b = drifted[0].body.split('\n');
|
|
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
if (a[i] !== b[i]) {
|
|
errors.push(` first difference at body line ${i + 1}:`);
|
|
errors.push(` ${first.file}: ${a[i] ?? '(end of block)'}`);
|
|
errors.push(` ${drifted[0].file}: ${b[i] ?? '(end of block)'}`);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
console.log(` ok @mirror ${name.padEnd(22)} ${copies.length} copies, byte-identical ` +
|
|
copies.map(c => `${c.file}:${c.line}`).join(' '));
|
|
}
|
|
}
|
|
|
|
if (!names.length) console.log(' (no @mirror groups found)');
|
|
|
|
/* Since @mirror markers are comments, deleting every one of them (or a source tree that failed to
|
|
* be read) disarms this whole gate AND re-permits the duplicates css-dup.mjs forbids — the two are
|
|
* mutually load-bearing, and neither notices the other going quiet. "0 groups" used to read exactly
|
|
* like "every mirror still agrees": both print a clean summary line and exit 0. MEASURED is the
|
|
* current count (10); the floor sits under it so removing one mirror is never blocked here, but a
|
|
* SEARCH tree gutted to nothing is not the same event as a clean sweep. */
|
|
const MEASURED_GROUPS = 10;
|
|
const GROUPS_FLOOR = 5;
|
|
if (names.length < GROUPS_FLOOR)
|
|
errors.push(`only ${names.length} @mirror group(s) found across SEARCH (last real run: `
|
|
+ `${MEASURED_GROUPS}) — either most of the forced-duplication tags were deleted, or SEARCH `
|
|
+ `read far fewer files than it should have. Either way this is not a clean sweep.`);
|
|
|
|
if (errors.length) {
|
|
console.error('\nFAIL: @mirror');
|
|
for (const e of errors) console.error(' ' + e);
|
|
console.error('\nA @mirror block is duplication the language forces on you. The tag is the promise');
|
|
console.error('that the copies stay identical; this is what keeps that promise. Fix the copies —');
|
|
console.error('or, if they are genuinely meant to differ now, they were never a mirror: untag them');
|
|
console.error('and write down why they diverged.');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\n@mirror: ${names.length} group(s), every copy byte-identical.`);
|