import { Schema } from '@effect/schema' import { $ } from 'bun' import { Console, Context, Data, Effect, Layer } from 'effect' // ============================================================================ // Domain Models & Schema // ============================================================================ 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 const BunTargetSchema = Schema.Literal( 'bun-windows-x64', 'bun-darwin-arm64', 'bun-darwin-x64', 'bun-linux-x64', 'bun-linux-arm64', ) type BunTarget = Schema.Schema.Type 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 const BuildResultSchema = Schema.Struct({ target: BunTargetSchema, outputs: Schema.Array(Schema.String), }) type BuildResult = Schema.Schema.Type // ============================================================================ // Error Models (使用 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 }> {} // ============================================================================ // Services // ============================================================================ /** * 配置服务 */ class BuildConfigService extends Context.Tag('BuildConfigService')< BuildConfigService, BuildConfig >() { /** * 从原始数据创建并验证配置 */ 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 */ static readonly Live = Layer.effect( BuildConfigService, BuildConfigService.fromRaw({ entrypoint: './.output/server/index.mjs', outputDir: './out', 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, }), }), }) } /** * 构建服务 */ 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: `server-${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: `server-${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}`) } } }), }) } // ============================================================================ // Main Program // ============================================================================ 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 Composition // ============================================================================ const MainLayer = Layer.mergeAll( BuildConfigService.Live, FileSystemService.Live, BuildService.Live, ReporterService.Live, ) // ============================================================================ // Runner // ============================================================================ 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) })