#!/usr/bin/env node // Build a signed .zxp for the Dragonflight Premiere panel. // // - Reads version from ../CSXS/manifest.xml () // - Generates a self-signed cert on first run (cert/dragonflight-selfsigned.p12) // - Stages the panel bundle into stage/ (excludes build/ + dev cruft) // - Calls zxp-sign-cmd to sign + package into dist/ // // Usage: node build-zxp.mjs // Output: dist/dragonflight-premiere-panel-.zxp import { mkdirSync, existsSync, readFileSync, writeFileSync, rmSync, cpSync, readdirSync, statSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; import { randomBytes } from 'node:crypto'; import zxp from 'zxp-sign-cmd'; const HERE = dirname(fileURLToPath(import.meta.url)); const PANEL_DIR = resolve(HERE, '..'); const MANIFEST = join(PANEL_DIR, 'CSXS', 'manifest.xml'); const CERT_DIR = join(HERE, 'cert'); const CERT_FILE = join(CERT_DIR, 'dragonflight-selfsigned.p12'); const PASS_FILE = join(CERT_DIR, 'cert-passphrase.txt'); const STAGE_DIR = join(HERE, 'stage'); const DIST_DIR = join(HERE, 'dist'); // Files/dirs to exclude from the staged bundle. const EXCLUDE = new Set(['build', 'install-windows.ps1', '.git', '.gitignore', 'node_modules']); function readVersion() { const xml = readFileSync(MANIFEST, 'utf8'); const m = xml.match(/([^<]+)<\/ExtensionBundleVersion>/); if (!m) throw new Error(`Could not find in ${MANIFEST}`); return m[1].trim(); } function ensureCert() { mkdirSync(CERT_DIR, { recursive: true }); if (existsSync(CERT_FILE) && existsSync(PASS_FILE)) { return readFileSync(PASS_FILE, 'utf8').trim(); } console.log('No signing cert found — generating self-signed cert (one-time)…'); const passphrase = randomBytes(24).toString('base64url'); writeFileSync(PASS_FILE, passphrase + '\n', { mode: 0o600 }); return new Promise((res, rej) => { zxp.selfSignedCert({ country: 'US', province: 'WA', org: 'Wild Dragon LLC', name: 'Wild Dragon LLC', password: passphrase, output: CERT_FILE, validityDays: 365 * 25, }, (err) => { if (err) return rej(err); console.log(` wrote ${CERT_FILE}`); console.log(` wrote ${PASS_FILE}`); console.log(' >> COMMIT both files so future builds reuse them. <<'); res(passphrase); }); }); } function stageBundle() { if (existsSync(STAGE_DIR)) rmSync(STAGE_DIR, { recursive: true, force: true }); mkdirSync(STAGE_DIR, { recursive: true }); for (const entry of readdirSync(PANEL_DIR)) { if (EXCLUDE.has(entry)) continue; const src = join(PANEL_DIR, entry); const dst = join(STAGE_DIR, entry); cpSync(src, dst, { recursive: true }); } } function signZxp(version, passphrase) { mkdirSync(DIST_DIR, { recursive: true }); const output = join(DIST_DIR, `dragonflight-premiere-panel-${version}.zxp`); if (existsSync(output)) rmSync(output); return new Promise((res, rej) => { zxp.sign({ input: STAGE_DIR, output, cert: CERT_FILE, password: passphrase, }, (err) => { if (err) return rej(err); const bytes = statSync(output).size; console.log(`Built ${output} (${(bytes / 1024).toFixed(1)} KB)`); res(output); }); }); } async function main() { const version = readVersion(); console.log(`Dragonflight Premiere panel — ZXP build v${version}`); const passphrase = await ensureCert(); stageBundle(); await signZxp(version, passphrase); rmSync(STAGE_DIR, { recursive: true, force: true }); } main().catch((err) => { console.error('ZXP build failed:', err); process.exit(1); });