refactor: 重构桌面端构建配置与依赖管理

- 删除桌面端构建配置文件
- 添加二进制文件复制工具,支持多平台目标映射并自动处理文件路径与扩展名,通过类型安全配置和错误处理确保构建流程稳定可靠。
- 添加依赖项以支持类型安全的模式定义和项目配置,并新增脚本用于复制二进制文件。
- 添加桌面应用的TypeScript配置文件并继承统一的tsconfig设置,排除node_modules和src-tauri目录。
- 添加必要的开发依赖项以支持类型检查和构建工具链
This commit is contained in:
2026-01-21 23:38:16 +08:00
parent 712ed1919f
commit 46984c2687
5 changed files with 465 additions and 0 deletions

View 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)
})