refactor: 重构桌面端构建配置与依赖管理
- 删除桌面端构建配置文件 - 添加二进制文件复制工具,支持多平台目标映射并自动处理文件路径与扩展名,通过类型安全配置和错误处理确保构建流程稳定可靠。 - 添加依赖项以支持类型安全的模式定义和项目配置,并新增脚本用于复制二进制文件。 - 添加桌面应用的TypeScript配置文件并继承统一的tsconfig设置,排除node_modules和src-tauri目录。 - 添加必要的开发依赖项以支持类型检查和构建工具链
This commit is contained in:
452
apps/desktop/copy-binaries.ts
Normal file
452
apps/desktop/copy-binaries.ts
Normal file
@@ -0,0 +1,452 @@
|
||||
import * as path from 'node:path'
|
||||
import { Schema } from '@effect/schema'
|
||||
import { $ } from 'bun'
|
||||
import { Console, Context, Data, Effect, Layer } from 'effect'
|
||||
|
||||
// ============================================================================
|
||||
// Domain Models & Schema
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Bun 构建目标后缀
|
||||
*/
|
||||
const BunTargetSuffixSchema = Schema.Literal(
|
||||
'windows-x64',
|
||||
'darwin-arm64',
|
||||
'darwin-x64',
|
||||
'linux-x64',
|
||||
'linux-arm64',
|
||||
)
|
||||
|
||||
type BunTargetSuffix = Schema.Schema.Type<typeof BunTargetSuffixSchema>
|
||||
|
||||
/**
|
||||
* Tauri sidecar 目标三元组
|
||||
*/
|
||||
const TauriTargetSchema = Schema.Literal(
|
||||
'x86_64-pc-windows-msvc',
|
||||
'aarch64-apple-darwin',
|
||||
'x86_64-apple-darwin',
|
||||
'x86_64-unknown-linux-gnu',
|
||||
'aarch64-unknown-linux-gnu',
|
||||
)
|
||||
|
||||
type TauriTarget = Schema.Schema.Type<typeof TauriTargetSchema>
|
||||
|
||||
/**
|
||||
* 目标映射配置
|
||||
*/
|
||||
const TargetMappingSchema = Schema.Struct({
|
||||
bunSuffix: BunTargetSuffixSchema,
|
||||
tauriTarget: TauriTargetSchema,
|
||||
})
|
||||
|
||||
type TargetMapping = Schema.Schema.Type<typeof TargetMappingSchema>
|
||||
|
||||
/**
|
||||
* 复制配置
|
||||
*/
|
||||
const CopyConfigSchema = Schema.Struct({
|
||||
sourceDir: Schema.String.pipe(Schema.nonEmptyString()),
|
||||
targetDir: Schema.String.pipe(Schema.nonEmptyString()),
|
||||
baseName: Schema.String.pipe(Schema.nonEmptyString()),
|
||||
mappings: Schema.Array(TargetMappingSchema).pipe(Schema.minItems(1)),
|
||||
})
|
||||
|
||||
type CopyConfig = Schema.Schema.Type<typeof CopyConfigSchema>
|
||||
|
||||
/**
|
||||
* 复制结果
|
||||
*/
|
||||
const CopyResultSchema = Schema.Struct({
|
||||
bunSuffix: BunTargetSuffixSchema,
|
||||
tauriTarget: TauriTargetSchema,
|
||||
sourceFile: Schema.String,
|
||||
targetFile: Schema.String,
|
||||
success: Schema.Boolean,
|
||||
})
|
||||
|
||||
type CopyResult = Schema.Schema.Type<typeof CopyResultSchema>
|
||||
|
||||
// ============================================================================
|
||||
// Error Models
|
||||
// ============================================================================
|
||||
|
||||
class ConfigError extends Data.TaggedError('ConfigError')<{
|
||||
readonly message: string
|
||||
readonly cause: unknown
|
||||
}> {}
|
||||
|
||||
class FileSystemError extends Data.TaggedError('FileSystemError')<{
|
||||
readonly operation: string
|
||||
readonly path: string
|
||||
readonly cause: unknown
|
||||
}> {}
|
||||
|
||||
class CopyError extends Data.TaggedError('CopyError')<{
|
||||
readonly source: string
|
||||
readonly target: string
|
||||
readonly cause: unknown
|
||||
}> {}
|
||||
|
||||
// ============================================================================
|
||||
// Services
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 配置服务
|
||||
*/
|
||||
class CopyConfigService extends Context.Tag('CopyConfigService')<
|
||||
CopyConfigService,
|
||||
CopyConfig
|
||||
>() {
|
||||
/**
|
||||
* 从原始数据创建并验证配置
|
||||
*/
|
||||
static fromRaw = (raw: unknown) =>
|
||||
Effect.gen(function* () {
|
||||
const decoded = yield* Schema.decodeUnknown(CopyConfigSchema)(raw)
|
||||
return decoded
|
||||
}).pipe(
|
||||
Effect.catchAll((error) =>
|
||||
Effect.fail(
|
||||
new ConfigError({
|
||||
message: '配置验证失败',
|
||||
cause: error,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
/**
|
||||
* 默认配置 Layer
|
||||
*/
|
||||
static readonly Live = Layer.effect(
|
||||
CopyConfigService,
|
||||
CopyConfigService.fromRaw({
|
||||
sourceDir: path.join(__dirname, '..', 'server', 'out'),
|
||||
targetDir: path.join(__dirname, 'src-tauri', 'binaries'),
|
||||
baseName: 'server',
|
||||
mappings: [
|
||||
{
|
||||
bunSuffix: 'windows-x64',
|
||||
tauriTarget: 'x86_64-pc-windows-msvc',
|
||||
},
|
||||
{
|
||||
bunSuffix: 'darwin-arm64',
|
||||
tauriTarget: 'aarch64-apple-darwin',
|
||||
},
|
||||
{
|
||||
bunSuffix: 'darwin-x64',
|
||||
tauriTarget: 'x86_64-apple-darwin',
|
||||
},
|
||||
{
|
||||
bunSuffix: 'linux-x64',
|
||||
tauriTarget: 'x86_64-unknown-linux-gnu',
|
||||
},
|
||||
{
|
||||
bunSuffix: 'linux-arm64',
|
||||
tauriTarget: 'aarch64-unknown-linux-gnu',
|
||||
},
|
||||
],
|
||||
} satisfies CopyConfig),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件系统服务
|
||||
*/
|
||||
class FileSystemService extends Context.Tag('FileSystemService')<
|
||||
FileSystemService,
|
||||
{
|
||||
readonly ensureDir: (dir: string) => Effect.Effect<void, FileSystemError>
|
||||
readonly fileExists: (
|
||||
filePath: string,
|
||||
) => Effect.Effect<boolean, FileSystemError>
|
||||
readonly copyFile: (
|
||||
source: string,
|
||||
target: string,
|
||||
) => Effect.Effect<void, CopyError>
|
||||
}
|
||||
>() {
|
||||
static readonly Live = Layer.succeed(FileSystemService, {
|
||||
ensureDir: (dir: string) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
await $`mkdir -p ${dir}`
|
||||
},
|
||||
catch: (cause: unknown) =>
|
||||
new FileSystemError({
|
||||
operation: 'ensureDir',
|
||||
path: dir,
|
||||
cause,
|
||||
}),
|
||||
}),
|
||||
|
||||
fileExists: (filePath: string) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
const file = Bun.file(filePath)
|
||||
return await file.exists()
|
||||
},
|
||||
catch: (cause: unknown) =>
|
||||
new FileSystemError({
|
||||
operation: 'fileExists',
|
||||
path: filePath,
|
||||
cause,
|
||||
}),
|
||||
}),
|
||||
|
||||
copyFile: (source: string, target: string) =>
|
||||
Effect.tryPromise({
|
||||
try: async () => {
|
||||
await $`cp ${source} ${target}`
|
||||
},
|
||||
catch: (cause: unknown) =>
|
||||
new CopyError({
|
||||
source,
|
||||
target,
|
||||
cause,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制服务
|
||||
*/
|
||||
class CopyService extends Context.Tag('CopyService')<
|
||||
CopyService,
|
||||
{
|
||||
readonly copyBinary: (
|
||||
config: CopyConfig,
|
||||
mapping: TargetMapping,
|
||||
) => Effect.Effect<CopyResult, CopyError | FileSystemError>
|
||||
readonly copyAllBinaries: (
|
||||
config: CopyConfig,
|
||||
) => Effect.Effect<ReadonlyArray<CopyResult>, CopyError | FileSystemError>
|
||||
}
|
||||
>() {
|
||||
static readonly Live = Layer.effect(
|
||||
CopyService,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystemService
|
||||
|
||||
return {
|
||||
copyBinary: (config: CopyConfig, mapping: TargetMapping) =>
|
||||
Effect.gen(function* () {
|
||||
const { sourceDir, targetDir, baseName } = config
|
||||
const { bunSuffix, tauriTarget } = mapping
|
||||
|
||||
// 确定文件扩展名(Windows 需要 .exe)
|
||||
const ext = tauriTarget.includes('windows') ? '.exe' : ''
|
||||
|
||||
// 构建源文件和目标文件路径
|
||||
const sourceFile = path.join(
|
||||
sourceDir,
|
||||
`${baseName}-${bunSuffix}${ext}`,
|
||||
)
|
||||
const targetFile = path.join(
|
||||
targetDir,
|
||||
`${baseName}-${tauriTarget}${ext}`,
|
||||
)
|
||||
|
||||
// 检查源文件是否存在
|
||||
const exists = yield* fs.fileExists(sourceFile)
|
||||
if (!exists) {
|
||||
yield* Console.log(`⚠️ 跳过 ${bunSuffix}: 源文件不存在`)
|
||||
return {
|
||||
bunSuffix,
|
||||
tauriTarget,
|
||||
sourceFile,
|
||||
targetFile,
|
||||
success: false,
|
||||
} satisfies CopyResult
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
yield* fs.copyFile(sourceFile, targetFile)
|
||||
|
||||
yield* Console.log(`✓ ${bunSuffix} → ${tauriTarget}`)
|
||||
yield* Console.log(` ${sourceFile}`)
|
||||
yield* Console.log(` → ${targetFile}\n`)
|
||||
|
||||
return {
|
||||
bunSuffix,
|
||||
tauriTarget,
|
||||
sourceFile,
|
||||
targetFile,
|
||||
success: true,
|
||||
} satisfies CopyResult
|
||||
}),
|
||||
|
||||
copyAllBinaries: (config: CopyConfig) =>
|
||||
Effect.gen(function* () {
|
||||
const effects = config.mappings.map((mapping) =>
|
||||
Effect.gen(function* () {
|
||||
const { sourceDir, targetDir, baseName } = config
|
||||
const { bunSuffix, tauriTarget } = mapping
|
||||
|
||||
const ext = tauriTarget.includes('windows') ? '.exe' : ''
|
||||
const sourceFile = path.join(
|
||||
sourceDir,
|
||||
`${baseName}-${bunSuffix}${ext}`,
|
||||
)
|
||||
const targetFile = path.join(
|
||||
targetDir,
|
||||
`${baseName}-${tauriTarget}${ext}`,
|
||||
)
|
||||
|
||||
const exists = yield* fs.fileExists(sourceFile)
|
||||
if (!exists) {
|
||||
yield* Console.log(`⚠️ 跳过 ${bunSuffix}: 源文件不存在`)
|
||||
return {
|
||||
bunSuffix,
|
||||
tauriTarget,
|
||||
sourceFile,
|
||||
targetFile,
|
||||
success: false,
|
||||
} satisfies CopyResult
|
||||
}
|
||||
|
||||
yield* fs.copyFile(sourceFile, targetFile)
|
||||
|
||||
yield* Console.log(`✓ ${bunSuffix} → ${tauriTarget}`)
|
||||
yield* Console.log(` ${sourceFile}`)
|
||||
yield* Console.log(` → ${targetFile}\n`)
|
||||
|
||||
return {
|
||||
bunSuffix,
|
||||
tauriTarget,
|
||||
sourceFile,
|
||||
targetFile,
|
||||
success: true,
|
||||
} satisfies CopyResult
|
||||
}),
|
||||
)
|
||||
|
||||
return yield* Effect.all(effects, { concurrency: 'unbounded' })
|
||||
}),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 报告服务
|
||||
*/
|
||||
class ReporterService extends Context.Tag('ReporterService')<
|
||||
ReporterService,
|
||||
{
|
||||
readonly printSummary: (
|
||||
results: ReadonlyArray<CopyResult>,
|
||||
) => Effect.Effect<void>
|
||||
}
|
||||
>() {
|
||||
static readonly Live = Layer.succeed(ReporterService, {
|
||||
printSummary: (results: ReadonlyArray<CopyResult>) =>
|
||||
Effect.gen(function* () {
|
||||
const successful = results.filter((r) => r.success)
|
||||
const failed = results.filter((r) => !r.success)
|
||||
|
||||
yield* Console.log('\n📦 复制摘要:')
|
||||
yield* Console.log(` ✅ 成功: ${successful.length}`)
|
||||
yield* Console.log(` ⚠️ 跳过: ${failed.length}`)
|
||||
|
||||
if (successful.length > 0) {
|
||||
yield* Console.log('\n成功复制的文件:')
|
||||
for (const result of successful) {
|
||||
yield* Console.log(
|
||||
` • ${result.bunSuffix} → ${result.tauriTarget}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
yield* Console.log('\n跳过的文件:')
|
||||
for (const result of failed) {
|
||||
yield* Console.log(` • ${result.bunSuffix} (源文件不存在)`)
|
||||
}
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Program
|
||||
// ============================================================================
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const config = yield* CopyConfigService
|
||||
const fs = yield* FileSystemService
|
||||
const copier = yield* CopyService
|
||||
const reporter = yield* ReporterService
|
||||
|
||||
yield* Console.log('📦 开始复制二进制文件到 Tauri sidecar 目录...\n')
|
||||
|
||||
// 1. 检查源目录
|
||||
const sourceExists = yield* fs.fileExists(config.sourceDir)
|
||||
if (!sourceExists) {
|
||||
yield* Console.error(`❌ 源目录不存在: ${config.sourceDir}`)
|
||||
yield* Console.log(
|
||||
'💡 提示: 请先在 apps/server 中运行 bun run compile 构建服务器二进制文件',
|
||||
)
|
||||
return yield* Effect.fail(
|
||||
new FileSystemError({
|
||||
operation: 'checkSourceDir',
|
||||
path: config.sourceDir,
|
||||
cause: '源目录不存在',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// 2. 创建目标目录
|
||||
yield* fs.ensureDir(config.targetDir)
|
||||
yield* Console.log(`✓ 目标目录: ${config.targetDir}\n`)
|
||||
|
||||
// 3. 并行复制所有二进制文件
|
||||
const results = yield* copier.copyAllBinaries(config)
|
||||
|
||||
// 4. 输出摘要
|
||||
yield* reporter.printSummary(results)
|
||||
|
||||
return results
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Layer Composition
|
||||
// ============================================================================
|
||||
|
||||
const MainLayer = Layer.mergeAll(
|
||||
CopyConfigService.Live,
|
||||
FileSystemService.Live,
|
||||
CopyService.Live.pipe(Layer.provide(FileSystemService.Live)),
|
||||
ReporterService.Live,
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Runner
|
||||
// ============================================================================
|
||||
|
||||
const runnable = program.pipe(
|
||||
Effect.provide(MainLayer),
|
||||
Effect.catchTags({
|
||||
ConfigError: (error) =>
|
||||
Console.error(`❌ 配置错误: ${error.message}`, error.cause),
|
||||
FileSystemError: (error) =>
|
||||
Console.error(
|
||||
`❌ 文件系统错误 [${error.operation}]: ${error.path}`,
|
||||
error.cause,
|
||||
),
|
||||
CopyError: (error) =>
|
||||
Console.error(
|
||||
`❌ 复制失败: ${error.source} → ${error.target}`,
|
||||
error.cause,
|
||||
),
|
||||
}),
|
||||
Effect.tapErrorCause((cause) => Console.error('❌ 未预期的错误:', cause)),
|
||||
)
|
||||
|
||||
Effect.runPromise(runnable).catch(() => {
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -5,11 +5,15 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tauri build",
|
||||
"copy-binaries": "bun copy-binaries.ts",
|
||||
"dev": "tauri dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/schema": "catalog:",
|
||||
"@furtherverse/tsconfig": "workspace:*",
|
||||
"@tauri-apps/cli": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
4
apps/desktop/tsconfig.json
Normal file
4
apps/desktop/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@furtherverse/tsconfig/bun.json",
|
||||
"exclude": ["node_modules", "src-tauri"]
|
||||
}
|
||||
Reference in New Issue
Block a user