diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts index 831f06f02d3..88b57887a80 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts @@ -1,6 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; import * as PlatformError from "effect/PlatformError"; @@ -69,6 +70,7 @@ function runShellEnvironment(input: { readonly platform: NodeJS.Platform; readonly handler: (command: ChildProcess.Command) => string; readonly failure?: PlatformError.PlatformError; + readonly fs?: FileSystem.FileSystem; }) { const environmentLayer = Layer.succeed( DesktopEnvironment.DesktopEnvironment, @@ -85,17 +87,23 @@ function runShellEnvironment(input: { ), ); - const program = Effect.gen(function* () { + const coreProgram = Effect.gen(function* () { const shellEnvironment = yield* DesktopShellEnvironment.DesktopShellEnvironment; yield* shellEnvironment.installIntoProcess; }).pipe( - Effect.provide( - DesktopShellEnvironment.layer.pipe( - Layer.provide(Layer.mergeAll(environmentLayer, NodeServices.layer, spawnerLayer)), - ), - ), + Effect.provide(DesktopShellEnvironment.layer), + Effect.provide(Layer.mergeAll(environmentLayer, spawnerLayer)), ); + const program = input.fs + ? coreProgram.pipe( + Effect.provideService(FileSystem.FileSystem, input.fs), + Effect.provide(NodeServices.layer) + ) + : coreProgram.pipe( + Effect.provide(NodeServices.layer) + ); + return withProcessEnv(input.env, program); } @@ -343,4 +351,83 @@ describe("DesktopShellEnvironment", () => { Effect.provide(Logger.layer([logger], { mergeWithExisting: false })), ); }); + + it.effect("skips ALL PowerShell probes when node is available statically with quotes and no fnm", () => + Effect.gen(function* () { + const mockDir = "C:\\mock\\temp"; + const env: NodeJS.ProcessEnv = { PATH: `"${mockDir}"` }; + const commands: ChildProcess.Command[] = []; + + const mockFs = { + exists: (path: string) => Effect.succeed(path.includes("node.exe")), + } as FileSystem.FileSystem; + + yield* runShellEnvironment({ + env, + platform: "win32", + fs: mockFs, + handler: (command) => { + commands.push(command); + return envOutput({ PATH: mockDir }); + }, + }); + + assert.equal(commands.length, 0); + assert.equal(env.PATH, `"${mockDir}"`); + }), + ); + + it.effect("loads PowerShell probes concurrently when node and fnm.cmd are both available", () => + Effect.gen(function* () { + const mockDir = "C:\\mock\\temp"; + const env: NodeJS.ProcessEnv = { PATH: mockDir }; + const commands: ChildProcess.Command[] = []; + + const mockFs = { + exists: (path: string) => Effect.succeed(path.includes("node.exe") || path.includes("fnm.cmd")), + } as FileSystem.FileSystem; + + yield* runShellEnvironment({ + env, + platform: "win32", + fs: mockFs, + handler: (command) => { + commands.push(command); + return envOutput({ PATH: mockDir }); + }, + }); + + assert.equal(commands.length, 2); + + const noProfileIdx = commands.findIndex((c) => c._tag === "StandardCommand" && c.args.includes("-NoProfile")); + const profileIdx = commands.findIndex((c) => c._tag === "StandardCommand" && !c.args.includes("-NoProfile")); + assert.isTrue(noProfileIdx !== -1); + assert.isTrue(profileIdx !== -1); + }), + ); + + it.effect("skips PowerShell probes when node is in knownWindowsCliDirs but not on PATH", () => + Effect.gen(function* () { + const mockDir = "C:\\mock\\temp"; + const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows\\System32", LOCALAPPDATA: mockDir }; + const commands: ChildProcess.Command[] = []; + + const mockFs = { + exists: (path: string) => Effect.succeed(path.includes("node.exe") && path.includes(mockDir)), + } as FileSystem.FileSystem; + + yield* runShellEnvironment({ + env, + platform: "win32", + fs: mockFs, + handler: (command) => { + commands.push(command); + return envOutput({ PATH: "C:\\Windows\\System32" }); + }, + }); + + assert.equal(commands.length, 0); + assert.isTrue(env.PATH!.includes("nodejs")); + }), + ); }); diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts index bd8aa6654f7..066e79549a6 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts @@ -378,12 +378,50 @@ const readWindowsEnvironment = Effect.fn("desktop.shellEnvironment.readWindowsEn const installWindowsEnvironment = Effect.fn("desktop.shellEnvironment.installWindowsEnvironment")( function* ( config: ShellEnvironmentConfig, - ): Effect.fn.Return { + ): Effect.fn.Return { + const fileSystem = yield* FileSystem.FileSystem; + + // Fast pre-check using static paths (no PowerShell required) + const staticPaths = mergePaths("win32", [ + trimNonEmpty(knownWindowsCliDirs(config.env).join(";")), + readEnvPath(config.env), + ]); + + let nodeFound = false; + let fnmFound = false; + if (Option.isSome(staticPaths)) { + for (const dir of staticPaths.value.split(";")) { + const cleanDir = dir.trim().replace(/^"+|"+$/g, ""); + if (!nodeFound && (yield* Effect.orElseSucceed(fileSystem.exists(`${cleanDir}/node.exe`), () => false))) { + nodeFound = true; + } + if (!fnmFound && ( + (yield* Effect.orElseSucceed(fileSystem.exists(`${cleanDir}/fnm.exe`), () => false)) || + (yield* Effect.orElseSucceed(fileSystem.exists(`${cleanDir}/fnm.cmd`), () => false)) || + (yield* Effect.orElseSucceed(fileSystem.exists(`${cleanDir}/fnm.ps1`), () => false)) + )) { + fnmFound = true; + } + if (nodeFound && fnmFound) break; + } + } + + const skipProfile = nodeFound && !fnmFound; + + // If node is found statically and no fnm wrappers exist, we can skip BOTH PowerShell probes entirely! + if (skipProfile) { + if (Option.isSome(staticPaths)) { + config.env.PATH = staticPaths.value; + } + return; // Exit early, 0ms startup! + } + // Concurrent, not sequential: these two probes are independent (only their // results are combined below) and each spawns its own PowerShell. Run in // series they sit at offset 0 of desktop.startup, before anything else, and // launch traces measured them at 2718ms then 2066ms — the entire 4.8s - // startup span, of which desktop.bootstrap is ~30ms. + // startup span, of which desktop.bootstrap is ~30ms. Total cost here is + // therefore bounded by the slowest single probe. const [noProfile, profile] = yield* Effect.all( [ readWindowsEnvironment(["PATH"], { loadProfile: false }), @@ -391,13 +429,18 @@ const installWindowsEnvironment = Effect.fn("desktop.shellEnvironment.installWin ], { concurrency: 2 }, ); - const mergedPath = mergePaths("win32", [ - trimNonEmpty(profile.PATH), + + const fastMergedPath = mergePaths("win32", [ trimNonEmpty(knownWindowsCliDirs(config.env).join(";")), trimNonEmpty(noProfile.PATH), readEnvPath(config.env), ]); + const mergedPath = mergePaths("win32", [ + trimNonEmpty(profile.PATH), + fastMergedPath, + ]); + if (Option.isSome(mergedPath)) { config.env.PATH = mergedPath.value; }