1/**
2 * Post-processing service for linting and formatting generated code.
3 *
4 * @since 4.0.0
5 */
6import * as Context from "effect/Context"
7import * as Data from "effect/Data"
8import * as Effect from "effect/Effect"
9import * as Layer from "effect/Layer"
10import * as ChildProcess from "effect/process/ChildProcess"
11import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"
12import * as Stream from "effect/Stream"
13
14/**
15 * Error during post-processing (lint or format).
16 *
17 * **Example** (Creating a post-process error)
18 *
19 * ```ts import.meta.vitest
20 * import * as PostProcess from "@effect/ai-codegen/PostProcess"
21 *
22 * const error = new PostProcess.PostProcessError({
23 * step: "lint",
24 * command: "pnpm exec oxlint --fix /path/to/file.ts",
25 * filePath: "/path/to/file.ts",
26 * exitCode: 1,
27 * stdout: "",
28 * stderr: "error: some lint error",
29 * cause: new Error("Lint failed")
30 * })
31 *
32 * error.step // => "lint"
33 * error.exitCode // => 1
34 * ```
35 *
36 * @category errors
37 * @since 4.0.0
38 */
39export class PostProcessError extends Data.TaggedError("PostProcessError")<{
40 readonly step: "lint" | "format"
41 readonly command: string
42 readonly filePath: string
43 readonly exitCode?: number | undefined
44 readonly stdout: string
45 readonly stderr: string
46 readonly cause: unknown
47}> {
48 override get message(): string {
49 const lines: Array<string> = [
50 `${this.step} failed for ${this.filePath}`,
51 `command: ${this.command}`
52 ]
53 if (this.exitCode !== undefined) {
54 lines.push(`exit code: ${this.exitCode}`)
55 }
56 if (this.stderr.length > 0) {
57 lines.push(`stderr:\n${this.stderr}`)
58 }
59 if (this.stdout.length > 0) {
60 lines.push(`stdout:\n${this.stdout}`)
61 }
62 return lines.join("\n")
63 }
64}
65
66/**
67 * Service for post-processing generated code.
68 *
69 * @category services
70 * @since 4.0.0
71 */
72export interface PostProcessor {
73 readonly lint: (filePath: string) => Effect.Effect<void, PostProcessError>
74 readonly format: (filePath: string) => Effect.Effect<void, PostProcessError>
75}
76
77/**
78 * Service tag for linting and formatting generated code.
79 *
80 * @category services
81 * @since 4.0.0
82 */
83export const PostProcessor: Context.Service<PostProcessor, PostProcessor> = Context.Service(
84 "@effect/ai-codegen/PostProcessor"
85)
86
87/**
88 * Layer providing the PostProcessor service.
89 *
90 * @category layers
91 * @since 4.0.0
92 */
93export const layer: Layer.Layer<
94 PostProcessor,
95 never,
96 ChildProcessSpawner.ChildProcessSpawner
97> = Effect.gen(function*() {
98 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
99
100 const collectStream = (stream: Stream.Stream<Uint8Array, any>) =>
101 stream.pipe(
102 Stream.decodeText(),
103 Stream.mkString,
104 Effect.orElseSucceed(() => "")
105 )
106
107 const runCommand = Effect.fn("runCommand")(function*(
108 command: string,
109 args: ReadonlyArray<string>,
110 step: "lint" | "format",
111 filePath: string
112 ) {
113 const fullCommand = [command, ...args].join(" ")
114 const cmd = ChildProcess.make(command, args, {
115 stdout: "pipe",
116 stderr: "pipe"
117 })
118
119 yield* Effect.scoped(Effect.gen(function*() {
120 const handle = yield* spawner.spawn(cmd).pipe(
121 Effect.mapError((cause) =>
122 new PostProcessError({ step, command: fullCommand, filePath, stdout: "", stderr: "", cause })
123 )
124 )
125
126 const [stdout, stderr] = yield* Effect.all([
127 collectStream(handle.stdout),
128 collectStream(handle.stderr)
129 ])
130
131 const exitCode = yield* handle.exitCode.pipe(
132 Effect.mapError((cause) =>
133 new PostProcessError({ step, command: fullCommand, filePath, stdout, stderr, cause })
134 )
135 )
136
137 if (exitCode !== 0) {
138 return yield* new PostProcessError({
139 step,
140 command: fullCommand,
141 filePath,
142 exitCode,
143 stdout,
144 stderr,
145 cause: new Error(`Command exited with code ${exitCode}`)
146 })
147 }
148 }))
149 })
150
151 const lint = Effect.fn("lint")(function*(filePath: string) {
152 yield* runCommand("pnpm", ["exec", "oxlint", "--silent", "--quiet", "--fix", filePath], "lint", filePath)
153 })
154
155 const format = Effect.fn("format")(function*(filePath: string) {
156 yield* runCommand("pnpm", ["exec", "dprint", "--log-level", "silent", "fmt", filePath], "format", filePath)
157 })
158
159 return { lint, format }
160}).pipe(Layer.effect(PostProcessor))