Files
ismis-helper/scripts/build.js
T
adrianbonpin bd6c05d1b4
ci / test (pull_request) Successful in 2m48s
fix(build): package manifest.json and drop dev files from zips
collectFiles() skipped manifest.json unconditionally, and the final zip is
produced by calling collectFiles() on the built target dir — so both
dist/*.zip shipped with no manifest.json at all and could not be installed.
The per-target manifest was written into the dir correctly, then filtered
back out on the way into the archive.

Split the exclusion list: EXCLUDED_DIRS plus DEV_ONLY_FILES (manifest.json,
AGENTS.md, README.md, package.json, package-lock.json), and only apply the
latter when reading the repo root. Reading a built target keeps its
generated manifest.

Firefox zip: 26 files/1.84 MB (no manifest) -> 23 files/1.68 MB.
Chrome zip gains its manifest.json and background.chrome.js as before.
2026-09-28 02:23:12 +08:00

183 lines
6.1 KiB
JavaScript

'use strict';
// Zero-dependency build script: packages the extension for Firefox and Chrome.
// Usage: node scripts/build.js [firefox|chrome|all] [--version X.Y.Z]
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const DIST = path.join(ROOT, 'dist');
const FF_GECKO_ID = process.env.FF_GECKO_ID || 'ismis-helper@adrianbonpin';
// Order matters for the single-file Chrome service worker:
// fontkit -> pdf-lib -> pdf.js -> background.js
const CHROME_BG_PARTS = [
'libs/fontkit.min.js',
'libs/pdf-lib.min.js',
'pdf.js',
'export-csv.js',
'export-json.js',
'export-xlsx.js',
'background.js',
];
// Directories never copied into a build target.
const EXCLUDED_DIRS = new Set([
'.git', 'node_modules', 'dist', 'docs', 'scripts', 'test', 'web-ext-artifacts',
]);
// Repo-root files that are for contributors, not for the shipped extension.
// manifest.json is here because it is generated per target, not copied.
const DEV_ONLY_FILES = new Set([
'manifest.json', 'AGENTS.md', 'README.md', 'package.json', 'package-lock.json',
]);
// Everything to copy from ROOT into a target, relative paths.
// `excludeDev` is true when reading ROOT (skip contributor files) and false when
// reading a built target (its generated manifest.json MUST be packaged).
function collectFiles(srcDir, { dir = '', excludeDev = false, out = [] } = {}) {
for (const entry of fs.readdirSync(srcDir)) {
const abs = path.join(srcDir, entry);
const rel = dir ? dir + '/' + entry : entry;
if (entry.startsWith('.') || EXCLUDED_DIRS.has(entry)) continue;
if (excludeDev && DEV_ONLY_FILES.has(entry)) continue;
if (fs.statSync(abs).isDirectory()) {
collectFiles(abs, { dir: rel, excludeDev, out });
} else {
out.push(rel);
}
}
return out;
}
function copyDir(relList, destDir) {
for (const rel of relList) {
const src = path.join(ROOT, rel);
const dst = path.join(destDir, rel);
fs.mkdirSync(path.dirname(dst), { recursive: true });
fs.copyFileSync(src, dst);
}
}
function readManifest() {
return JSON.parse(fs.readFileSync(path.join(ROOT, 'manifest.json'), 'utf8'));
}
// ---- STORED zip writer (no compression, valid for both browsers) ----
function crc32(buf) {
let table = [];
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
table[n] = c >>> 0;
}
let crc = 0xffffffff;
for (let i = 0; i < buf.length; i++) crc = table[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
return (crc ^ 0xffffffff) >>> 0;
}
function writeStoredZip(files, baseDir, zipPath) {
const chunks = [];
const central = [];
let offset = 0;
for (const rel of files) {
const data = fs.readFileSync(path.join(baseDir, rel));
const name = Buffer.from(rel, 'utf8');
const crc = crc32(data);
const lh = Buffer.alloc(30);
lh.writeUInt32LE(0x04034b50, 0);
lh.writeUInt16LE(20, 4);
lh.writeUInt16LE(0, 6);
lh.writeUInt16LE(0, 8); // method: stored
lh.writeUInt16LE(0, 10);
lh.writeUInt16LE(0, 12);
lh.writeUInt32LE(crc, 14);
lh.writeUInt32LE(data.length, 18);
lh.writeUInt32LE(data.length, 22);
lh.writeUInt16LE(name.length, 26);
lh.writeUInt16LE(0, 28);
chunks.push(lh, name, data);
const ch = Buffer.alloc(46);
ch.writeUInt32LE(0x02014b50, 0);
ch.writeUInt16LE(20, 4);
ch.writeUInt16LE(20, 6);
ch.writeUInt16LE(0, 8);
ch.writeUInt16LE(0, 10);
ch.writeUInt16LE(0, 12);
ch.writeUInt32LE(crc, 16);
ch.writeUInt32LE(data.length, 20);
ch.writeUInt32LE(data.length, 24);
ch.writeUInt16LE(name.length, 28);
ch.writeUInt16LE(0, 30);
ch.writeUInt16LE(0, 32);
ch.writeUInt16LE(0, 34);
ch.writeUInt16LE(0, 36);
ch.writeUInt32LE(0, 38);
ch.writeUInt32LE(offset, 42);
central.push(ch, name);
offset += lh.length + name.length + data.length;
}
const cdStart = offset;
let cdSize = 0;
central.forEach((b) => { cdSize += b.length; });
const eocd = Buffer.alloc(22);
eocd.writeUInt32LE(0x06054b50, 0);
eocd.writeUInt16LE(files.length, 8);
eocd.writeUInt16LE(files.length, 10);
eocd.writeUInt32LE(cdSize, 12);
eocd.writeUInt32LE(cdStart, 16);
eocd.writeUInt16LE(0, 20);
const all = Buffer.concat([...chunks, ...central, eocd]);
fs.writeFileSync(zipPath, all);
}
function buildTarget(target, version) {
const dir = path.join(DIST, target);
fs.rmSync(dir, { recursive: true, force: true });
fs.mkdirSync(dir, { recursive: true });
const relList = collectFiles(ROOT, { excludeDev: true });
copyDir(relList, dir);
if (target === 'firefox') {
const m = readManifest();
if (version) m.version = version;
m.browser_specific_settings = { gecko: { id: FF_GECKO_ID } };
fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(m, null, 2) + '\n');
} else { // chrome
const m = readManifest();
if (version) m.version = version;
delete m.background;
m.background = { service_worker: 'background.chrome.js' };
delete m.browser_specific_settings;
fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(m, null, 2) + '\n');
const worker = CHROME_BG_PARTS
.map((p) => fs.readFileSync(path.join(ROOT, p), 'utf8'))
.join('\n;\n');
fs.writeFileSync(path.join(dir, 'background.chrome.js'), worker);
}
const suffix = version ? '-' + version : '';
const zipName = path.join(DIST, `ismis-helper-${target}${suffix}.zip`);
writeStoredZip(collectFiles(dir), dir, zipName);
console.log(`built ${target}: ${zipName}`);
}
function main() {
const args = process.argv.slice(2);
const rawTargets = args.filter((a) => !a.startsWith('--'));
const vi = args.indexOf('--version');
let version = null;
if (vi !== -1 && args[vi + 1]) version = String(args[vi + 1]).replace(/^v/, '');
const requested = rawTargets.length ? rawTargets : ['all'];
const targets = requested.includes('all')
? ['firefox', 'chrome']
: requested.filter((t) => t === 'firefox' || t === 'chrome');
if (!targets.length) {
console.error('Usage: node scripts/build.js [firefox|chrome|all] [--version X.Y.Z]');
process.exit(1);
}
for (const t of targets) buildTarget(t, version);
}
main();