Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions electron/mainCjsNormalize.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import {
findElectronMainCjsEsmSyntax,
normalizeElectronMainCjsSource,
} from "../scripts/normalize-electron-main-cjs.mjs";

describe("Electron main CJS normalizer", () => {
it("converts Rollup named export blocks to CommonJS assignments", () => {
const source = [
'const MAIN_DIST = "dist-electron";',
'const RENDERER_DIST = "dist";',
"const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;",
"export {",
" MAIN_DIST,",
" RENDERER_DIST,",
" VITE_DEV_SERVER_URL",
"};",
].join("\n");

const result = normalizeElectronMainCjsSource(source);

expect(result.changed).toBe(true);
expect(result.source).toContain("exports.MAIN_DIST = MAIN_DIST;");
expect(result.source).toContain("exports.RENDERER_DIST = RENDERER_DIST;");
expect(result.source).toContain("exports.VITE_DEV_SERVER_URL = VITE_DEV_SERVER_URL;");
expect(findElectronMainCjsEsmSyntax(result.source)).toEqual([]);
});

it("converts single-line aliased named exports to CommonJS assignments", () => {
const source = "export { VITE_DEV_SERVER_URL as devServerUrl };";

const result = normalizeElectronMainCjsSource(source);

expect(result.changed).toBe(true);
expect(result.source).toBe("exports.devServerUrl = VITE_DEV_SERVER_URL;");
expect(findElectronMainCjsEsmSyntax(result.source)).toEqual([]);
});

it("reports unsupported ESM export syntax that cannot be normalized safely", () => {
const source = "export default MAIN_DIST;";

expect(findElectronMainCjsEsmSyntax(source)).toEqual([
{ line: 1, text: "export default MAIN_DIST;" },
]);
});

it("preserves an unsupported export block exactly while normalizing other syntax", () => {
const source = [
'import fs from "node:fs";',
"export {",
' MAIN_DIST as "main-dist",',
"};",
].join("\n");

const result = normalizeElectronMainCjsSource(source);

expect(result.changed).toBe(true);
expect(result.source).toBe(
[
'const fs = require("node:fs");',
"export {",
' MAIN_DIST as "main-dist",',
"};",
].join("\n"),
);
expect(findElectronMainCjsEsmSyntax(result.source)).toEqual([
{ line: 2, text: "export {" },
]);
});
});
83 changes: 83 additions & 0 deletions scripts/normalize-electron-main-cjs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,49 @@ function convertImportLine(line) {
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;
Expand Down Expand Up @@ -296,15 +339,48 @@ export function normalizeElectronMainCjsSource(source) {
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 {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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);
Expand Down Expand Up @@ -348,6 +424,13 @@ export function findElectronMainCjsEsmSyntax(source) {
});
continue;
}
if (/^[ \t]*export\b/.test(line)) {
matches.push({
line: index + 1,
text: line.trim(),
});
continue;
}
}

state = updateLexicalState(line, state);
Expand Down
36 changes: 34 additions & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ import react from "@vitejs/plugin-react";
import { defineConfig, type Plugin } from "vite";
import electron from "vite-plugin-electron/simple";

function electronMainCjsOutputPlugin(): Plugin {
return {
name: "recordly-electron-main-cjs-output",
enforce: "post",
config(config) {
// Vite mergeConfig concatenates lib.formats with the plugin's ESM default.
config.build ??= {};
const build = config.build;
const lib = build.lib;
if (lib && typeof lib === "object") {
lib.formats = ["cjs"];
lib.fileName = (_format, entryName) => `${entryName}.cjs`;
}

build.rollupOptions ??= {};
const rollupOptions = build.rollupOptions;
const cjsOutput = {
format: "cjs" as const,
inlineDynamicImports: true,
entryFileNames: "[name].cjs",
chunkFileNames: "[name]-[hash].cjs",
};

rollupOptions.output = Array.isArray(rollupOptions.output)
? rollupOptions.output.map((output) => ({ ...output, ...cjsOutput }))
: { ...(rollupOptions.output ?? {}), ...cjsOutput };
},
};
}

function electronMainCjsGuardPlugin(): Plugin {
return {
name: "recordly-electron-main-cjs-guard",
Expand Down Expand Up @@ -37,17 +67,19 @@ export default defineConfig({
lib: {
entry: "electron/main.ts",
formats: ["cjs"],
fileName: (_format, entryName) => `${entryName}.cjs`,
},
rollupOptions: {
external: ["ffmpeg-static", "uiohook-napi"],
output: {
format: "cjs",
inlineDynamicImports: true,
entryFileNames: "[name].cjs",
chunkFileNames: "[name].cjs",
chunkFileNames: "[name]-[hash].cjs",
},
},
},
plugins: [electronMainCjsGuardPlugin()],
plugins: [electronMainCjsOutputPlugin(), electronMainCjsGuardPlugin()],
},
},
preload: {
Expand Down