Skip to content

Commit 1045a43

Browse files
authored
refactor: collapse format barrel into format/index.ts (#22898)
1 parent 26af77c commit 1045a43

2 files changed

Lines changed: 194 additions & 193 deletions

File tree

packages/opencode/src/format/format.ts

Lines changed: 0 additions & 192 deletions
This file was deleted.
Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,194 @@
1-
export * as Format from "./format"
1+
import { Effect, Layer, Context } from "effect"
2+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
3+
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
4+
import { InstanceState } from "@/effect"
5+
import path from "path"
6+
import { mergeDeep } from "remeda"
7+
import z from "zod"
8+
import { Config } from "../config"
9+
import { Log } from "../util"
10+
import * as Formatter from "./formatter"
11+
12+
const log = Log.create({ service: "format" })
13+
14+
export const Status = z
15+
.object({
16+
name: z.string(),
17+
extensions: z.string().array(),
18+
enabled: z.boolean(),
19+
})
20+
.meta({
21+
ref: "FormatterStatus",
22+
})
23+
export type Status = z.infer<typeof Status>
24+
25+
export interface Interface {
26+
readonly init: () => Effect.Effect<void>
27+
readonly status: () => Effect.Effect<Status[]>
28+
readonly file: (filepath: string) => Effect.Effect<void>
29+
}
30+
31+
export class Service extends Context.Service<Service, Interface>()("@opencode/Format") {}
32+
33+
export const layer = Layer.effect(
34+
Service,
35+
Effect.gen(function* () {
36+
const config = yield* Config.Service
37+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
38+
39+
const state = yield* InstanceState.make(
40+
Effect.fn("Format.state")(function* (_ctx) {
41+
const commands: Record<string, string[] | false> = {}
42+
const formatters: Record<string, Formatter.Info> = {}
43+
44+
const cfg = yield* config.get()
45+
46+
if (cfg.formatter !== false) {
47+
for (const item of Object.values(Formatter)) {
48+
formatters[item.name] = item
49+
}
50+
for (const [name, item] of Object.entries(cfg.formatter ?? {})) {
51+
// Ruff and uv are both the same formatter, so disabling either should disable both.
52+
if (["ruff", "uv"].includes(name) && (cfg.formatter?.ruff?.disabled || cfg.formatter?.uv?.disabled)) {
53+
// TODO combine formatters so shared backends like Ruff/uv don't need linked disable handling here.
54+
delete formatters.ruff
55+
delete formatters.uv
56+
continue
57+
}
58+
if (item.disabled) {
59+
delete formatters[name]
60+
continue
61+
}
62+
const info = mergeDeep(formatters[name] ?? {}, {
63+
extensions: [],
64+
...item,
65+
})
66+
67+
formatters[name] = {
68+
...info,
69+
name,
70+
enabled: async () => info.command ?? false,
71+
}
72+
}
73+
} else {
74+
log.info("all formatters are disabled")
75+
}
76+
77+
async function getCommand(item: Formatter.Info) {
78+
let cmd = commands[item.name]
79+
if (cmd === false || cmd === undefined) {
80+
cmd = await item.enabled()
81+
commands[item.name] = cmd
82+
}
83+
return cmd
84+
}
85+
86+
async function isEnabled(item: Formatter.Info) {
87+
const cmd = await getCommand(item)
88+
return cmd !== false
89+
}
90+
91+
async function getFormatter(ext: string) {
92+
const matching = Object.values(formatters).filter((item) => item.extensions.includes(ext))
93+
const checks = await Promise.all(
94+
matching.map(async (item) => {
95+
log.info("checking", { name: item.name, ext })
96+
const cmd = await getCommand(item)
97+
if (cmd) {
98+
log.info("enabled", { name: item.name, ext })
99+
}
100+
return {
101+
item,
102+
cmd,
103+
}
104+
}),
105+
)
106+
return checks.filter((x) => x.cmd).map((x) => ({ item: x.item, cmd: x.cmd! }))
107+
}
108+
109+
function formatFile(filepath: string) {
110+
return Effect.gen(function* () {
111+
log.info("formatting", { file: filepath })
112+
const ext = path.extname(filepath)
113+
114+
for (const { item, cmd } of yield* Effect.promise(() => getFormatter(ext))) {
115+
if (cmd === false) continue
116+
log.info("running", { command: cmd })
117+
const replaced = cmd.map((x) => x.replace("$FILE", filepath))
118+
const dir = yield* InstanceState.directory
119+
const code = yield* spawner
120+
.spawn(
121+
ChildProcess.make(replaced[0]!, replaced.slice(1), {
122+
cwd: dir,
123+
env: item.environment,
124+
extendEnv: true,
125+
}),
126+
)
127+
.pipe(
128+
Effect.flatMap((handle) => handle.exitCode),
129+
Effect.scoped,
130+
Effect.catch(() =>
131+
Effect.sync(() => {
132+
log.error("failed to format file", {
133+
error: "spawn failed",
134+
command: cmd,
135+
...item.environment,
136+
file: filepath,
137+
})
138+
return ChildProcessSpawner.ExitCode(1)
139+
}),
140+
),
141+
)
142+
if (code !== 0) {
143+
log.error("failed", {
144+
command: cmd,
145+
...item.environment,
146+
})
147+
}
148+
}
149+
})
150+
}
151+
152+
log.info("init")
153+
154+
return {
155+
formatters,
156+
isEnabled,
157+
formatFile,
158+
}
159+
}),
160+
)
161+
162+
const init = Effect.fn("Format.init")(function* () {
163+
yield* InstanceState.get(state)
164+
})
165+
166+
const status = Effect.fn("Format.status")(function* () {
167+
const { formatters, isEnabled } = yield* InstanceState.get(state)
168+
const result: Status[] = []
169+
for (const formatter of Object.values(formatters)) {
170+
const isOn = yield* Effect.promise(() => isEnabled(formatter))
171+
result.push({
172+
name: formatter.name,
173+
extensions: formatter.extensions,
174+
enabled: isOn,
175+
})
176+
}
177+
return result
178+
})
179+
180+
const file = Effect.fn("Format.file")(function* (filepath: string) {
181+
const { formatFile } = yield* InstanceState.get(state)
182+
yield* formatFile(filepath)
183+
})
184+
185+
return Service.of({ init, status, file })
186+
}),
187+
)
188+
189+
export const defaultLayer = layer.pipe(
190+
Layer.provide(Config.defaultLayer),
191+
Layer.provide(CrossSpawnSpawner.defaultLayer),
192+
)
193+
194+
export * as Format from "."

0 commit comments

Comments
 (0)