140 lines
5.2 KiB
TypeScript
140 lines
5.2 KiB
TypeScript
export * as Watcher from "./watcher"
|
|
|
|
// @ts-ignore
|
|
import { createWrapper } from "@parcel/watcher/wrapper"
|
|
import type ParcelWatcher from "@parcel/watcher"
|
|
import { makeLocationNode } from "../effect/app-node"
|
|
import { Cause, Context, Effect, Layer } from "effect"
|
|
import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
|
|
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 { Ignore } from "./ignore"
|
|
import { Protected } from "./protected"
|
|
|
|
declare const OPENCODE_LIBC: string | undefined
|
|
|
|
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
|
|
|
export const Event = FileSystemWatcher.Event
|
|
|
|
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 {
|
|
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") {}
|
|
|
|
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) {
|
|
yield* Effect.logError("watcher backend not supported", {
|
|
directory: location.directory,
|
|
platform: process.platform,
|
|
})
|
|
return Service.of({})
|
|
}
|
|
|
|
const w = watcher()
|
|
if (!w) return Service.of({})
|
|
|
|
yield* Effect.logInfo("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) => {
|
|
pending.then((subscription) => subscription.unsubscribe()).catch(() => {})
|
|
return Effect.logError("failed to subscribe", { directory, cause: Cause.pretty(cause) })
|
|
}),
|
|
)
|
|
}
|
|
|
|
const config = (yield* (yield* Config.Service).entries())
|
|
.filter((entry): entry is Config.Document => entry.type === "document")
|
|
.flatMap((item) => item.info.watcher?.ignore ?? [])
|
|
if (location.vcs && (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.repo.discover(location.directory))?.gitDirectory
|
|
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) => {
|
|
return Effect.logError("failed to init watcher service", { cause: Cause.pretty(cause) }).pipe(
|
|
Effect.as(Service.of({})),
|
|
)
|
|
}),
|
|
),
|
|
)
|
|
|
|
export const node = makeLocationNode({
|
|
service: Service,
|
|
layer,
|
|
deps: [FSUtil.node, Location.node, Config.node, Git.node, EventV2.node],
|
|
})
|