424 lines
14 KiB
JavaScript
424 lines
14 KiB
JavaScript
const {
|
|
copyFileSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
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',
|
|
'@parcel/watcher',
|
|
'electron-updater',
|
|
'i18next',
|
|
'jsonc-parser',
|
|
'node-pty',
|
|
'posthog-node',
|
|
// serve-sim (for CLI JS entry + closure + state/middleware + to make packaged require('serve-sim') + its internal relatives work; mirrors other runtime JS like ws/yaml/zod. Natives/dylibs still via extraResources + the node_modules/serve-sim copy in resources from builder. Client if added too.
|
|
'serve-sim',
|
|
'qrcode',
|
|
'ssh2',
|
|
'tweetnacl',
|
|
'ws',
|
|
'yaml',
|
|
'zod'
|
|
]
|
|
|
|
const NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM = {
|
|
darwin: 'darwin-',
|
|
linux: 'linux-',
|
|
win32: 'win32-'
|
|
}
|
|
const NODE_PTY_CONPTY_RUNTIME_FILES = ['conpty.dll', 'OpenConsole.exe']
|
|
const PARCEL_WATCHER_PLATFORM_PREFIX_BY_PLATFORM = {
|
|
darwin: 'watcher-darwin',
|
|
linux: 'watcher-linux',
|
|
win32: 'watcher-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) {
|
|
const nested = join(fromDir, 'node_modules', packageName, 'package.json')
|
|
if (existsSync(nested)) {
|
|
return nested
|
|
}
|
|
// Why: published serve-sim has no "." export (only ./middleware and ./state), so
|
|
// require.resolve('serve-sim') fails even though the package is present for bridge exec.
|
|
if (packageName === 'serve-sim') {
|
|
const direct = join(projectDir, 'node_modules', 'serve-sim', 'package.json')
|
|
if (existsSync(direct)) {
|
|
return direct
|
|
}
|
|
}
|
|
try {
|
|
return requireFromProject.resolve(`${packageName}/package.json`, { paths: [fromDir] })
|
|
} catch {
|
|
let entryPath
|
|
try {
|
|
entryPath = requireFromProject.resolve(packageName, { paths: [fromDir] })
|
|
} catch {
|
|
throw new Error(`Could not resolve package ${packageName} from ${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 isKnownOmittedServeSimDependency(packageName, fromDir) {
|
|
if (packageName !== 'inspect-webkit') {
|
|
return false
|
|
}
|
|
const serveSimPackageJsonPath = join(projectDir, 'node_modules', 'serve-sim', 'package.json')
|
|
if (!existsSync(serveSimPackageJsonPath)) {
|
|
return false
|
|
}
|
|
try {
|
|
return realpathSync(fromDir) === realpathSync(dirname(serveSimPackageJsonPath))
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function collectPackagedRuntimePackages() {
|
|
const packages = new Map()
|
|
const visit = (packageName, fromDir = projectDir) => {
|
|
if (packageName === 'electron' || packages.has(packageName)) {
|
|
return
|
|
}
|
|
|
|
let packageInfo
|
|
try {
|
|
packageInfo = readPackage(packageName, fromDir)
|
|
} catch (error) {
|
|
// Why: serve-sim declares inspect-webkit, but current installs omit it.
|
|
// Keep that escape hatch narrow so broken packages still fail packaging.
|
|
if (isKnownOmittedServeSimDependency(packageName, fromDir)) {
|
|
return
|
|
}
|
|
throw error
|
|
}
|
|
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)
|
|
}
|
|
|
|
// Why: @parcel/watcher loads its native .node addon from a platform-specific
|
|
// optionalDependency (e.g. @parcel/watcher-linux-x64-glibc) that the
|
|
// dependencies graph above never reaches. Include the ones installed for the
|
|
// build's supported architectures; afterPack pruning trims non-target
|
|
// platforms. Without this the packaged main bundle's import of
|
|
// '@parcel/watcher' resolves at runtime but throws loading its binary.
|
|
const parcelWatcherDir = packages.get('@parcel/watcher')
|
|
if (parcelWatcherDir) {
|
|
const parcelWatcherPackage = JSON.parse(
|
|
readFileSync(join(parcelWatcherDir, 'package.json'), 'utf8')
|
|
)
|
|
for (const optionalName of Object.keys(parcelWatcherPackage.optionalDependencies ?? {})) {
|
|
try {
|
|
visit(optionalName)
|
|
} catch {
|
|
// Optional platform subpackage is not installed for this build; skip it.
|
|
}
|
|
}
|
|
}
|
|
|
|
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 normalizeNodePtyWindowsArch(electronArch) {
|
|
if (electronArch === 'x64' || electronArch === 1) {
|
|
return 'x64'
|
|
}
|
|
if (electronArch === 'arm64' || electronArch === 3) {
|
|
return 'arm64'
|
|
}
|
|
return process.arch === 'arm64' ? 'arm64' : 'x64'
|
|
}
|
|
|
|
function findNodePtyConptySourceDir(nodePtyDir, windowsArch) {
|
|
const conptyRoot = join(nodePtyDir, 'third_party', 'conpty')
|
|
if (!existsSync(conptyRoot)) {
|
|
throw new Error(`Packaged node-pty is missing ${conptyRoot}`)
|
|
}
|
|
for (const entry of readdirSync(conptyRoot, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) {
|
|
continue
|
|
}
|
|
const sourceDir = join(conptyRoot, entry.name, `win10-${windowsArch}`)
|
|
if (existsSync(sourceDir)) {
|
|
return sourceDir
|
|
}
|
|
}
|
|
throw new Error(`Packaged node-pty has no ConPTY payload for win10-${windowsArch}`)
|
|
}
|
|
|
|
function ensurePackagedNodePtyConptyRuntime(nodePtyDir, electronArch) {
|
|
const releaseDir = join(nodePtyDir, 'build', 'Release')
|
|
if (!existsSync(join(releaseDir, 'conpty.node'))) {
|
|
return
|
|
}
|
|
|
|
const runtimeDir = join(releaseDir, 'conpty')
|
|
const missingRuntimeFiles = NODE_PTY_CONPTY_RUNTIME_FILES.filter(
|
|
(filename) => !existsSync(join(runtimeDir, filename))
|
|
)
|
|
if (missingRuntimeFiles.length === 0) {
|
|
return
|
|
}
|
|
|
|
const windowsArch = normalizeNodePtyWindowsArch(electronArch)
|
|
const sourceDir = findNodePtyConptySourceDir(nodePtyDir, windowsArch)
|
|
mkdirSync(runtimeDir, { recursive: true })
|
|
for (const filename of missingRuntimeFiles) {
|
|
const sourceFile = join(sourceDir, filename)
|
|
if (!existsSync(sourceFile)) {
|
|
throw new Error(`Packaged node-pty is missing ${sourceFile}`)
|
|
}
|
|
// Why: node-pty's Windows addon loads conpty.dll relative to conpty.node,
|
|
// but its install script can run before electron-builder gathers resources.
|
|
copyFileSync(sourceFile, join(runtimeDir, filename))
|
|
}
|
|
}
|
|
|
|
function prunePackagedNodePty(resourcesDir, electronPlatformName, electronArch) {
|
|
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') {
|
|
ensurePackagedNodePtyConptyRuntime(nodePtyDir, electronArch)
|
|
} else {
|
|
// 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 prunePackagedParcelWatcher(resourcesDir, electronPlatformName) {
|
|
const parcelDir = join(resourcesDir, 'node_modules', '@parcel')
|
|
if (!existsSync(parcelDir)) {
|
|
return
|
|
}
|
|
|
|
// Why: we package every installed @parcel/watcher-<platform> optional
|
|
// subpackage (supportedArchitectures fetches all), but each build only needs
|
|
// its own platform's binary. Keep the core package and the matching platform
|
|
// subpackages; drop the rest so a Linux serve doesn't ship macOS/Windows .node.
|
|
const keepPrefix = PARCEL_WATCHER_PLATFORM_PREFIX_BY_PLATFORM[electronPlatformName]
|
|
for (const entry of readdirSync(parcelDir, { withFileTypes: true })) {
|
|
if (!entry.isDirectory() || entry.name === 'watcher') {
|
|
continue
|
|
}
|
|
// Why: only ever prune the watcher's own platform subpackages. Guards against
|
|
// nuking an unrelated @parcel/* runtime dep if one is added to the roots later.
|
|
if (!entry.name.startsWith('watcher-')) {
|
|
continue
|
|
}
|
|
if (keepPrefix && entry.name.startsWith(keepPrefix)) {
|
|
continue
|
|
}
|
|
rmSync(join(parcelDir, entry.name), { 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, electronArch) {
|
|
prunePackagedNodePty(resourcesDir, electronPlatformName, electronArch)
|
|
prunePackagedParcelWatcher(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,
|
|
prunePackagedParcelWatcher,
|
|
prunePackagedRuntimeNodeModules,
|
|
prunePackagedSherpaOnnx,
|
|
prunePackagedRuntimeTypeDeclarations,
|
|
prunePackagedZodSources,
|
|
verifyPackagedMainRuntimeDeps
|
|
}
|