From 46984c268776b18dd7d17801a601ae48e9126ec3 Mon Sep 17 00:00:00 2001 From: imbytecat Date: Wed, 21 Jan 2026 23:38:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E7=AB=AF=E6=9E=84=E5=BB=BA=E9=85=8D=E7=BD=AE=E4=B8=8E?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除桌面端构建配置文件 - 添加二进制文件复制工具,支持多平台目标映射并自动处理文件路径与扩展名,通过类型安全配置和错误处理确保构建流程稳定可靠。 - 添加依赖项以支持类型安全的模式定义和项目配置,并新增脚本用于复制二进制文件。 - 添加桌面应用的TypeScript配置文件并继承统一的tsconfig设置,排除node_modules和src-tauri目录。 - 添加必要的开发依赖项以支持类型检查和构建工具链 --- apps/desktop/build.ts | 0 apps/desktop/copy-binaries.ts | 452 ++++++++++++++++++++++++++++++++++ apps/desktop/package.json | 4 + apps/desktop/tsconfig.json | 4 + bun.lock | 5 + 5 files changed, 465 insertions(+) delete mode 100644 apps/desktop/build.ts create mode 100644 apps/desktop/copy-binaries.ts create mode 100644 apps/desktop/tsconfig.json diff --git a/apps/desktop/build.ts b/apps/desktop/build.ts deleted file mode 100644 index e69de29..0000000 diff --git a/apps/desktop/copy-binaries.ts b/apps/desktop/copy-binaries.ts new file mode 100644 index 0000000..f737b7a --- /dev/null +++ b/apps/desktop/copy-binaries.ts @@ -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 + +/** + * 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 + +/** + * 目标映射配置 + */ +const TargetMappingSchema = Schema.Struct({ + bunSuffix: BunTargetSuffixSchema, + tauriTarget: TauriTargetSchema, +}) + +type TargetMapping = Schema.Schema.Type + +/** + * 复制配置 + */ +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 + +/** + * 复制结果 + */ +const CopyResultSchema = Schema.Struct({ + bunSuffix: BunTargetSuffixSchema, + tauriTarget: TauriTargetSchema, + sourceFile: Schema.String, + targetFile: Schema.String, + success: Schema.Boolean, +}) + +type CopyResult = Schema.Schema.Type + +// ============================================================================ +// 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 + readonly fileExists: ( + filePath: string, + ) => Effect.Effect + readonly copyFile: ( + source: string, + target: string, + ) => Effect.Effect + } +>() { + 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 + readonly copyAllBinaries: ( + config: CopyConfig, + ) => Effect.Effect, 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, + ) => Effect.Effect + } +>() { + static readonly Live = Layer.succeed(ReporterService, { + printSummary: (results: ReadonlyArray) => + 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) +}) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index bfc3066..3713e68 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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:" } } diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json new file mode 100644 index 0000000..28138db --- /dev/null +++ b/apps/desktop/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@furtherverse/tsconfig/bun.json", + "exclude": ["node_modules", "src-tauri"] +} diff --git a/bun.lock b/bun.lock index c462785..427b55e 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,12 @@ "name": "@furtherverse/desktop", "version": "1.0.0", "devDependencies": { + "@effect/schema": "catalog:", + "@furtherverse/tsconfig": "workspace:*", "@tauri-apps/cli": "catalog:", + "@types/bun": "catalog:", + "effect": "catalog:", + "typescript": "catalog:", }, }, "apps/server": {