75 lines
4.0 KiB
JavaScript
75 lines
4.0 KiB
JavaScript
// Mechanical CommonJS -> ESM conversion of desktop LS Tools modules (no logic changes).
|
|
// usage: node tools/port-module.mjs <reference file> <output file>
|
|
// const x = require("y"); -> import x from "y";
|
|
// const { a, b: c } = require("y"); -> import { a, b as c } from "y";
|
|
// module.exports = { A, B: C }; -> export { A, C as B };
|
|
// module.exports = X; -> export default X;
|
|
// Relative requires get ".js" added. Anything else that is not this simple is reported, so it
|
|
// gets ported by hand instead of guessed.
|
|
import fs from "node:fs";
|
|
import * as acorn from "acorn";
|
|
|
|
const [src, dst] = process.argv.slice(2);
|
|
let text = fs.readFileSync(src, "utf8");
|
|
const problems = [];
|
|
|
|
const fixSpec = (s) => (s.startsWith(".") && !s.endsWith(".js") && !s.endsWith(".json") ? s + ".js" : s);
|
|
|
|
text = text.replace(/^const (\w+) = require\("([^"]+)"\);?$/gm, (_, name, spec) => `import ${name} from "${fixSpec(spec)}";`);
|
|
text = text.replace(/^const \{([^}]+)\} = require\("([^"]+)"\);?$/gm, (_, names, spec) => {
|
|
const list = names.split(",").map((n) => n.trim()).filter(Boolean).map((n) => n.replace(/^(\w+)\s*:\s*(\w+)$/, "$1 as $2"));
|
|
return `import { ${list.join(", ")} } from "${fixSpec(spec)}";`;
|
|
});
|
|
// module.exports = { ... } is parsed (acorn), not split on commas: values can be whole arrow
|
|
// functions or methods with commas inside.
|
|
{
|
|
const ast = acorn.parse(text, { ecmaVersion: "latest", sourceType: "module", allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true });
|
|
const st = ast.body.find(
|
|
(n) =>
|
|
n.type === "ExpressionStatement" &&
|
|
n.expression.type === "AssignmentExpression" &&
|
|
n.expression.left.type === "MemberExpression" &&
|
|
n.expression.left.object.name === "module" &&
|
|
n.expression.left.property.name === "exports" &&
|
|
n.expression.right.type === "ObjectExpression",
|
|
);
|
|
if (st) {
|
|
const consts = [];
|
|
const list = [];
|
|
for (const p of st.expression.right.properties) {
|
|
if (p.type !== "Property" || p.computed) {
|
|
problems.push(`module.exports: unsupported property ${text.slice(p.start, p.end).slice(0, 60)}`);
|
|
continue;
|
|
}
|
|
const key = p.key.type === "Identifier" ? p.key.name : String(p.key.value);
|
|
if (p.shorthand) list.push([key, key]);
|
|
else if (!p.method && p.value.type === "Identifier") list.push([p.value.name, key]);
|
|
else if (p.method || p.kind !== "init") {
|
|
// method shorthand / getter: keep the exact source inside a one-property object
|
|
consts.push(`const __export_${key} = ({ ${text.slice(p.start, p.end)} }).${key};`);
|
|
list.push([`__export_${key}`, key]);
|
|
} else {
|
|
// key: expression (e.g. texture: assets.texture, save: async (...) => {...})
|
|
consts.push(`const __export_${key} = ${text.slice(p.value.start, p.value.end)};`);
|
|
list.push([`__export_${key}`, key]);
|
|
}
|
|
}
|
|
const named = list.map(([local, key]) => (local === key ? key : `${local} as ${key}`));
|
|
// default export too: other modules do `const x = require("./y")` -> `import x from "./y.js"`
|
|
const defaults = list.map(([local, key]) => (local === key ? key : `${key}: ${local}`));
|
|
const out = `${consts.join("\n")}${consts.length ? "\n" : ""}export { ${named.join(", ")} };\nexport default { ${defaults.join(", ")} };\n`;
|
|
text = text.slice(0, st.start) + out + text.slice(st.end);
|
|
}
|
|
}
|
|
text = text.replace(/^module\.exports = (\w+);?\s*$/m, "export default $1;\n");
|
|
|
|
for (const [i, line] of text.split("\n").entries()) {
|
|
if (/\brequire\(/.test(line)) problems.push(`${i + 1}: require left: ${line.trim()}`);
|
|
if (/\bmodule\.exports\b|\bexports\.\w+\s*=/.test(line)) problems.push(`${i + 1}: exports left: ${line.trim()}`);
|
|
if (/\b__dirname\b|\b__filename\b|process\.(env|platform|argv)/.test(line)) problems.push(`${i + 1}: node global: ${line.trim()}`);
|
|
}
|
|
fs.mkdirSync(dst.replace(/[\\/][^\\/]+$/, ""), { recursive: true });
|
|
fs.writeFileSync(dst, `// Ported from LS Tools desktop: ${src.replace(/\\/g, "/").split("/src/").pop()} (mechanical CJS -> ESM)\n` + text);
|
|
console.log(`${dst}: ${problems.length ? problems.length + " to check" : "ok"}`);
|
|
for (const p of problems) console.log(" " + p);
|