257 lines
8.2 KiB
JavaScript
257 lines
8.2 KiB
JavaScript
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/<platform>-<arch>, 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
|
|
}
|