/** * 跨平台构建脚本 * * 使用 Effect 框架实现类型安全的多目标编译。 * 将 TanStack Start 的 SSR 服务器打包为独立可执行文件, * 用于 Tauri 的 sidecar 机制。 * * 支持目标平台: * - Windows (x64) * - macOS (ARM64/x64) * - Linux (x64/ARM64) * * 用法: bun run build:compile */ import { Schema } from '@effect/schema' import { $ } from 'bun' import { Console, Context, Data, Effect, Layer } from 'effect' // ============================================================================ // 项目配置 // ============================================================================ /** 项目名称 - 用于生成 sidecar 文件名 */ const PROJECT_NAME = 'openbridgeTokenUsageViewerServer' // ============================================================================ // 领域模型和 Schema 定义 // ============================================================================ /** * Bun 目标平台到 Rust 目标三元组的映射 * 用于生成 Tauri sidecar 所需的文件名格式 */ const targetMap = { 'bun-windows-x64': 'x86_64-pc-windows-msvc', 'bun-darwin-arm64': 'aarch64-apple-darwin', 'bun-darwin-x64': 'x86_64-apple-darwin', 'bun-linux-x64': 'x86_64-unknown-linux-gnu', 'bun-linux-arm64': 'aarch64-unknown-linux-gnu', } as const /** Bun 编译目标 Schema */ const BunTargetSchema = Schema.Literal( 'bun-windows-x64', 'bun-darwin-arm64', 'bun-darwin-x64', 'bun-linux-x64', 'bun-linux-arm64', ) /** Bun 编译目标类型 */ type BunTarget = Schema.Schema.Type /** 构建配置 Schema */ const BuildConfigSchema = Schema.Struct({ /** 入口文件路径 */ entrypoint: Schema.String.pipe(Schema.nonEmptyString()), /** 输出目录 */ outputDir: Schema.String.pipe(Schema.nonEmptyString()), /** 目标平台列表 */ targets: Schema.Array(BunTargetSchema).pipe(Schema.minItems(1)), }) /** 构建配置类型 */ type BuildConfig = Schema.Schema.Type /** 构建结果 Schema */ const BuildResultSchema = Schema.Struct({ /** 编译目标 */ target: BunTargetSchema, /** 输出文件路径列表 */ outputs: Schema.Array(Schema.String), }) /** 构建结果类型 */ type BuildResult = Schema.Schema.Type // ============================================================================ // 错误模型 (使用 Effect Data.TaggedError) // ============================================================================ /** 清理目录错误 */ class CleanError extends Data.TaggedError('CleanError')<{ readonly dir: string readonly cause: unknown }> {} /** 构建错误 */ class BuildError extends Data.TaggedError('BuildError')<{ readonly target: BunTarget readonly cause: unknown }> {} /** 配置验证错误 */ class ConfigError extends Data.TaggedError('ConfigError')<{ readonly message: string readonly cause: unknown }> {} // ============================================================================ // 服务层 // ============================================================================ /** * 构建配置服务 * * 提供类型安全的配置验证和默认配置。 */ class BuildConfigService extends Context.Tag('BuildConfigService')< BuildConfigService, BuildConfig >() { /** * 从原始数据创建并验证配置 * * @param raw - 原始配置对象 * @returns 验证后的配置或配置错误 */ static fromRaw = (raw: unknown) => Effect.gen(function* () { const decoded = yield* Schema.decodeUnknown(BuildConfigSchema)(raw) return decoded }).pipe( Effect.catchAll((error) => Effect.fail( new ConfigError({ message: '配置验证失败', cause: error, }), ), ), ) /** * 默认配置 Layer * * 输出到 src-tauri/binaries 目录,供 Tauri sidecar 使用 */ static readonly Live = Layer.effect( BuildConfigService, BuildConfigService.fromRaw({ entrypoint: '.output/server/index.mjs', outputDir: 'src-tauri/binaries', targets: ['bun-windows-x64', 'bun-darwin-arm64', 'bun-linux-x64'], }), ) } /** * 文件系统服务 * * 提供目录清理等文件操作。 */ class FileSystemService extends Context.Tag('FileSystemService')< FileSystemService, { /** 清理指定目录 */ readonly cleanDir: (dir: string) => Effect.Effect } >() { static readonly Live = Layer.succeed(FileSystemService, { cleanDir: (dir: string) => Effect.tryPromise({ try: async () => { await $`rm -rf ${dir}` }, catch: (cause: unknown) => new CleanError({ dir, cause, }), }), }) } /** * 构建服务 * * 使用 Bun.build 进行跨平台编译。 */ class BuildService extends Context.Tag('BuildService')< BuildService, { /** 为单个目标编译 */ readonly buildForTarget: ( config: BuildConfig, target: BunTarget, ) => Effect.Effect /** 并行编译所有目标 */ readonly buildAll: ( config: BuildConfig, ) => Effect.Effect, BuildError> } >() { static readonly Live = Layer.succeed(BuildService, { buildForTarget: (config: BuildConfig, target: BunTarget) => Effect.gen(function* () { yield* Console.log(`🔨 开始构建: ${target}`) const output = yield* Effect.tryPromise({ try: () => Bun.build({ entrypoints: [config.entrypoint], compile: { outfile: `${PROJECT_NAME}-${targetMap[target]}`, target: target, }, outdir: config.outputDir, }), catch: (cause: unknown) => new BuildError({ target, cause, }), }) const paths = output.outputs.map((item: { path: string }) => item.path) return { target, outputs: paths, } satisfies BuildResult }), buildAll: (config: BuildConfig) => Effect.gen(function* () { // 为每个目标创建编译任务 const effects = config.targets.map((target) => Effect.gen(function* () { yield* Console.log(`🔨 开始构建: ${target}`) const output = yield* Effect.tryPromise({ try: () => Bun.build({ entrypoints: [config.entrypoint], compile: { outfile: `${PROJECT_NAME}-${targetMap[target]}`, target: target, }, outdir: config.outputDir, }), catch: (cause: unknown) => new BuildError({ target, cause, }), }) const paths = output.outputs.map( (item: { path: string }) => item.path, ) return { target, outputs: paths, } satisfies BuildResult }), ) // 并行执行所有编译任务 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* () { yield* Console.log('\n📦 构建完成:') for (const result of results) { yield* Console.log(` ${result.target}:`) for (const path of result.outputs) { yield* Console.log(` - ${path}`) } } }), }) } // ============================================================================ // 主程序 // ============================================================================ /** * 构建流程主程序 * * 步骤: * 1. 清理输出目录 * 2. 并行构建所有目标平台 * 3. 输出构建摘要 */ const program = Effect.gen(function* () { const config = yield* BuildConfigService const fs = yield* FileSystemService const builder = yield* BuildService const reporter = yield* ReporterService // 1. 清理输出目录 yield* fs.cleanDir(config.outputDir) yield* Console.log(`✓ 已清理输出目录: ${config.outputDir}`) // 2. 并行构建所有目标 const results = yield* builder.buildAll(config) // 3. 输出构建摘要 yield* reporter.printSummary(results) return results }) // ============================================================================ // Layer 组合 // ============================================================================ /** 合并所有服务 Layer */ const MainLayer = Layer.mergeAll( BuildConfigService.Live, FileSystemService.Live, BuildService.Live, ReporterService.Live, ) // ============================================================================ // 执行入口 // ============================================================================ /** 可运行的程序(附带错误处理) */ const runnable = program.pipe( Effect.provide(MainLayer), Effect.catchTags({ CleanError: (error) => Console.error(`❌ 清理目录失败: ${error.dir}`, error.cause), BuildError: (error) => Console.error(`❌ 构建失败 [${error.target}]:`, error.cause), ConfigError: (error) => Console.error(`❌ 配置错误: ${error.message}`, error.cause), }), Effect.tapErrorCause((cause) => Console.error('❌ 未预期的错误:', cause)), ) // 执行程序 Effect.runPromise(runnable).catch(() => { process.exit(1) })