76 lines
2.9 KiB
JavaScript
76 lines
2.9 KiB
JavaScript
import { access, stat } from 'node:fs/promises'
|
|
import { constants } from 'node:fs'
|
|
import { homedir, platform } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { execFile } from 'node:child_process'
|
|
|
|
const SUPPORTED_VERSIONS = new Set(['3.8', '4.0', '4.1', '4.2', '4.3'])
|
|
|
|
async function executable(path) {
|
|
if (!path) return false
|
|
try {
|
|
await access(path, platform() === 'win32' ? constants.F_OK : constants.X_OK)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export async function findSpineExecutable() {
|
|
const candidates = []
|
|
if (process.env.SPINE_EXECUTABLE) candidates.push(process.env.SPINE_EXECUTABLE)
|
|
if (platform() === 'darwin') {
|
|
candidates.push(
|
|
'/Applications/Spine.app/Contents/MacOS/Spine',
|
|
join(homedir(), 'Applications/Spine.app/Contents/MacOS/Spine'),
|
|
)
|
|
} else if (platform() === 'win32') {
|
|
const programFiles = [process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.LOCALAPPDATA].filter(Boolean)
|
|
for (const root of programFiles) {
|
|
candidates.push(join(root, 'Spine', 'Spine.com'), join(root, 'Esoteric Software', 'Spine', 'Spine.com'))
|
|
}
|
|
} else {
|
|
candidates.push('/opt/Spine/Spine.sh', join(homedir(), 'Spine/Spine.sh'), '/usr/local/bin/Spine', '/usr/bin/Spine')
|
|
}
|
|
for (const candidate of candidates) if (await executable(candidate)) return candidate
|
|
return null
|
|
}
|
|
|
|
export function normalizeSpineVersion(value) {
|
|
const match = /^(3\.8|4\.[0-3])(?:\.\d+)?$/.exec(String(value || '').trim())
|
|
if (!match || !SUPPORTED_VERSIONS.has(match[1])) throw new Error(`不支持的 Spine 版本:${value}`)
|
|
return match[1]
|
|
}
|
|
|
|
function run(executablePath, args, timeoutMs = 180000) {
|
|
return new Promise((resolve, reject) => {
|
|
execFile(executablePath, args, { timeout: timeoutMs, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
if (error) {
|
|
const details = [stdout, stderr, error.message]
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
.replace(/^Licensed to:.*$/gim, 'Spine 授权信息已隐藏')
|
|
.trim()
|
|
reject(new Error(details || 'Spine 命令执行失败'))
|
|
return
|
|
}
|
|
resolve({ stdout, stderr })
|
|
})
|
|
})
|
|
}
|
|
|
|
export async function importSpineProject({ executablePath, inputPath, outputPath, projectName, spineVersion }) {
|
|
const version = normalizeSpineVersion(spineVersion)
|
|
const editorVersion = `${version}.xx`
|
|
const result = await run(executablePath, [
|
|
'--update', editorVersion,
|
|
'--input', inputPath,
|
|
'--output', outputPath,
|
|
'--import', projectName,
|
|
])
|
|
const output = await stat(outputPath).catch(() => null)
|
|
if (!output?.isFile() || output.size === 0) throw new Error('Spine 未生成有效的工程文件')
|
|
const detected = /Starting:\s+Spine\s+([0-9.]+)/i.exec(`${result.stdout}\n${result.stderr}`)?.[1] || editorVersion
|
|
return { ...result, editorVersion: detected, size: output.size }
|
|
}
|