diff --git a/package.json b/package.json index 82d9311..99060db 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "private": true, "description": "Firefox extension for the USC ISMIS portal", "scripts": { - "test": "node --test" + "test": "node --test", + "build": "node scripts/build.js all" }, "devDependencies": { "pdf-lib": "^1.17.1" diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..4adce7e --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,166 @@ +'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', + 'background.js', +]; + +// Everything to copy from ROOT into a target, relative paths. +function collectFiles(srcDir, out = [], prefix = '') { + for (const entry of fs.readdirSync(srcDir)) { + const abs = path.join(srcDir, entry); + const rel = prefix ? prefix + '/' + entry : entry; + if (entry === '.git' || entry === 'node_modules' || entry === 'dist' || + entry === 'docs' || entry === 'scripts' || entry === 'manifest.json' || + entry.startsWith('.')) { + continue; + } + if (fs.statSync(abs).isDirectory()) collectFiles(abs, out, rel); + 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); + 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();