Files
Recordly/scripts/normalize-electron-main-cjs.mjs

476 lines
10 KiB
JavaScript

import fs from "node:fs/promises";
import { pathToFileURL } from "node:url";
const mainBundleUrl = new URL("../dist-electron/main.cjs", import.meta.url);
const IMPORT_META_URL_CJS_REPLACEMENT = 'require("node:url").pathToFileURL(__filename).href';
function convertNamedImports(namedSpec) {
return namedSpec.replace(/\s+as\s+/g, ": ");
}
function convertImportLine(line) {
const importFromMatch = line.match(
/^([ \t]*)import\s+([^;\n]+?)\s+from\s+(["'][^"']+["'])\s*;?[ \t]*$/,
);
if (importFromMatch) {
const [, indent, rawSpec, moduleLiteral] = importFromMatch;
const spec = rawSpec.trim();
if (spec.startsWith("* as ")) {
return `${indent}const ${spec.slice(5).trim()} = require(${moduleLiteral});`;
}
if (spec.startsWith("{")) {
return `${indent}const ${convertNamedImports(spec)} = require(${moduleLiteral});`;
}
const commaIndex = spec.indexOf(",");
if (commaIndex >= 0) {
const defaultName = spec.slice(0, commaIndex).trim();
const namedSpec = spec.slice(commaIndex + 1).trim();
return [
`${indent}const ${defaultName} = require(${moduleLiteral});`,
`${indent}const ${convertNamedImports(namedSpec)} = ${defaultName};`,
].join("\n");
}
return `${indent}const ${spec} = require(${moduleLiteral});`;
}
const sideEffectImportMatch = line.match(/^([ \t]*)import\s+(["'][^"']+["'])\s*;?[ \t]*$/);
if (sideEffectImportMatch) {
const [, indent, moduleLiteral] = sideEffectImportMatch;
return `${indent}require(${moduleLiteral});`;
}
if (/^[ \t]*export\s*\{\s*\}\s*;?[ \t]*$/.test(line)) {
return "";
}
return null;
}
function convertNamedExports(namedSpec, indent = "") {
const statements = [];
for (const rawSpecifier of namedSpec.split(",")) {
const specifier = rawSpecifier.trim();
if (!specifier) {
continue;
}
const aliasMatch = specifier.match(
/^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/,
);
const localName = aliasMatch ? aliasMatch[1] : specifier;
const exportName = aliasMatch ? aliasMatch[2] : specifier;
if (
!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(localName) ||
!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(exportName)
) {
return null;
}
statements.push(`${indent}exports.${exportName} = ${localName};`);
}
return statements;
}
function convertExportLine(line) {
const singleLineMatch = line.match(
/^([ \t]*)export\s*\{\s*([^}]*)\s*\}\s*;?[ \t]*$/,
);
if (singleLineMatch) {
const [, indent, namedSpec] = singleLineMatch;
return convertNamedExports(namedSpec, indent);
}
const blockStartMatch = line.match(/^([ \t]*)export\s*\{\s*$/);
if (blockStartMatch) {
return { indent: blockStartMatch[1], specifiers: [], rawLines: [line] };
}
return null;
}
function updateLexicalState(line, state) {
let mode = state.mode;
let escaped = false;
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
const next = line[index + 1];
if (mode === "block-comment") {
if (char === "*" && next === "/") {
mode = null;
index += 1;
}
continue;
}
if (mode === "single-quote" || mode === "double-quote" || mode === "template") {
if (escaped) {
escaped = false;
continue;
}
if (char === "\\") {
escaped = true;
continue;
}
if (
(mode === "single-quote" && char === "'") ||
(mode === "double-quote" && char === '"') ||
(mode === "template" && char === "`")
) {
mode = null;
}
continue;
}
if (char === "/" && next === "/") {
break;
}
if (char === "/" && next === "*") {
mode = "block-comment";
index += 1;
continue;
}
if (char === "'") {
mode = "single-quote";
} else if (char === '"') {
mode = "double-quote";
} else if (char === "`") {
mode = "template";
}
}
if (mode === "single-quote" || mode === "double-quote") {
mode = null;
}
return { mode };
}
function hasTokenBoundary(line, startIndex, endIndex) {
const before = line[startIndex - 1];
const after = line[endIndex];
const isIdentifier = (char) => Boolean(char && /[A-Za-z0-9_$]/.test(char));
return !isIdentifier(before) && !isIdentifier(after);
}
function replaceImportMetaUrlInCode(line, state) {
const token = "import.meta.url";
let mode = state.mode;
let escaped = false;
let changed = false;
let normalizedLine = "";
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
const next = line[index + 1];
if (mode === "block-comment") {
normalizedLine += char;
if (char === "*" && next === "/") {
normalizedLine += next;
mode = null;
index += 1;
}
continue;
}
if (mode === "single-quote" || mode === "double-quote" || mode === "template") {
normalizedLine += char;
if (escaped) {
escaped = false;
continue;
}
if (char === "\\") {
escaped = true;
continue;
}
if (
(mode === "single-quote" && char === "'") ||
(mode === "double-quote" && char === '"') ||
(mode === "template" && char === "`")
) {
mode = null;
}
continue;
}
if (char === "/" && next === "/") {
normalizedLine += line.slice(index);
break;
}
if (char === "/" && next === "*") {
normalizedLine += char + next;
mode = "block-comment";
index += 1;
continue;
}
if (char === "'") {
normalizedLine += char;
mode = "single-quote";
continue;
}
if (char === '"') {
normalizedLine += char;
mode = "double-quote";
continue;
}
if (char === "`") {
normalizedLine += char;
mode = "template";
continue;
}
if (
line.startsWith(token, index) &&
hasTokenBoundary(line, index, index + token.length)
) {
normalizedLine += IMPORT_META_URL_CJS_REPLACEMENT;
changed = true;
index += token.length - 1;
continue;
}
normalizedLine += char;
}
if (mode === "single-quote" || mode === "double-quote") {
mode = null;
}
return {
line: normalizedLine,
state: { mode },
changed,
};
}
function containsImportMetaInCode(line, state) {
const token = "import.meta";
let mode = state.mode;
let escaped = false;
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
const next = line[index + 1];
if (mode === "block-comment") {
if (char === "*" && next === "/") {
mode = null;
index += 1;
}
continue;
}
if (mode === "single-quote" || mode === "double-quote" || mode === "template") {
if (escaped) {
escaped = false;
continue;
}
if (char === "\\") {
escaped = true;
continue;
}
if (
(mode === "single-quote" && char === "'") ||
(mode === "double-quote" && char === '"') ||
(mode === "template" && char === "`")
) {
mode = null;
}
continue;
}
if (char === "/" && next === "/") {
break;
}
if (char === "/" && next === "*") {
mode = "block-comment";
index += 1;
continue;
}
if (char === "'") {
mode = "single-quote";
continue;
}
if (char === '"') {
mode = "double-quote";
continue;
}
if (char === "`") {
mode = "template";
continue;
}
if (
line.startsWith(token, index) &&
hasTokenBoundary(line, index, index + token.length)
) {
return true;
}
}
return false;
}
export function normalizeElectronMainCjsSource(source) {
let changed = false;
const lineBreak = source.includes("\r\n") ? "\r\n" : "\n";
const lines = source.split(/\r?\n/);
const normalizedLines = [];
let state = { mode: null };
let exportBlock = null;
for (const line of lines) {
if (exportBlock) {
if (/^[ \t]*\}\s*;?[ \t]*$/.test(line)) {
const statements = convertNamedExports(
exportBlock.specifiers.join(","),
exportBlock.indent,
);
if (statements === null) {
normalizedLines.push(...exportBlock.rawLines, line);
} else {
normalizedLines.push(...statements);
changed = true;
}
exportBlock = null;
continue;
}
exportBlock.specifiers.push(line.trim().replace(/,$/, ""));
exportBlock.rawLines.push(line);
continue;
}
if (state.mode === null) {
const converted = convertImportLine(line);
if (converted !== null) {
normalizedLines.push(converted);
changed = true;
continue;
}
const convertedExport = convertExportLine(line);
if (convertedExport !== null) {
if (Array.isArray(convertedExport)) {
normalizedLines.push(...convertedExport);
changed = true;
} else {
exportBlock = convertedExport;
}
continue;
}
}
const normalized = replaceImportMetaUrlInCode(line, state);
if (normalized.changed) {
changed = true;
}
normalizedLines.push(normalized.line);
state = normalized.state;
}
if (!changed) {
return { source, changed };
}
const normalized = normalizedLines.join(lineBreak);
return { source: normalized, changed };
}
export function findElectronMainCjsEsmSyntax(source) {
const lines = source.split(/\r?\n/);
const matches = [];
let state = { mode: null };
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
if (state.mode === null) {
const converted = convertImportLine(line);
if (converted !== null) {
matches.push({
line: index + 1,
text: line.trim(),
});
continue;
}
if (containsImportMetaInCode(line, state)) {
matches.push({
line: index + 1,
text: line.trim(),
});
continue;
}
if (/^[ \t]*export\b/.test(line)) {
matches.push({
line: index + 1,
text: line.trim(),
});
continue;
}
}
state = updateLexicalState(line, state);
}
return matches;
}
export async function normalizeElectronMainCjs(bundleUrl = mainBundleUrl) {
let source;
try {
source = await fs.readFile(bundleUrl, "utf8");
} catch (error) {
throw new Error(`Unable to read dist-electron/main.cjs: ${error}`);
}
const normalized = normalizeElectronMainCjsSource(source);
if (normalized.changed) {
await fs.writeFile(bundleUrl, normalized.source, "utf8");
}
return normalized;
}
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
if (isDirectRun) {
const result = await normalizeElectronMainCjs();
const remainingImports = findElectronMainCjsEsmSyntax(result.source);
if (remainingImports.length > 0) {
const details = remainingImports
.map((match) => `line ${match.line}: ${match.text}`)
.join("\n");
throw new Error(`dist-electron/main.cjs still contains ESM import syntax:\n${details}`);
}
console.log(
result.changed
? "Electron main CJS normalized: dist-electron/main.cjs"
: "Electron main CJS already normalized: dist-electron/main.cjs",
);
}