const fs = require('fs'); const path = require('path'); const archiver = require('archiver'); const rootDir = path.join(__dirname, '..'); const extDir = path.join(rootDir, 'extension'); const distDir = path.join(rootDir, 'dist'); const baseManifestPath = path.join(extDir, 'manifest.base.json'); // Ensure dist directory exists if (fs.existsSync(distDir)) { fs.rmSync(distDir, { recursive: true, force: true }); } fs.mkdirSync(distDir, { recursive: true }); // Sync shared constants from root /shared to /extension/shared console.log('Syncing protocol constants...'); const masterSharedDir = path.join(rootDir, 'shared'); const extSharedDir = path.join(extDir, 'shared'); if (!fs.existsSync(extSharedDir)) { fs.mkdirSync(extSharedDir, { recursive: true }); } const sharedFiles = ['constants.js', 'blacklist.js', 'README.md']; for (const file of sharedFiles) { const src = path.join(masterSharedDir, file); const dest = path.join(extSharedDir, file); if (!fs.existsSync(src)) { throw new Error(`CRITICAL: Source shared file missing: ${src}. Aborting build to prevent broken artifacts.`); } fs.copyFileSync(src, dest); } console.log('✓ constants.js, blacklist.js, and README.md synced to extension/shared/'); // Read the base manifest const baseManifest = JSON.parse(fs.readFileSync(baseManifestPath, 'utf8')); // Helper to copy files, ignoring manifest.json and manifest.base.json // Also injects shared constants into content.js function copyExtensionFiles(targetDir) { fs.mkdirSync(targetDir, { recursive: true }); // Read master constants for injection const masterConstantsPath = path.join(rootDir, 'shared', 'constants.js'); const constantsContent = fs.readFileSync(masterConstantsPath, 'utf8'); // Robust Extraction using flexible regex const eventsMatch = constantsContent.match(/export const EVENTS\s*=\s*({[\s\S]+?});/); const heartbeatMatch = constantsContent.match(/export const HEARTBEAT_INTERVAL\s*=\s*(\d+);/); if (!eventsMatch) { throw new Error('CRITICAL: Could not find EVENTS object in shared/constants.js'); } if (!heartbeatMatch) { throw new Error('CRITICAL: Could not find HEARTBEAT_INTERVAL in shared/constants.js'); } const eventsObject = eventsMatch[1]; const heartbeatVal = heartbeatMatch[1]; const items = fs.readdirSync(extDir); for (const item of items) { if (item === 'manifest.json' || item === 'manifest.base.json') continue; const srcPath = path.join(extDir, item); const destPath = path.join(targetDir, item); if (fs.lstatSync(srcPath).isDirectory()) { fs.cpSync(srcPath, destPath, { recursive: true }); } else { if (item === 'content.js') { // Perform injection let content = fs.readFileSync(srcPath, 'utf8'); // 1. Inject Events const eStart = '// --- SHARED_EVENTS_INJECT_START ---'; const eEnd = '// --- SHARED_EVENTS_INJECT_END ---'; const ePattern = new RegExp(`${eStart}[\\s\\S]+?${eEnd}`); const eRep = `${eStart}\n // This block is automatically updated by /scripts/build-extension.js\n const EVENTS = ${eventsObject};\n ${eEnd}`; if (ePattern.test(content)) { content = content.replace(ePattern, eRep); } else { console.warn('⚠️ WARNING: Event markers not found in content.js'); } // 2. Inject Heartbeat const hStart = '// --- SHARED_HEARTBEAT_INJECT_START ---'; const hEnd = '// --- SHARED_HEARTBEAT_INJECT_END ---'; const hPattern = new RegExp(`${hStart}[\\s\\S]+?${hEnd}`); const hRep = `${hStart}\n const HEARTBEAT_INTERVAL_VAL = ${heartbeatVal};\n ${hEnd}`; if (hPattern.test(content)) { content = content.replace(hPattern, hRep); } else { console.warn('⚠️ WARNING: Heartbeat markers not found in content.js'); } fs.writeFileSync(destPath, content); console.log('✓ Injected shared constants into content.js'); } else { fs.copyFileSync(srcPath, destPath); } } } } // Helper to zip a directory function zipDirectory(sourceDir, outPath) { return new Promise((resolve, reject) => { const archive = archiver('zip', { zlib: { level: 9 } }); const stream = fs.createWriteStream(outPath); archive .directory(sourceDir, false) .on('error', err => reject(err)) .pipe(stream); stream.on('close', () => resolve()); archive.finalize(); }); } async function buildBrowser(browserName, manifestModifier) { console.log(`Building for ${browserName}...`); const browserDistDir = path.join(distDir, browserName); // 1. Copy files copyExtensionFiles(browserDistDir); // 2. Modify and write manifest const browserManifest = manifestModifier(JSON.parse(JSON.stringify(baseManifest))); fs.writeFileSync( path.join(browserDistDir, 'manifest.json'), JSON.stringify(browserManifest, null, 2) ); // 3. Zip it const zipPath = path.join(distDir, `koalasync-${browserName}.zip`); await zipDirectory(browserDistDir, zipPath); console.log(`Successfully built and zipped ${browserName} -> ${zipPath}`); } async function run() { try { // Build Chrome await buildBrowser('chrome', (manifest) => { manifest.background = { service_worker: "background.js", type: "module" }; return manifest; }); // Build Firefox await buildBrowser('firefox', (manifest) => { manifest.background = { scripts: ["background.js"], type: "module" }; manifest.browser_specific_settings = { gecko: { id: "koalasync@shik3i.net" } }; return manifest; }); console.log('Build complete!'); } catch (error) { console.error('Build failed:', error); process.exit(1); } } run();