Files

79 lines
3.9 KiB
JavaScript

// 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(", "));