| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- export * as Watcher from "./watcher"
- // @ts-ignore
- import { createWrapper } from "@parcel/watcher/wrapper"
- import type ParcelWatcher from "@parcel/watcher"
- import { Cause, Context, Effect, Layer, Schema } from "effect"
- import path from "path"
- import { Config } from "../config"
- import { EventV2 } from "../event"
- import { Flag } from "../flag/flag"
- import { FSUtil } from "../fs-util"
- import { Git } from "../git"
- import { Location } from "../location"
- import { lazy } from "../util/lazy"
- import * as Log from "../util/log"
- import { Ignore } from "./ignore"
- import { Protected } from "./protected"
- declare const OPENCODE_LIBC: string | undefined
- const log = Log.create({ service: "file.watcher" })
- const SUBSCRIBE_TIMEOUT_MS = 10_000
- export const Event = {
- Updated: EventV2.define({
- type: "file.watcher.updated",
- schema: {
- file: Schema.String,
- event: Schema.Literals(["add", "change", "unlink"]),
- },
- }),
- }
- const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
- try {
- const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
- const binding = require(
- `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`,
- )
- return createWrapper(binding) as typeof import("@parcel/watcher")
- } catch (error) {
- log.error("failed to load watcher binding", { error })
- return
- }
- })
- function getBackend() {
- if (process.platform === "win32") return "windows"
- if (process.platform === "darwin") return "fs-events"
- if (process.platform === "linux") return "inotify"
- }
- function protecteds(dir: string) {
- return Protected.paths().filter((item) => {
- const relative = path.relative(dir, item)
- return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
- })
- }
- export const hasNativeBinding = () => !!watcher()
- export interface Interface {}
- export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileWatcher") {}
- export const layer = Layer.effect(
- Service,
- Effect.gen(function* () {
- if (yield* Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) return Service.of({})
- const backend = getBackend()
- const location = yield* Location.Service
- if (!backend) {
- log.error("watcher backend not supported", { directory: location.directory, platform: process.platform })
- return Service.of({})
- }
- const w = watcher()
- if (!w) return Service.of({})
- log.info("watcher backend", { directory: location.directory, platform: process.platform, backend })
- const events = yield* EventV2.Service
- const fs = yield* FSUtil.Service
- const git = yield* Git.Service
- const context = yield* Effect.context()
- const runFork = Effect.runForkWith(context)
- const subscriptions: ParcelWatcher.AsyncSubscription[] = []
- yield* Effect.addFinalizer(() =>
- Effect.promise(() => Promise.allSettled(subscriptions.map((subscription) => subscription.unsubscribe()))),
- )
- const callback: ParcelWatcher.SubscribeCallback = (_error, updates) => {
- for (const update of updates) {
- if (update.type === "create") runFork(events.publish(Event.Updated, { file: update.path, event: "add" }))
- if (update.type === "update") runFork(events.publish(Event.Updated, { file: update.path, event: "change" }))
- if (update.type === "delete") runFork(events.publish(Event.Updated, { file: update.path, event: "unlink" }))
- }
- }
- const subscribe = (directory: string, ignore: string[]) => {
- const pending = w.subscribe(directory, callback, { ignore, backend })
- return Effect.promise(() => pending).pipe(
- Effect.tap((subscription) => Effect.sync(() => subscriptions.push(subscription))),
- Effect.timeout(SUBSCRIBE_TIMEOUT_MS),
- Effect.catchCause((cause) => {
- log.error("failed to subscribe", { directory, cause: Cause.pretty(cause) })
- pending.then((subscription) => subscription.unsubscribe()).catch(() => {})
- return Effect.void
- }),
- )
- }
- const config = (yield* (yield* Config.Service).get()).flatMap((item) => item.info.watcher?.ignore ?? [])
- if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
- yield* Effect.forkScoped(
- subscribe(location.directory, [...Ignore.PATTERNS, ...config, ...protecteds(location.directory)]),
- )
- }
- if (location.vcs?.type === "git") {
- const resolved = yield* git.dir(location.directory)
- const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined
- if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) {
- const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap(
- (entry) => (entry.name === "HEAD" ? [] : [entry.name]),
- )
- yield* Effect.forkScoped(subscribe(vcs, ignore))
- }
- }
- return Service.of({})
- }).pipe(
- Effect.catchCause((cause) => {
- log.error("failed to init watcher service", { cause: Cause.pretty(cause) })
- return Effect.succeed(Service.of({}))
- }),
- ),
- )
- export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer), Layer.provide(Git.defaultLayer))
|