Files
fullstack-starter/apps/desktop/copy-binaries.ts
imbytecat ece366c4d7 refactor: 移除未使用的类型定义,清理冗余代码
- 移除未使用的类型定义,清理冗余代码。
2026-01-21 23:38:31 +08:00

449 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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',
)
/**
* 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',
)
/**
* 目标映射配置
*/
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)
})