index.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { Bus } from "../bus"
  2. import { File } from "../file"
  3. import { Log } from "../util/log"
  4. import path from "path"
  5. import z from "zod"
  6. import * as Formatter from "./formatter"
  7. import { Config } from "../config/config"
  8. import { mergeDeep } from "remeda"
  9. import { Instance } from "../project/instance"
  10. export namespace Format {
  11. const log = Log.create({ service: "format" })
  12. export const Status = z
  13. .object({
  14. name: z.string(),
  15. extensions: z.string().array(),
  16. enabled: z.boolean(),
  17. })
  18. .meta({
  19. ref: "FormatterStatus",
  20. })
  21. export type Status = z.infer<typeof Status>
  22. const state = Instance.state(async () => {
  23. const enabled: Record<string, boolean> = {}
  24. const cfg = await Config.get()
  25. const formatters: Record<string, Formatter.Info> = {}
  26. if (cfg.formatter === false) {
  27. log.info("all formatters are disabled")
  28. return {
  29. enabled,
  30. formatters,
  31. }
  32. }
  33. for (const item of Object.values(Formatter)) {
  34. formatters[item.name] = item
  35. }
  36. for (const [name, item] of Object.entries(cfg.formatter ?? {})) {
  37. if (item.disabled) {
  38. delete formatters[name]
  39. continue
  40. }
  41. const result: Formatter.Info = mergeDeep(formatters[name] ?? {}, {
  42. command: [],
  43. extensions: [],
  44. ...item,
  45. })
  46. if (result.command.length === 0) continue
  47. result.enabled = async () => true
  48. result.name = name
  49. formatters[name] = result
  50. }
  51. return {
  52. enabled,
  53. formatters,
  54. }
  55. })
  56. async function isEnabled(item: Formatter.Info) {
  57. const s = await state()
  58. let status = s.enabled[item.name]
  59. if (status === undefined) {
  60. status = await item.enabled()
  61. s.enabled[item.name] = status
  62. }
  63. return status
  64. }
  65. async function getFormatter(ext: string) {
  66. const formatters = await state().then((x) => x.formatters)
  67. const result = []
  68. for (const item of Object.values(formatters)) {
  69. log.info("checking", { name: item.name, ext })
  70. if (!item.extensions.includes(ext)) continue
  71. if (!(await isEnabled(item))) continue
  72. log.info("enabled", { name: item.name, ext })
  73. result.push(item)
  74. }
  75. return result
  76. }
  77. export async function status() {
  78. const s = await state()
  79. const result: Status[] = []
  80. for (const formatter of Object.values(s.formatters)) {
  81. const enabled = await isEnabled(formatter)
  82. result.push({
  83. name: formatter.name,
  84. extensions: formatter.extensions,
  85. enabled,
  86. })
  87. }
  88. return result
  89. }
  90. export function init() {
  91. log.info("init")
  92. Bus.subscribe(File.Event.Edited, async (payload) => {
  93. const file = payload.properties.file
  94. log.info("formatting", { file })
  95. const ext = path.extname(file)
  96. for (const item of await getFormatter(ext)) {
  97. log.info("running", { command: item.command })
  98. try {
  99. const proc = Bun.spawn({
  100. cmd: item.command.map((x) => x.replace("$FILE", file)),
  101. cwd: Instance.directory,
  102. env: { ...process.env, ...item.environment },
  103. stdout: "ignore",
  104. stderr: "ignore",
  105. })
  106. const exit = await proc.exited
  107. if (exit !== 0)
  108. log.error("failed", {
  109. command: item.command,
  110. ...item.environment,
  111. })
  112. } catch (error) {
  113. log.error("failed to format file", {
  114. error,
  115. command: item.command,
  116. ...item.environment,
  117. file,
  118. })
  119. }
  120. }
  121. })
  122. }
  123. }