diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs index 0d5774680..96df2d6a1 100644 --- a/config/electron-builder.config.cjs +++ b/config/electron-builder.config.cjs @@ -4,6 +4,7 @@ const { join, resolve } = require('node:path') const electronBuilderNativeRebuild = require('./scripts/electron-builder-native-rebuild.cjs') const { createPackagedRuntimeNodeModuleResources, + prunePackagedRuntimeNodeModules, verifyPackagedMainRuntimeDeps } = require('./packaged-runtime-node-modules.cjs') @@ -122,6 +123,7 @@ module.exports = { if (!existsSync(resourcesDir)) { return } + prunePackagedRuntimeNodeModules(resourcesDir, context.electronPlatformName) verifyPackagedMainRuntimeDeps(resourcesDir) for (const filename of readdirSync(resourcesDir)) { if (!filename.startsWith('agent-browser-')) { diff --git a/config/packaged-runtime-node-modules.cjs b/config/packaged-runtime-node-modules.cjs index 662a781eb..f55100751 100644 --- a/config/packaged-runtime-node-modules.cjs +++ b/config/packaged-runtime-node-modules.cjs @@ -1,4 +1,4 @@ -const { existsSync, readFileSync, realpathSync } = require('node:fs') +const { existsSync, readFileSync, readdirSync, realpathSync, rmSync } = require('node:fs') const { dirname, join, resolve } = require('node:path') const { builtinModules, createRequire } = require('node:module') @@ -19,6 +19,14 @@ const PACKAGED_RUNTIME_PACKAGE_ROOTS = [ '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}`) @@ -150,11 +158,99 @@ function verifyPackagedMainRuntimeDeps(resourcesDir, asar = require('@electron/a } } +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 } diff --git a/config/scripts/electron-builder-config.test.mjs b/config/scripts/electron-builder-config.test.mjs index f9585679b..9c6092ebb 100644 --- a/config/scripts/electron-builder-config.test.mjs +++ b/config/scripts/electron-builder-config.test.mjs @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -7,7 +7,14 @@ import { describe, expect, it } from 'vitest' const require = createRequire(import.meta.url) const electronBuilderConfig = require('../electron-builder.config.cjs') const electronBuilderNativeRebuild = require('./electron-builder-native-rebuild.cjs') -const { findAsarEntry, verifyPackagedMainRuntimeDeps } = require('../packaged-runtime-node-modules.cjs') +const { + findAsarEntry, + prunePackagedNodePty, + prunePackagedSherpaOnnx, + prunePackagedRuntimeTypeDeclarations, + prunePackagedZodSources, + verifyPackagedMainRuntimeDeps +} = require('../packaged-runtime-node-modules.cjs') describe('electron-builder config', () => { it('excludes repo-only source trees from app.asar', () => { @@ -104,4 +111,90 @@ describe('electron-builder config', () => { ) expect(findAsarEntry(['/out/main/index.js'], 'out/main/index.js')).toBe('/out/main/index.js') }) + + it('prunes non-target node-pty prebuilds from packaged runtime resources', async () => { + const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-node-pty-prune-')) + try { + const prebuildsDir = join(resourcesDir, 'node_modules', 'node-pty', 'prebuilds') + await mkdir(join(prebuildsDir, 'darwin-arm64'), { recursive: true }) + await mkdir(join(prebuildsDir, 'darwin-x64'), { recursive: true }) + await mkdir(join(prebuildsDir, 'linux-x64'), { recursive: true }) + await mkdir(join(prebuildsDir, 'win32-x64'), { recursive: true }) + await mkdir(join(resourcesDir, 'node_modules', 'node-pty', 'third_party', 'conpty'), { + recursive: true + }) + await mkdir(join(resourcesDir, 'node_modules', 'node-pty', 'deps', 'winpty'), { + recursive: true + }) + + prunePackagedNodePty(resourcesDir, 'darwin') + + await expect(readdir(prebuildsDir).then((entries) => entries.sort())).resolves.toEqual([ + 'darwin-arm64', + 'darwin-x64' + ]) + await expect( + readdir(join(resourcesDir, 'node_modules', 'node-pty', 'third_party')) + ).resolves.toEqual([]) + await expect(readdir(join(resourcesDir, 'node_modules', 'node-pty', 'deps'))).resolves.toEqual( + [] + ) + } finally { + await rm(resourcesDir, { recursive: true, force: true }) + } + }) + + it('prunes type declaration artifacts from packaged runtime node_modules', async () => { + const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-type-prune-')) + try { + const packageDir = join(resourcesDir, 'node_modules', 'example-package') + await mkdir(join(packageDir, 'dist'), { recursive: true }) + await writeFile(join(packageDir, 'dist', 'index.cjs'), 'module.exports = {}', 'utf8') + await writeFile(join(packageDir, 'dist', 'index.d.ts'), 'export type Value = string', 'utf8') + await writeFile(join(packageDir, 'dist', 'index.d.cts'), 'export type Value = string', 'utf8') + await writeFile(join(packageDir, 'dist', 'index.d.mts.map'), '{}', 'utf8') + + prunePackagedRuntimeTypeDeclarations(resourcesDir) + + await expect(readdir(join(packageDir, 'dist'))).resolves.toEqual(['index.cjs']) + } finally { + await rm(resourcesDir, { recursive: true, force: true }) + } + }) + + it('prunes duplicate darwin sherpa-onnx runtime dylib aliases', async () => { + const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-sherpa-prune-')) + try { + const packageDir = join(resourcesDir, 'node_modules', 'sherpa-onnx-darwin-arm64') + await mkdir(packageDir, { recursive: true }) + await writeFile(join(packageDir, 'sherpa-onnx.node'), '', 'utf8') + await writeFile(join(packageDir, 'libonnxruntime.1.23.2.dylib'), '', 'utf8') + await writeFile(join(packageDir, 'libonnxruntime.dylib'), '', 'utf8') + + prunePackagedSherpaOnnx(resourcesDir, 'darwin') + + await expect(readdir(packageDir).then((entries) => entries.sort())).resolves.toEqual([ + 'libonnxruntime.1.23.2.dylib', + 'sherpa-onnx.node' + ]) + } finally { + await rm(resourcesDir, { recursive: true, force: true }) + } + }) + + it('prunes zod TypeScript sources from packaged runtime resources', async () => { + const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-zod-prune-')) + try { + const packageDir = join(resourcesDir, 'node_modules', 'zod') + await mkdir(join(packageDir, 'src'), { recursive: true }) + await writeFile(join(packageDir, 'index.cjs'), 'module.exports = {}', 'utf8') + await writeFile(join(packageDir, 'src', 'index.ts'), 'export const value = true', 'utf8') + + prunePackagedZodSources(resourcesDir) + + await expect(readdir(packageDir)).resolves.toEqual(['index.cjs']) + } finally { + await rm(resourcesDir, { recursive: true, force: true }) + } + }) })