Files
small-packages/luci-theme-footstrap/tools/jsmin-verify.mjs
T
2026-09-20 01:22:33 +08:00

116 lines
4.7 KiB
JavaScript

/* Gate: prove jsmin's output is the SAME PROGRAM as the source.
*
* `luci.mk` minifies every LuCI package's JS with jsmin, whose hand-written scanner decides REGEX
* vs DIVISION from exactly one preceding character against a fixed allow-list — `n`, the last
* letter of `return`, is not on it, nor is `>` from `=>`. So `return /re/` makes jsmin read the
* regex's `//` as a line comment and swallow the rest of the file, exiting 0 while doing it
* (openwrt/luci#8299, #8020, #8021, #8256).
*
* A zero exit code therefore proves nothing. The only proof jsmin did not eat our code is
* comparing the TOKEN STREAM of source and output — same tokens, same order, or the build is
* wrong. acorn is already an eslint transitive dependency.
*
* `allowReturnOutsideFunction`: a LuCI resource file is evaluated inside a function wrapper and
* legitimately ends in a top-level `return baseclass.extend()`. */
import { readFileSync, existsSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { join } from 'node:path';
import * as acorn from 'acorn';
const JSMIN = process.env.JSMIN || '/tmp/jsmin';
const OPTS = { ecmaVersion: 2022, allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true };
if (!existsSync(JSMIN)) {
console.error(`jsmin not found at ${JSMIN}. Build it from luci-base/src/jsmin.c and set $JSMIN.`);
process.exit(2);
}
const tokens = (src) => [...acorn.tokenizer(src, OPTS)].map(t => `${t.type.label}${t.value ?? ''}`);
/* Which JS ships, asked of the filesystem here, once: luci.mk copies htdocs/ wholesale, so every
* .js under it IS the shipped set. Spelling that glob out in a CI step and handing the result over
* in an environment variable puts the answer in two places, and a file left out of one of them
* silently drops this check on its JS. */
const explicit = process.argv.length > 2;
const files = explicit
? process.argv.slice(2)
: execFileSync('find', ['luci-theme-footstrap/htdocs', '-name', '*.js'], { encoding: 'utf8' })
.split('\n').filter(Boolean).sort();
if (!files.length) {
console.error('found no shipped JS — the glue is broken, not the theme');
process.exit(2);
}
/* The floor below applies only to the AUTO-DISCOVERED set (CI's own invocation, per build.yml):
* an explicit file list on the command line is a deliberate one-file check during development and
* is allowed to be short. `find` returning a short list is a different event — the glob broke, or
* `luci-theme-footstrap/htdocs` resolved to the wrong directory — and it used to read exactly like
* "every shipped file passed": a payload that shrank from 17 files to 1 would still print "jsmin
* output is token-identical to the source for every shipped file". MEASURED is the current count
* (17); the floor sits under it so a normal file addition/removal never trips it. */
const MEASURED_FILES = 17;
const FILES_FLOOR = 8;
if (!explicit && files.length < FILES_FLOOR) {
console.error(`found only ${files.length} shipped JS file(s) under luci-theme-footstrap/htdocs `
+ `(last real run: ${MEASURED_FILES}) — the glob is broken, or the directory is wrong. This `
+ `is not "every file verified"; it is almost no file checked.`);
process.exit(2);
}
let failed = 0, before = 0, after = 0;
for (const f of files) {
const src = readFileSync(f, 'utf8');
const name = f.split('/').pop();
let min;
try {
min = execFileSync(JSMIN, { input: src, encoding: 'utf8' });
} catch (e) {
console.log(` FAIL ${name}: jsmin exited non-zero — ${String(e.stderr || e.message).trim()}`);
failed++;
continue;
}
let a, b;
try {
a = tokens(src);
b = tokens(min);
} catch (e) {
console.log(` FAIL ${name}: output does not parse — ${e.message}`);
failed++;
continue;
}
const n = Math.max(a.length, b.length);
let diff = -1;
for (let i = 0; i < n; i++) {
if (a[i] !== b[i]) { diff = i; break; }
}
if (diff !== -1) {
const show = (t) => (t === undefined ? '<END OF FILE>' : t.replace('', ' '));
console.log(` FAIL ${name}: token ${diff} differs — source has [${show(a[diff])}], jsmin produced [${show(b[diff])}]`);
console.log(' jsmin ate part of this file. Look for `return /regex/` or `=> /regex/`:');
console.log(' its regex lookback allow-list has no `n` (return) and no `>` (arrow).');
failed++;
continue;
}
before += src.length;
after += min.length;
console.log(` ok ${name.padEnd(34)} ${String(a.length).padStart(5)} tokens identical ${src.length} -> ${min.length} B`);
}
if (before) {
const pct = Math.round(100 - (100 * after) / before);
console.log(`\n total ${before} -> ${after} B (-${pct}%)`);
}
if (failed) {
console.error(`\n${failed} file(s) failed the jsmin equivalence check`);
process.exit(1);
}
console.log('\njsmin output is token-identical to the source for every shipped file');