// Generates src/worker/lib/main/handlers.js from the desktop main/index.js: the statements inside // registerIpc() are copied as source text (no logic changes), except // - handle()/task() registrations of channels the web version doesn't have (dropped prefixes), // - every require("x") becomes an identifier imported once at the top of the module. // usage: node tools/gen-handlers.mjs import fs from "node:fs"; import path from "node:path"; import * as acorn from "acorn"; const SRC = path.resolve("../reference/lstools-desktop/src/main/index.js"); const OUT = path.resolve("src/worker/lib/main/handlers.js"); // Answered by the browser (bridge.js) or not part of the web version. const DROP = ["dialog:", "shell:", "store:", "ws:", "app:", "pm:", "cz:", "lr:", "mon:", "iop:", "snd:"]; // Handlers that need a module the web version does not ship are left out too. const MAIN_DIR = path.dirname(OUT); // Kept channels whose body opens an Electron dialog: answered in the browser (bridge.js) instead. const DIALOG_CHANNELS = ["fxlab:openJson", "fxlab:saveJson", "fx:openFile", "dat:pickFile"]; // Non-registration statements that wire up removed parts (IOP Manager notifier, Monitor, Patch Manager). const DROP_STATEMENT = /\b(iop\.setNotifier|monitor\.|pm\.|patchman|launcherpub|clientzip)\b/; const text = fs.readFileSync(SRC, "utf8"); const ast = acorn.parse(text, { ecmaVersion: "latest", sourceType: "script", locations: true }); const reg = ast.body.find((n) => n.type === "FunctionDeclaration" && n.id.name === "registerIpc"); if (!reg) throw new Error("registerIpc not found"); const kept = []; const dropped = []; for (const st of reg.body.body) { const call = st.type === "ExpressionStatement" && st.expression.type === "CallExpression" ? st.expression : null; const isReg = call && call.callee.type === "Identifier" && ["handle", "task"].includes(call.callee.name) && call.arguments[0]?.type === "Literal"; if (isReg) { const ch = call.arguments[0].value; if (DROP.some((p) => ch.startsWith(p)) || DIALOG_CHANNELS.includes(ch)) { dropped.push(ch); continue; } } else if (DROP_STATEMENT.test(text.slice(st.start, st.end))) { dropped.push(`(statement line ${st.loc.start.line})`); continue; } const needs = [...text.slice(st.start, st.end).matchAll(/require\("(\.\/[^"]+)"\)/g)].map((m) => (m[1].endsWith(".js") || m[1].endsWith(".json") ? m[1] : m[1] + ".js")); if (needs.some((s) => !fs.existsSync(path.join(MAIN_DIR, s)))) { dropped.push("(unshipped)"); continue; } // keep comments right above the statement kept.push(text.slice(st.start, st.end)); } // requires -> hoisted imports const imports = new Map(); // spec -> identifier const idFor = (spec) => { if (!imports.has(spec)) imports.set(spec, `__req_${spec.replace(/^node:/, "node_").replace(/[^\w]/g, "_")}`); return imports.get(spec); }; let body = kept.join("\n\n").replace(/require\("([^"]+)"\)/g, (_, spec) => idFor(spec)); // names index.js defines outside registerIpc that the handlers use const fixSpec = (s) => (s.startsWith(".") && !s.endsWith(".js") && !s.endsWith(".json") ? s + ".js" : s); const head = [...imports].map(([spec, id]) => `import ${id} from "${fixSpec(spec)}";`); head.push(`import path from "node:path";`, `import { app } from "electron";`, `import store from "./store.js";`, `import iop from "./iopman.js";`); const out = `// Generated by tools/gen-handlers.mjs from the desktop main/index.js registerIpc(). Do not edit. // Dropped channels: ${dropped.length} ${head.join("\n")} const tasks = new Map(); const win = null; export function register(handle, task) { ${body.replace(/^/gm, "\t")} } `; fs.writeFileSync(OUT, out); const left = [...out.matchAll(/\brequire\(/g)].length; const regs = [...out.matchAll(/\b(handle|task)\("([\w:.-]+)"/g)].map((m) => m[2]); console.log(`kept ${regs.length} channels, dropped ${dropped.length}, imports ${imports.size}, require() left ${left}`); console.log("imports: " + [...imports.keys()].join(", "));