From 4c55066700d98538c01f3d912da4f1848bbcdc19 Mon Sep 17 00:00:00 2001 From: Bingran You Date: Mon, 20 Apr 2026 12:56:05 -0700 Subject: [PATCH 1/2] perf(tree): speed up and harden workspace repo discovery - readdirSync uses withFileTypes to drop the per-entry statSync round-trip - skip symlinks explicitly to prevent recursion cycles through shared dirs - expand IGNORED_DIRS (target, vendor, __pycache__, .gradle, .idea, .vscode, coverage, out, .cache, .pytest_cache) so large monorepos prune more aggressively and avoid misclassifying vendored folders - normalize the TREE_SUBMODULES_DIR prefix to forward-slash so the .gitmodules filter works on Windows where join() yields backslashes - warn when a .gitmodules entry points at an uninitialized submodule instead of silently dropping it - pin localeCompare to the "en" locale so sort order is stable across CI runners with differing system locales --- src/products/tree/engine/workspace.ts | 39 ++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/products/tree/engine/workspace.ts b/src/products/tree/engine/workspace.ts index c52c518e..57d797c0 100644 --- a/src/products/tree/engine/workspace.ts +++ b/src/products/tree/engine/workspace.ts @@ -1,4 +1,4 @@ -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { existsSync, readdirSync, readFileSync, type Dirent } from "node:fs"; import { join, relative, resolve } from "node:path"; import { Repo } from "#products/tree/engine/repo.js"; import { TREE_SUBMODULES_DIR } from "#products/tree/engine/runtime/asset-loader.js"; @@ -22,15 +22,19 @@ const IGNORED_DIRS = new Set([ "node_modules", ".next", ".turbo", + "target", + "vendor", + "__pycache__", + ".gradle", + ".idea", + ".vscode", + "coverage", + "out", + ".cache", + ".pytest_cache", ]); -function isDirectory(path: string): boolean { - try { - return statSync(path).isDirectory(); - } catch { - return false; - } -} +const TREE_SUBMODULES_PREFIX = `${TREE_SUBMODULES_DIR.split(/[\\/]/).join("/")}/`; function parseGitmodules(root: string): string[] { try { @@ -39,7 +43,7 @@ function parseGitmodules(root: string): string[] { .map((match) => match[1]?.trim()) .filter( (value): value is string => - Boolean(value) && !value.startsWith(`${TREE_SUBMODULES_DIR}/`), + Boolean(value) && !value.startsWith(TREE_SUBMODULES_PREFIX), ); } catch { return []; @@ -51,21 +55,23 @@ function discoverNestedRepos( current: string, results: Map, ): void { - let entries: string[] = []; + let entries: Dirent[] = []; try { - entries = readdirSync(current); + entries = readdirSync(current, { withFileTypes: true }); } catch { return; } for (const entry of entries) { - if (IGNORED_DIRS.has(entry)) { + if (IGNORED_DIRS.has(entry.name)) { continue; } - const child = join(current, entry); - if (!isDirectory(child)) { + // Skip symlinks to avoid recursion cycles (repo pointing into itself, + // shared toolchain dirs, etc.). + if (entry.isSymbolicLink() || !entry.isDirectory()) { continue; } + const child = join(current, entry.name); const repo = new Repo(child); if (repo.isGitRepo() && repo.root !== root && repo.root === resolve(child)) { @@ -92,6 +98,9 @@ export function discoverWorkspaceRepos(root: string): WorkspaceRepoCandidate[] { const submoduleRoot = resolve(root, submodulePath); const repo = new Repo(submoduleRoot); if (!repo.isGitRepo()) { + console.warn( + `warning: submodule "${submodulePath}" is declared in .gitmodules but is not initialized; skipping. Run \`git submodule update --init\` to include it.`, + ); continue; } results.set(submodulePath, { @@ -107,6 +116,6 @@ export function discoverWorkspaceRepos(root: string): WorkspaceRepoCandidate[] { } return [...results.values()].sort((left, right) => - left.relativePath.localeCompare(right.relativePath) + left.relativePath.localeCompare(right.relativePath, "en") ); } From 0d3cf9e28785d719e5d4a931b780564a8c853a51 Mon Sep 17 00:00:00 2001 From: bingran-you via breeze-runner Date: Tue, 21 Apr 2026 18:57:25 -0700 Subject: [PATCH 2/2] fix(tree): handle unknown dirent types --- src/products/tree/engine/workspace.ts | 37 +++++++++++- tests/tree/workspace.test.ts | 81 +++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 tests/tree/workspace.test.ts diff --git a/src/products/tree/engine/workspace.ts b/src/products/tree/engine/workspace.ts index 57d797c0..1ed78e72 100644 --- a/src/products/tree/engine/workspace.ts +++ b/src/products/tree/engine/workspace.ts @@ -1,4 +1,10 @@ -import { existsSync, readdirSync, readFileSync, type Dirent } from "node:fs"; +import { + existsSync, + lstatSync, + readdirSync, + readFileSync, + type Dirent, +} from "node:fs"; import { join, relative, resolve } from "node:path"; import { Repo } from "#products/tree/engine/repo.js"; import { TREE_SUBMODULES_DIR } from "#products/tree/engine/runtime/asset-loader.js"; @@ -36,6 +42,33 @@ const IGNORED_DIRS = new Set([ const TREE_SUBMODULES_PREFIX = `${TREE_SUBMODULES_DIR.split(/[\\/]/).join("/")}/`; +function isTraversableDirectory(current: string, entry: Dirent): boolean { + if (entry.isSymbolicLink()) { + return false; + } + if (entry.isDirectory()) { + return true; + } + if ( + entry.isFile() || + entry.isBlockDevice() || + entry.isCharacterDevice() || + entry.isFIFO() || + entry.isSocket() + ) { + return false; + } + + // Some filesystems report an unknown dirent type; fall back to lstat so + // nested repos on network/FUSE mounts are still discovered. + try { + const stats = lstatSync(join(current, entry.name)); + return !stats.isSymbolicLink() && stats.isDirectory(); + } catch { + return false; + } +} + function parseGitmodules(root: string): string[] { try { const text = readFileSync(join(root, ".gitmodules"), "utf-8"); @@ -68,7 +101,7 @@ function discoverNestedRepos( } // Skip symlinks to avoid recursion cycles (repo pointing into itself, // shared toolchain dirs, etc.). - if (entry.isSymbolicLink() || !entry.isDirectory()) { + if (!isTraversableDirectory(current, entry)) { continue; } const child = join(current, entry.name); diff --git a/tests/tree/workspace.test.ts b/tests/tree/workspace.test.ts new file mode 100644 index 00000000..39639a68 --- /dev/null +++ b/tests/tree/workspace.test.ts @@ -0,0 +1,81 @@ +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { makeSourceRepo, useTmpDir } from "../helpers.js"; + +const mockState = vi.hoisted(() => ({ + root: null as string | null, + entries: null as import("node:fs").Dirent[] | null, +})); + +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + readdirSync: ((path: unknown, options?: unknown) => { + if ( + path === mockState.root && + typeof options === "object" && + options !== null && + "withFileTypes" in options && + options.withFileTypes === true && + mockState.entries !== null + ) { + return mockState.entries; + } + return actual.readdirSync( + path as Parameters[0], + options as never, + ); + }) as typeof actual.readdirSync, + }; +}); + +describe("discoverWorkspaceRepos", () => { + afterEach(() => { + mockState.root = null; + mockState.entries = null; + vi.resetModules(); + }); + + it("falls back to lstat when dirent type is unknown", async () => { + const tmp = useTmpDir(); + makeSourceRepo(tmp.path); + const nestedRoot = join(tmp.path, "nested"); + makeSourceRepo(nestedRoot); + + const actualFs = await vi.importActual("node:fs"); + const realReaddirSync = actualFs.readdirSync.bind(actualFs); + const rootEntries = realReaddirSync(tmp.path, { withFileTypes: true }); + + mockState.root = tmp.path; + mockState.entries = rootEntries.map((entry) => + entry.name === "nested" + ? ({ + name: entry.name, + isBlockDevice: () => false, + isCharacterDevice: () => false, + isDirectory: () => false, + isFIFO: () => false, + isFile: () => false, + isSocket: () => false, + isSymbolicLink: () => false, + } as import("node:fs").Dirent) + : entry + ); + vi.resetModules(); + + const { discoverWorkspaceRepos } = await import( + "#products/tree/engine/workspace.js" + ); + + expect(discoverWorkspaceRepos(tmp.path)).toEqual([ + { + kind: "nested-git-repo", + name: "nested", + relativePath: "nested", + root: nestedRoot, + }, + ]); + }); +});