ci / test (pull_request) Successful in 2m48s
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.
183 lines
6.1 KiB
JavaScript
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();
|