Kaynağa Gözat

fix(core): bound recursive file watching (#35323)

Kit Langton 1 ay önce
ebeveyn
işleme
945d1c8cb2

+ 1 - 1
packages/core/src/filesystem/ignore.ts

@@ -45,7 +45,7 @@ const FILES = [
   "**/.nyc_output/**",
 ]
 
-export const PATTERNS = [...FILES, ...FOLDERS]
+export const PATTERNS = [...FILES, ...FOLDERS, `**/{${Array.from(FOLDERS).join(",")}}/**`]
 
 export function match(filepath: string, opts?: { extra?: string[]; whitelist?: string[] }) {
   for (const pattern of opts?.whitelist || []) {

+ 29 - 0
packages/core/test/filesystem/ignore.test.ts

@@ -1,5 +1,7 @@
 import { expect, test } from "bun:test"
 import { Ignore } from "@opencode-ai/core/filesystem/ignore"
+// @ts-ignore
+import { createWrapper } from "@parcel/watcher/wrapper"
 
 test("match nested and non-nested", () => {
   expect(Ignore.match("node_modules/index.js")).toBe(true)
@@ -8,3 +10,30 @@ test("match nested and non-nested", () => {
   expect(Ignore.match("node_modules/bar")).toBe(true)
   expect(Ignore.match("node_modules/bar/")).toBe(true)
 })
+
+test("parcel patterns ignore built-in folders at any depth", async () => {
+  let ignoreGlobs: string[] = []
+  const watcher = createWrapper({
+    subscribe: async (
+      _directory: string,
+      _callback: (...args: unknown[]) => unknown,
+      options: { ignoreGlobs?: string[] },
+    ) => {
+      ignoreGlobs = options.ignoreGlobs ?? []
+    },
+  })
+  await watcher.subscribe("/tmp/project", () => {}, { ignore: Ignore.PATTERNS })
+  const patterns = ignoreGlobs.map((source) => new RegExp(source))
+
+  for (const path of [
+    "nested/node_modules",
+    "nested/node_modules/package/index.js",
+    "nested/.git",
+    "nested/.git/HEAD",
+    "nested/dist",
+    "nested/dist/index.js",
+  ]) {
+    expect(patterns.some((pattern) => pattern.test(path))).toBe(true)
+  }
+  expect(patterns.some((pattern) => pattern.test("nested/src/index.ts"))).toBe(false)
+})

+ 25 - 5
packages/core/test/filesystem/watcher.test.ts

@@ -2,7 +2,7 @@ import { $ } from "bun"
 import { describe, expect } from "bun:test"
 import fs from "fs/promises"
 import path from "path"
-import { Deferred, Duration, Effect, Fiber, Layer, Option, Stream } from "effect"
+import { Deferred, Duration, Effect, Fiber, Layer, Option, Schedule, Stream } from "effect"
 import { Config } from "@opencode-ai/core/config"
 import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
@@ -149,12 +149,14 @@ describeWatcher("LocationWatcher", () => {
         const update = yield* watcher
           .subscribe({ path: target, type: "file" })
           .pipe(Stream.take(1), Stream.runHead, Effect.forkScoped({ startImmediately: true }))
-        yield* Effect.yieldNow
-
         yield* fs.writeFileString(sibling, "sibling")
-        yield* fs.writeFileString(target, "target")
+        const writes = yield* Effect.suspend(() => fs.writeFileString(target, `target-${Math.random()}`)).pipe(
+          Effect.repeat(Schedule.spaced("10 millis")),
+          Effect.forkScoped,
+        )
+        const event = yield* Fiber.join(update).pipe(Effect.ensuring(Fiber.interrupt(writes)))
 
-        expect((yield* Fiber.join(update)).valueOrUndefined?.path).toBe(target)
+        expect(event.valueOrUndefined?.path).toBe(target)
       }).pipe(Effect.provide(AppNodeBuilder.build(Watcher.node))),
     ),
   )
@@ -197,6 +199,24 @@ describeWatcher("LocationWatcher", () => {
     ),
   )
 
+  it.live("ignores dependency, VCS, and build directories at any depth", () =>
+    withTmp((directory) =>
+      Effect.gen(function* () {
+        const afs = yield* FSUtil.Service
+        yield* ready(directory)
+        const roots = ["node_modules", ".git", "dist"].map((name) => path.join(directory, "nested", name))
+        const files = roots.map((root) => path.join(root, "package", "index.js"))
+        yield* noUpdate(
+          (event) => roots.some((root) => event.file === root || event.file.startsWith(`${root}${path.sep}`)),
+          Effect.forEach(files, (file) => afs.writeWithDirs(file, "ignored"), {
+            concurrency: "unbounded",
+            discard: true,
+          }),
+        )
+      }),
+    ),
+  )
+
   it.live("cleanup stops publishing events", () =>
     Effect.gen(function* () {
       const events = yield* EventV2.Service

+ 2 - 2
packages/web/src/content/docs/config.mdx

@@ -749,12 +749,12 @@ You can configure file watcher ignore patterns through the `watcher` option.
 {
   "$schema": "https://opencode.ai/config.json",
   "watcher": {
-    "ignore": ["node_modules/**", "dist/**", ".git/**"]
+    "ignore": ["**/generated/**"]
   }
 }
 ```
 
-Patterns follow glob syntax. Use this to exclude noisy directories from file watching.
+Patterns follow glob syntax. Common dependency, VCS, build, and cache directories are ignored automatically at any depth.
 
 ---