'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();