theseus/addon-build/docx-editor/make-icons.mjs
Local Dev 56b3f2f206 feat(docx-editor): ship it in the build, updated over the first-party channel
Moving it out of the build left it with no way in. Settings can only install
from the community catalogue, so a first-party extension that isn't bundled
has a working update channel and no first copy for anyone to update — the
mechanism was all there and the front door was missing.

So it goes back beside screenshot, aegis and pdf-editor: seeded into every
profile by the build, listed under "Built into Theseus", and kept current
between releases by the operator-signed channel at
theseus.x/extensions/docx-editor/. That is the arrangement docs/ADDON-UPDATES.md
describes, and the one the signing script was written for.

About 400 KB compressed in the installer, most of it the vendored editor
libraries — next to the ~4 MB of pdf.js that pdf-editor already ships, the
weight argument for keeping it out didn't survive contact with the numbers.

The end-to-end driver goes back to checking that a fresh profile seeds it,
which is the property that actually matters now.
2026-09-21 22:47:13 +02:00

101 lines
4.3 KiB
JavaScript

// Rasterise extensions/docx-editor/icon.svg to PNGs.
//
// npm run icons (from addon-build/docx-editor/)
//
// SVG is what the dock and the catalogue use; the PNGs are for everywhere
// that can't take one — a favicon fallback, a listing thumbnail, an OS file
// association later on. There is no sharp/resvg on this machine, so the
// rasteriser is Electron itself: a hidden window, the SVG scaled to fill it,
// and capturePage(). Re-run it whenever icon.svg changes.
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
const here = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(here, "../..");
const EXT = path.resolve(ROOT, "bundled-addons/docx-editor");
const SIZES = [16, 32, 48, 128, 256];
const electron = path.join(ROOT, "node_modules", "electron", "dist",
process.platform === "win32" ? "electron.exe" : "electron");
if (!fs.existsSync(electron)) {
console.error(`electron not found at ${electron} — run npm install in TheseusNavigator first`);
process.exit(2);
}
// The main script runs inside Electron; it is written to a temp dir rather
// than committed, because it is scaffolding for this script and nothing else.
const work = fs.mkdtempSync(path.join(os.tmpdir(), "docx-icons-"));
const svg = fs.readFileSync(path.join(EXT, "icon.svg"), "utf8");
for (const size of SIZES) {
fs.writeFileSync(path.join(work, `page-${size}.html`), `<!doctype html><meta charset="utf-8">
<style>html,body{margin:0;padding:0;background:transparent}
svg{width:${size}px;height:${size}px;display:block}</style>${svg}`);
}
fs.writeFileSync(path.join(work, "main.js"), `
const { app, BrowserWindow } = require("electron");
const fs = require("fs"), path = require("path");
const work = ${JSON.stringify(work)};
const out = ${JSON.stringify(EXT)};
const sizes = ${JSON.stringify(SIZES)};
app.disableHardwareAcceleration();
// One window, resized per icon. Creating and destroying an offscreen window
// per size raced with itself: the next loadFile came back ERR_FAILED while
// the previous one was still tearing down its compositor.
app.whenReady().then(async () => {
const win = new BrowserWindow({
width: 256, height: 256, show: false, frame: false, transparent: true,
useContentSize: true, webPreferences: { offscreen: true },
});
for (const size of sizes) {
win.setContentSize(size, size);
await win.loadFile(path.join(work, "page-" + size + ".html"));
// Offscreen windows paint asynchronously and the first frame is often
// the blank one, so wait for a couple before capturing.
await new Promise((r) => {
let n = 0;
const onPaint = () => { if (++n >= 2) { win.webContents.off("paint", onPaint); r(); } };
win.webContents.on("paint", onPaint);
setTimeout(r, 1500);
});
const img = await win.webContents.capturePage();
fs.writeFileSync(path.join(out, "icon-" + size + ".png"), img.toPNG());
}
win.destroy();
app.quit();
}).catch((e) => { console.error("icon render failed:", e); app.exit(1); });
`);
fs.writeFileSync(path.join(work, "package.json"), JSON.stringify({ name: "docx-icons", main: "main.js" }));
execFileSync(electron, [work], { stdio: "inherit" });
for (const size of SIZES) {
const f = path.join(EXT, `icon-${size}.png`);
console.log(`${path.relative(ROOT, f)} ${fs.existsSync(f) ? (fs.statSync(f).size / 1024).toFixed(1) + " KB" : "MISSING"}`);
}
fs.rmSync(work, { recursive: true, force: true });
// The dock and the catalogue card read `icon` out of addon.json, and both
// take a data URL (this is how Aegis ships its mark). Deriving it from
// icon.svg here means there is one drawing, not two that drift apart.
const minified = svg
.replace(/<!--[\s\S]*?-->/g, "")
.replace(/<title>[\s\S]*?<\/title>/g, "")
.replace(/\s+/g, " ")
.replace(/>\s+</g, "><")
.trim();
const dataUrl = "data:image/svg+xml;utf8," + encodeURIComponent(minified);
const manifestPath = path.join(EXT, "addon.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
if (manifest.icon !== dataUrl) {
manifest.icon = dataUrl;
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
console.log(`addon.json icon updated (${dataUrl.length} chars)`);
} else {
console.log("addon.json icon already current");
}