Files

99 lines
4.9 KiB
JavaScript

// Copies the desktop renderer into public/legacy/ for the web host, without editing the scripts:
// js/<file>.js renderer scripts, byte for byte except the fixes in tools/legacy-patches.mjs
// (app.js and monitor.js are left out)
// views/<view>.html each <section id="view-<view>" class="view"> of index.html
// chrome.html shared overlays: #menu, #modal, #toast, #task
// legacy.css app.css scoped under .lsx (CSS nesting), @keyframes hoisted out,
// :root tokens moved onto .lsx so the web theme can override them
// usage: node tools/extract-legacy.mjs
import fs from "node:fs";
import path from "node:path";
import { applyPatches, PATCHES } from "./legacy-patches.mjs";
const REF = path.resolve("../reference/lstools-desktop/src/renderer");
const OUT = path.resolve("public/legacy");
const SKIP = new Set(["app.js", "monitor.js"]);
const DROP_VIEWS = new Set(["iop", "pm", "mon", "pw"]); // IOP Manager UI comes back later as its own page
fs.rmSync(OUT, { recursive: true, force: true });
fs.mkdirSync(path.join(OUT, "js"), { recursive: true });
fs.mkdirSync(path.join(OUT, "views"), { recursive: true });
// ---------- scripts ----------
const scripts = fs.readdirSync(REF).filter((f) => f.endsWith(".js") && !SKIP.has(f));
for (const f of scripts) {
if (!PATCHES.some((p) => p.file === f)) {
fs.copyFileSync(path.join(REF, f), path.join(OUT, "js", f));
continue;
}
fs.writeFileSync(path.join(OUT, "js", f), applyPatches(f, fs.readFileSync(path.join(REF, f), "utf8")));
console.log(`patched ${f}: ${PATCHES.filter((p) => p.file === f).length}`);
}
// ---------- markup ----------
const html = fs.readFileSync(path.join(REF, "index.html"), "utf8");
/** The element starting at `start` (an opening tag) up to its matching close tag. */
function element(src, start) {
const tag = src.slice(start + 1).match(/^[\w-]+/)[0];
const re = new RegExp(`<(/?)${tag}\\b[^>]*>`, "g");
re.lastIndex = start;
let depth = 0;
for (let m; (m = re.exec(src)); ) {
if (m[0].endsWith("/>")) continue;
depth += m[1] ? -1 : 1;
if (depth === 0) return src.slice(start, re.lastIndex);
}
throw new Error(`unclosed <${tag}> at ${start}`);
}
const views = [];
for (const m of html.matchAll(/<section id="view-([\w-]+)" class="view[^"]*">/g)) {
const name = m[1];
if (DROP_VIEWS.has(name)) continue;
fs.writeFileSync(path.join(OUT, "views", `${name}.html`), element(html, m.index));
views.push(name);
}
const chrome = [];
for (const id of ["menu", "modal", "toast", "task"]) {
const m = html.match(new RegExp(`<[\\w-]+[^>]*\\bid="${id}"[^>]*>`));
if (m) chrome.push(element(html, m.index));
else console.warn(`no #${id} in index.html`);
}
fs.writeFileSync(path.join(OUT, "chrome.html"), chrome.join("\n"));
// ---------- css ----------
let css = fs.readFileSync(path.join(REF, "app.css"), "utf8");
const keyframes = [];
css = css.replace(/@keyframes\s+[\w-]+\s*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/g, (k) => (keyframes.push(k), ""));
css = css.replace(/:root\s*\{/, "& {");
fs.writeFileSync(path.join(OUT, "legacy.css"), `/* Generated by tools/extract-legacy.mjs from the desktop app.css. Do not edit. */\n${keyframes.join("\n")}\n.lsx {\n${css}\n}\n`);
// ---------- host.js: the shared helpers of app.js, copied line for line ----------
// Line ranges of the reference app.js (1-based, inclusive). Only helpers the tool scripts use;
// IOP Manager / Patch Manager / Monitor code stays out.
const APP_RANGES = [
[6, 103], // $, bridge, call, el, size, hex, fmtDos, MAX_ENTRIES, toast, modal, confirmBox, prompt, runTask
[162, 162], // baseName
[494, 558], // ICONS, icon()
[564, 593], // showMenu, hideMenu, cmd
[818, 823], // replay
[1051, 1063], // unsavedWork (profileSource / profileSourceOption: web versions in host-web-after.js)
];
const appLines = fs.readFileSync(path.join(REF, "app.js"), "utf8").split(/\r?\n/);
const copied = APP_RANGES.map(([a, b]) => `// ---- app.js ${a}-${b} ----\n` + appLines.slice(a - 1, b).join("\n")).join("\n\n");
const webPart = fs.readFileSync(path.resolve("src/legacy/host-web.js"), "utf8");
const webAfter = fs.readFileSync(path.resolve("src/legacy/host-web-after.js"), "utf8");
fs.writeFileSync(
path.join(OUT, "host.js"),
`"use strict";\n// Generated by tools/extract-legacy.mjs: helpers copied from the desktop app.js + web additions.\n\n${webPart}\n\n${copied}\n\n${webAfter}\n`,
);
// Load order = the desktop index.html order (scripts depend on earlier ones' globals)
const order = [...html.matchAll(/<script src="([\w.-]+\.js)"><\/script>/g)].map((m) => m[1]).filter((f) => !SKIP.has(f));
const missing = scripts.filter((s) => !order.includes(s));
if (missing.length) console.warn("scripts not in index.html (not loaded):", missing.join(", "));
fs.writeFileSync(path.join(OUT, "manifest.json"), JSON.stringify({ order, views }, null, 2));
console.log(`scripts ${scripts.length}, views ${views.join(", ")}, keyframes ${keyframes.length}, chrome ${chrome.length}`);