const { existsSync, readFileSync, readdirSync, realpathSync, rmSync } = require('node:fs') const { dirname, join, resolve } = require('node:path') const { builtinModules, createRequire } = require('node:module') const projectDir = resolve(__dirname, '..') const requireFromProject = createRequire(join(projectDir, 'package.json')) const PACKAGED_RUNTIME_PACKAGE_ROOTS = [ '@electron-toolkit/utils', '@linear/sdk', 'electron-updater', 'node-pty', 'posthog-node', 'qrcode', 'ssh2', 'tweetnacl', 'ws', 'yaml', 'zod' ] const NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM = { darwin: 'darwin-', linux: 'linux-', win32: 'win32-' } const TYPE_DECLARATION_ARTIFACT_RE = /\.d\.(?:c|m)?ts(?:\.map)?$/ const VERSIONED_ONNXRUNTIME_DYLIB_RE = /^libonnxruntime\.\d[\d.]*\.dylib$/ const NODE_BUILTINS = new Set([ ...builtinModules, ...builtinModules.map((moduleName) => `node:${moduleName}`) ]) function packageNameFromSpecifier(specifier) { if (specifier.startsWith('@')) { const [scope, name] = specifier.split('/') return scope && name ? `${scope}/${name}` : specifier } return specifier.split('/')[0] } function isPackagedExternalSpecifier(specifier) { return ( !specifier.startsWith('.') && !specifier.startsWith('/') && specifier !== 'electron' && !NODE_BUILTINS.has(specifier) ) } function resolvePackageJsonPath(packageName, fromDir = projectDir) { try { return requireFromProject.resolve(`${packageName}/package.json`, { paths: [fromDir] }) } catch { const entryPath = requireFromProject.resolve(packageName, { paths: [fromDir] }) let dir = dirname(entryPath) while (dir !== dirname(dir)) { const packageJsonPath = join(dir, 'package.json') if (existsSync(packageJsonPath)) { return packageJsonPath } dir = dirname(dir) } throw new Error(`Could not find package.json for ${packageName}`) } } function readPackage(packageName, fromDir = projectDir) { const packageJsonPath = resolvePackageJsonPath(packageName, fromDir) const packageDir = realpathSync(dirname(packageJsonPath)) const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) return { name: packageJson.name ?? packageName, packageDir, dependencies: Object.keys(packageJson.dependencies ?? {}) } } function collectPackagedRuntimePackages() { const packages = new Map() const visit = (packageName, fromDir = projectDir) => { if (packageName === 'electron' || packages.has(packageName)) { return } const packageInfo = readPackage(packageName, fromDir) if (packages.has(packageInfo.name)) { return } packages.set(packageInfo.name, packageInfo.packageDir) for (const dependencyName of packageInfo.dependencies) { visit(dependencyName, packageInfo.packageDir) } } for (const packageName of PACKAGED_RUNTIME_PACKAGE_ROOTS) { visit(packageName) } return [...packages.entries()].sort(([left], [right]) => left.localeCompare(right)) } function createPackagedRuntimeNodeModuleResources() { return collectPackagedRuntimePackages().map(([packageName, packageDir]) => ({ from: packageDir, to: join('node_modules', ...packageName.split('/')) })) } function normalizeAsarEntryPath(entry) { return entry.replace(/\\/g, '/').replace(/^\/+/, '') } function findAsarEntry(entries, expectedPath) { return entries.find((entry) => normalizeAsarEntryPath(entry) === expectedPath) } function verifyPackagedMainRuntimeDeps(resourcesDir, asar = require('@electron/asar')) { const asarPath = join(resourcesDir, 'app.asar') if (!existsSync(asarPath)) { return } const mainFiles = ['out/main/index.js', 'out/main/agent-hooks/managed-agent-hook-controls.js'] const entries = asar.listPackage(asarPath) const missing = new Set() for (const file of mainFiles) { const entry = findAsarEntry(entries, file) if (!entry) { throw new Error(`Packaged main file ${file} was not found in ${asarPath}`) } // Why: @electron/asar lists entries with host separators; Windows returns // backslashes, and extractFile expects that same host-style path. const internalPath = entry.replace(/^[\\/]+/, '') const source = asar.extractFile(asarPath, internalPath).toString('utf8') for (const match of source.matchAll(/require\(["']([^"']+)["']\)/g)) { const specifier = match[1] if (!isPackagedExternalSpecifier(specifier)) { continue } const packageName = packageNameFromSpecifier(specifier) if (!existsSync(join(resourcesDir, 'node_modules', ...packageName.split('/')))) { missing.add(packageName) } } } if (missing.size > 0) { throw new Error( `Packaged main bundle has bare runtime imports without copied node_modules: ${[ ...missing ].join(', ')}` ) } } function prunePackagedNodePty(resourcesDir, electronPlatformName) { const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty') if (!existsSync(nodePtyDir)) { return } const allowedPrebuildPrefix = NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM[electronPlatformName] if (allowedPrebuildPrefix) { const prebuildsDir = join(nodePtyDir, 'prebuilds') if (existsSync(prebuildsDir)) { for (const entry of readdirSync(prebuildsDir, { withFileTypes: true })) { if (entry.isDirectory() && !entry.name.startsWith(allowedPrebuildPrefix)) { rmSync(join(prebuildsDir, entry.name), { recursive: true, force: true }) } } } } if (electronPlatformName !== 'win32') { // Why: conpty is Windows-only and node-pty resolves runtime binaries from // build/Release or prebuilds/-, not third_party/conpty. rmSync(join(nodePtyDir, 'third_party', 'conpty'), { recursive: true, force: true }) rmSync(join(nodePtyDir, 'deps', 'winpty'), { recursive: true, force: true }) } } function prunePackagedRuntimeTypeDeclarations(resourcesDir) { const nodeModulesDir = join(resourcesDir, 'node_modules') if (!existsSync(nodeModulesDir)) { return } pruneMatchingFiles(nodeModulesDir, (filename) => TYPE_DECLARATION_ARTIFACT_RE.test(filename)) } function prunePackagedSherpaOnnx(resourcesDir, electronPlatformName) { if (electronPlatformName !== 'darwin') { return } const nodeModulesDir = join(resourcesDir, 'node_modules') if (!existsSync(nodeModulesDir)) { return } for (const entry of readdirSync(nodeModulesDir, { withFileTypes: true })) { if (!entry.isDirectory() || !entry.name.startsWith('sherpa-onnx-darwin-')) { continue } const packageDir = join(nodeModulesDir, entry.name) const packageEntries = readdirSync(packageDir) const hasVersionedOnnxRuntime = packageEntries.some((filename) => VERSIONED_ONNXRUNTIME_DYLIB_RE.test(filename) ) if (hasVersionedOnnxRuntime) { // Why: darwin sherpa-onnx binaries link to the versioned ONNX Runtime // install name; the unversioned dylib is a duplicate fallback copy. rmSync(join(packageDir, 'libonnxruntime.dylib'), { force: true }) } } } function prunePackagedZodSources(resourcesDir) { // Why: Zod's src tree is TypeScript source only selected by the @zod/source // condition; packaged runtime import/require paths resolve to built JS. rmSync(join(resourcesDir, 'node_modules', 'zod', 'src'), { recursive: true, force: true }) } function prunePackagedRuntimeNodeModules(resourcesDir, electronPlatformName) { prunePackagedNodePty(resourcesDir, electronPlatformName) prunePackagedRuntimeTypeDeclarations(resourcesDir) prunePackagedSherpaOnnx(resourcesDir, electronPlatformName) prunePackagedZodSources(resourcesDir) } function pruneMatchingFiles(directory, shouldPrune) { for (const entry of readdirSync(directory, { withFileTypes: true })) { const entryPath = join(directory, entry.name) if (entry.isDirectory()) { pruneMatchingFiles(entryPath, shouldPrune) } else if (entry.isFile() && shouldPrune(entry.name)) { rmSync(entryPath, { force: true }) } } } module.exports = { PACKAGED_RUNTIME_PACKAGE_ROOTS, createPackagedRuntimeNodeModuleResources, findAsarEntry, isPackagedExternalSpecifier, packageNameFromSpecifier, prunePackagedNodePty, prunePackagedRuntimeNodeModules, prunePackagedSherpaOnnx, prunePackagedRuntimeTypeDeclarations, prunePackagedZodSources, verifyPackagedMainRuntimeDeps }