import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync, realpathSync } from 'node:fs'
import { delimiter, dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
export const APP_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..')
export const MIN_NODE_MAJOR = 24
function readJson(filePath) {
try {
return JSON.parse(readFileSync(filePath, 'utf8'))
} catch {
return null
}
}
function supportedNodeVersion(version) {
const [major = 0] = String(version || '').split('.').map(Number)
return major >= MIN_NODE_MAJOR
}
export function nodeArch(nodePath) {
try {
return execFileSync(nodePath, ['-p', 'process.arch'], { encoding: 'utf8' }).trim()
} catch {
return ''
}
}
export function nodeVersion(nodePath) {
try {
return execFileSync(nodePath, ['-p', 'process.versions.node'], { encoding: 'utf8' }).trim()
} catch {
return ''
}
}
function nativeBinaryArch(filePath) {
if (!existsSync(filePath)) return ''
try {
const output = execFileSync('file', [filePath], { encoding: 'utf8' })
if (output.includes('arm64')) return 'arm64'
if (output.includes('x86_64')) return 'x64'
} catch {
}
return ''
}
function macHardwareArch() {
if (process.platform !== 'darwin') return process.arch
try {
const supportsArm = execFileSync('sysctl', ['-n', 'hw.optional.arm64'], { encoding: 'utf8' }).trim()
if (supportsArm === '1') return 'arm64'
return process.arch
} catch {
return ''
}
}
function uniqueExistingPaths(paths) {
const seen = new Set()
return paths.filter((filePath) => {
if (!filePath || !existsSync(filePath)) return false
let key = filePath
try {
key = realpathSync(filePath)
} catch {
}
if (seen.has(key)) return false
seen.add(key)
return true
})
}
function runtimeMarker(appDir) {
const marker = readJson(join(appDir, '.desktop-deps.json'))
return marker?.platform === process.platform ? marker : null
}
function runtimeCandidates(targetArch, env) {
const pathCandidates = String(env.PATH || '')
.split(delimiter)
.filter(Boolean)
.map((dir) => join(dir, process.platform === 'win32' ? 'node.exe' : 'node'))
const preferred = process.platform === 'darwin'
? targetArch === 'arm64'
? ['/opt/homebrew/bin/node', '/usr/local/bin/node']
: ['/usr/local/bin/node', '/opt/homebrew/bin/node']
: []
return uniqueExistingPaths([
env.DSH_PROJECT_NODE,
env.DSH_NODE_BIN,
...preferred,
process.execPath,
...pathCandidates,
])
}
export function resolveProjectNode({ appDir = APP_DIR, env = process.env } = {}) {
const marker = runtimeMarker(appDir)
const sqliteArch = nativeBinaryArch(
join(appDir, 'server', 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node'),
)
const targetArch = env.DSH_PROJECT_ARCH || macHardwareArch() || sqliteArch || marker?.arch || process.arch
const targetMajor = Number(marker?.nodeMajor || 0)
const candidates = runtimeCandidates(targetArch, env).map((filePath) => ({
filePath,
arch: nodeArch(filePath),
version: nodeVersion(filePath),
}))
const compatible = candidates.filter(
(candidate) => candidate.arch === targetArch && supportedNodeVersion(candidate.version),
)
if (targetMajor) {
const exact = compatible.find(
(candidate) => Number(candidate.version.split('.')[0]) === targetMajor,
)
if (exact) return exact.filePath
}
if (compatible.length) return compatible[0].filePath
const found = candidates
.map((candidate) => `${candidate.filePath} (${candidate.arch || 'unknown'}, v${candidate.version || 'unknown'})`)
.join(', ')
throw new Error(
`找不到 ${targetArch} 且 Node >=${MIN_NODE_MAJOR} 的运行时。`
+ `${found ? ` 已检查: ${found}` : ''}`,
)
}
export function projectNodeEnv(nodePath, env = process.env) {
return {
...env,
PATH: `${dirname(nodePath)}${delimiter}${env.PATH || ''}`,
DSH_PROJECT_NODE: nodePath,
DSH_NODE_BIN: nodePath,
npm_node_execpath: nodePath,
}
}
export function resolveNpmCli(nodePath, env = process.env) {
if (env.npm_execpath && existsSync(env.npm_execpath)) return env.npm_execpath
const adjacentNpm = join(dirname(nodePath), process.platform === 'win32' ? 'npm.cmd' : 'npm')
if (existsSync(adjacentNpm) && process.platform !== 'win32') {
try {
const resolved = realpathSync(adjacentNpm)
if (resolved.endsWith('.js')) return resolved
} catch {
}
}
for (const filePath of [
'/opt/homebrew/lib/node_modules/npm/bin/npm-cli.js',
'/usr/local/lib/node_modules/npm/bin/npm-cli.js',
]) {
if (existsSync(filePath)) return filePath
}
throw new Error('找不到 npm CLI,请先安装 Node 24 或更高版本')
}