From 8faa7abccac4e06fc55fb472ac84fd0cfeb5300e Mon Sep 17 00:00:00 2001 From: badcuban Date: Mon, 25 May 2026 02:26:54 -0400 Subject: [PATCH] fix: include directory junctions in project browser --- .../src/workspace/WorkspaceEntries.test.ts | 22 ++++++ apps/server/src/workspace/WorkspaceEntries.ts | 70 +++++++++++++++---- 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/apps/server/src/workspace/WorkspaceEntries.test.ts b/apps/server/src/workspace/WorkspaceEntries.test.ts index f8a518d8b33..b271a210e09 100644 --- a/apps/server/src/workspace/WorkspaceEntries.test.ts +++ b/apps/server/src/workspace/WorkspaceEntries.test.ts @@ -334,6 +334,28 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceEntries", (it) => { }), ); + it.effect("includes directory symlinks when browsing", () => + Effect.gen(function* () { + const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; + const path = yield* Path.Path; + const platform = yield* HostProcessPlatform; + const cwd = yield* makeTempDir({ prefix: "t3code-workspace-browse-symlink-" }); + const target = path.join(cwd, "target"); + const link = path.join(cwd, "linked"); + yield* writeTextFile(cwd, "target/index.ts", "export {};\n"); + yield* Effect.promise(() => + fsPromises.symlink(target, link, platform === "win32" ? "junction" : "dir"), + ); + + const result = yield* workspaceEntries.browse({ + partialPath: yield* appendSeparator(cwd), + }); + + expect(result.entries).toContainEqual({ name: "linked", fullPath: link }); + expect(result.entries).toContainEqual({ name: "target", fullPath: target }); + }), + ); + it.effect("supports relative paths when cwd is provided", () => Effect.gen(function* () { const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; diff --git a/apps/server/src/workspace/WorkspaceEntries.ts b/apps/server/src/workspace/WorkspaceEntries.ts index bf9a51c74db..146415fb1d7 100644 --- a/apps/server/src/workspace/WorkspaceEntries.ts +++ b/apps/server/src/workspace/WorkspaceEntries.ts @@ -1,4 +1,5 @@ // @effect-diagnostics nodeBuiltinImport:off +import type { Dirent } from "node:fs"; import * as NodeFSP from "node:fs/promises"; import * as NodeOS from "node:os"; @@ -60,6 +61,41 @@ export class WorkspaceEntries extends Context.Service< } >()("t3/workspace/WorkspaceEntries") {} +const WINDOWS_LEGACY_PROFILE_JUNCTION_NAMES = new Set([ + "Application Data", + "Cookies", + "Local Settings", + "My Documents", + "NetHood", + "PrintHood", + "Recent", + "SendTo", + "Start Menu", + "Templates", +]); + +async function isDirectoryEntry( + dirent: Dirent, + fullPath: string, + platform: NodeJS.Platform, +): Promise { + if (dirent.isDirectory()) { + return true; + } + if (!dirent.isSymbolicLink()) { + return false; + } + if (platform === "win32" && WINDOWS_LEGACY_PROFILE_JUNCTION_NAMES.has(dirent.name)) { + return false; + } + + try { + return (await NodeFSP.stat(fullPath)).isDirectory(); + } catch { + return false; + } +} + function expandHomePath(input: string, path: Path.Path): string { if (input === "~") { return NodeOS.homedir(); @@ -103,6 +139,7 @@ const resolveBrowseTarget = ( const make = Effect.gen(function* () { const path = yield* Path.Path; + const platform = yield* HostProcessPlatform; const workspacePaths = yield* WorkspacePaths.WorkspacePaths; const workspaceSearchIndexes = yield* WorkspaceSearchIndex.WorkspaceSearchIndexMap; @@ -177,23 +214,28 @@ const make = Effect.gen(function* () { const showHidden = endsWithSeparator || prefix.startsWith("."); const lowerPrefix = prefix.toLowerCase(); - const entries: Array<{ readonly name: string; readonly fullPath: string }> = []; - for (const dirent of dirents) { - if ( - dirent.isDirectory() && - dirent.name.toLowerCase().startsWith(lowerPrefix) && - (showHidden || !dirent.name.startsWith(".")) - ) { - entries.push({ - name: dirent.name, - fullPath: path.join(parentPath, dirent.name), - }); - } - } + const entries = yield* Effect.forEach( + dirents, + (dirent) => + Effect.promise(async () => { + const fullPath = path.join(parentPath, dirent.name); + if ( + !dirent.name.toLowerCase().startsWith(lowerPrefix) || + (!showHidden && dirent.name.startsWith(".")) || + !(await isDirectoryEntry(dirent, fullPath, platform)) + ) { + return null; + } + return { name: dirent.name, fullPath }; + }), + { concurrency: 16 }, + ); return { parentPath, - entries: entries.toSorted((left, right) => left.name.localeCompare(right.name)), + entries: entries + .filter((entry): entry is NonNullable => entry !== null) + .toSorted((left, right) => left.name.localeCompare(right.name)), }; }, );