diff --git a/package.json b/package.json index 75c61f9c..9a3129b6 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "scripts": { "dev": "vite --config vite.config.ts", "postinstall": "node scripts/postinstall.mjs", - "build": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run smoke:electron-main-cjs && electron-builder", + "build": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run normalize:electron-main-cjs && npm run smoke:electron-main-cjs && electron-builder", "lint": "biome check .", "lint:fix": "biome check --write .", "format": "biome format --write .", @@ -30,11 +30,12 @@ "build:nvidia-cuda-compositor": "node scripts/build-nvidia-cuda-compositor.mjs", "build:windows-capture": "node scripts/build-windows-capture.mjs", "build:cursor-monitor": "node scripts/build-cursor-monitor.mjs", - "build:mac": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run smoke:electron-main-cjs && electron-builder --mac", - "build:win": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run smoke:electron-main-cjs && electron-builder --win", - "build:linux": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run smoke:electron-main-cjs && electron-builder --linux", + "build:mac": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run normalize:electron-main-cjs && npm run smoke:electron-main-cjs && electron-builder --mac", + "build:win": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run normalize:electron-main-cjs && npm run smoke:electron-main-cjs && electron-builder --win", + "build:linux": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run normalize:electron-main-cjs && npm run smoke:electron-main-cjs && electron-builder --linux", "i18n:check": "node scripts/i18n-check.mjs", "benchmark:export-queues": "node scripts/benchmark-export-queues.mjs", + "normalize:electron-main-cjs": "node scripts/normalize-electron-main-cjs.mjs", "smoke:electron-main-cjs": "node scripts/smoke-electron-main-cjs.mjs", "smoke:packaged-binaries": "node scripts/smoke-packaged-binaries.mjs", "checksums:release": "node scripts/write-release-checksums.mjs", diff --git a/scripts/normalize-electron-main-cjs.mjs b/scripts/normalize-electron-main-cjs.mjs new file mode 100644 index 00000000..ca27a2d2 --- /dev/null +++ b/scripts/normalize-electron-main-cjs.mjs @@ -0,0 +1,147 @@ +import fs from "node:fs/promises"; +import { pathToFileURL } from "node:url"; + +const mainBundleUrl = new URL("../dist-electron/main.cjs", import.meta.url); + +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; +} + +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 firstUnprocessedLine = 0; + + for (const line of lines) { + if (/^[ \t]*$/.test(line)) { + normalizedLines.push(line); + firstUnprocessedLine += 1; + continue; + } + + const converted = convertImportLine(line); + if (converted === null) { + break; + } + + normalizedLines.push(converted); + firstUnprocessedLine += 1; + changed = true; + } + + if (!changed) { + return { source, changed }; + } + + const normalized = [ + ...normalizedLines, + ...lines.slice(firstUnprocessedLine), + ].join(lineBreak); + + return { source: normalized, changed }; +} + +export function findElectronMainCjsEsmSyntax(source) { + const lines = source.split(/\r?\n/); + const matches = []; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + if (/^[ \t]*$/.test(line)) { + continue; + } + + const converted = convertImportLine(line); + if (converted === null) { + break; + } + + matches.push({ + line: index + 1, + text: line.trim(), + }); + } + + 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", + ); +} diff --git a/scripts/smoke-electron-main-cjs.mjs b/scripts/smoke-electron-main-cjs.mjs index 0586723b..f0e3457f 100644 --- a/scripts/smoke-electron-main-cjs.mjs +++ b/scripts/smoke-electron-main-cjs.mjs @@ -1,28 +1,30 @@ -import fs from "node:fs/promises"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { + findElectronMainCjsEsmSyntax, + normalizeElectronMainCjs, +} from "./normalize-electron-main-cjs.mjs"; const mainBundleUrl = new URL("../dist-electron/main.cjs", import.meta.url); -const mainBundlePath = mainBundleUrl.pathname; +const mainBundlePath = fileURLToPath(mainBundleUrl); -let source; -try { - source = await fs.readFile(mainBundleUrl, "utf8"); -} catch (error) { - throw new Error(`Unable to read dist-electron/main.cjs: ${error}`); -} - -const esmImportPattern = /^[ \t]*import\s+(?:(?:[\w*{][^\n;]*?)\s+from\s+)?["'][^"']+["'];?/gm; -const matches = [...source.matchAll(esmImportPattern)].map((match) => ({ - line: source.slice(0, match.index).split(/\r?\n/).length, - text: match[0].trim(), -})); +const { source } = await normalizeElectronMainCjs(mainBundleUrl); +const matches = findElectronMainCjsEsmSyntax(source); if (matches.length > 0) { const details = matches.map((match) => `line ${match.line}: ${match.text}`).join("\n"); throw new Error(`dist-electron/main.cjs contains ESM import syntax:\n${details}`); } -if (/\bimport\.meta\b/.test(source)) { - throw new Error("dist-electron/main.cjs contains import.meta syntax"); +const checkResult = spawnSync(process.execPath, ["--check", mainBundlePath], { + encoding: "utf8", +}); + +if (checkResult.status !== 0) { + const details = [checkResult.stdout, checkResult.stderr].filter(Boolean).join("\n"); + throw new Error( + `dist-electron/main.cjs does not parse as CommonJS:${details ? `\n${details}` : ""}`, + ); } console.log(`Electron main CJS smoke passed: ${mainBundlePath}`); diff --git a/vite.config.ts b/vite.config.ts index 5fce54bb..64614cdb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,29 @@ +import { spawnSync } from "node:child_process"; import path from "node:path"; import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; +import { defineConfig, type Plugin } from "vite"; import electron from "vite-plugin-electron/simple"; +function electronMainCjsGuardPlugin(): Plugin { + return { + name: "recordly-electron-main-cjs-guard", + closeBundle() { + const scriptPath = path.resolve(__dirname, "scripts/smoke-electron-main-cjs.mjs"); + const result = spawnSync(process.execPath, [scriptPath], { + cwd: __dirname, + encoding: "utf8", + }); + + if (result.status !== 0) { + const details = [result.stdout, result.stderr].filter(Boolean).join("\n"); + throw new Error( + `Electron main CJS smoke failed after Vite build.${details ? `\n${details}` : ""}`, + ); + } + }, + }; +} + // https://vitejs.dev/config/ export default defineConfig({ plugins: [ @@ -26,6 +47,7 @@ export default defineConfig({ }, }, }, + plugins: [electronMainCjsGuardPlugin()], }, }, preload: {