watcher.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import { BusEvent } from "@/bus/bus-event"
  2. import { Bus } from "@/bus"
  3. import z from "zod"
  4. import { Instance } from "../project/instance"
  5. import { Log } from "../util/log"
  6. import { FileIgnore } from "./ignore"
  7. import { Config } from "../config/config"
  8. import path from "path"
  9. // @ts-ignore
  10. import { createWrapper } from "@parcel/watcher/wrapper"
  11. import { lazy } from "@/util/lazy"
  12. import type ParcelWatcher from "@parcel/watcher"
  13. import { $ } from "bun"
  14. import { Flag } from "@/flag/flag"
  15. declare const OPENCODE_LIBC: string | undefined
  16. export namespace FileWatcher {
  17. const log = Log.create({ service: "file.watcher" })
  18. export const Event = {
  19. Updated: BusEvent.define(
  20. "file.watcher.updated",
  21. z.object({
  22. file: z.string(),
  23. event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]),
  24. }),
  25. ),
  26. }
  27. const watcher = lazy(() => {
  28. const binding = require(
  29. `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`,
  30. )
  31. return createWrapper(binding) as typeof import("@parcel/watcher")
  32. })
  33. const state = Instance.state(
  34. async () => {
  35. if (Instance.project.vcs !== "git") return {}
  36. log.info("init")
  37. const cfg = await Config.get()
  38. const backend = (() => {
  39. if (process.platform === "win32") return "windows"
  40. if (process.platform === "darwin") return "fs-events"
  41. if (process.platform === "linux") return "inotify"
  42. })()
  43. if (!backend) {
  44. log.error("watcher backend not supported", { platform: process.platform })
  45. return {}
  46. }
  47. log.info("watcher backend", { platform: process.platform, backend })
  48. const subscribe: ParcelWatcher.SubscribeCallback = (err, evts) => {
  49. if (err) return
  50. for (const evt of evts) {
  51. if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" })
  52. if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" })
  53. if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" })
  54. }
  55. }
  56. const subs: ParcelWatcher.AsyncSubscription[] = []
  57. const cfgIgnores = cfg.watcher?.ignore ?? []
  58. if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
  59. subs.push(
  60. await watcher().subscribe(Instance.directory, subscribe, {
  61. ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
  62. backend,
  63. }),
  64. )
  65. }
  66. const vcsDir = await $`git rev-parse --git-dir`
  67. .quiet()
  68. .nothrow()
  69. .cwd(Instance.worktree)
  70. .text()
  71. .then((x) => path.resolve(Instance.worktree, x.trim()))
  72. if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
  73. subs.push(
  74. await watcher().subscribe(vcsDir, subscribe, {
  75. ignore: ["hooks", "info", "logs", "objects", "refs", "worktrees", "modules", "lfs"],
  76. backend,
  77. }),
  78. )
  79. }
  80. return { subs }
  81. },
  82. async (state) => {
  83. if (!state.subs) return
  84. await Promise.all(state.subs.map((sub) => sub?.unsubscribe()))
  85. },
  86. )
  87. export function init() {
  88. if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) {
  89. return
  90. }
  91. state()
  92. }
  93. }