diff --git a/packages/cli/src/commands/preview.test.ts b/packages/cli/src/commands/preview.test.ts index 4d6c7528fd..e90dade70f 100644 --- a/packages/cli/src/commands/preview.test.ts +++ b/packages/cli/src/commands/preview.test.ts @@ -9,6 +9,7 @@ import { foregroundPreviewReadyPayload, handlePreviewKillAll, handlePreviewList, + openStudioBrowser, previewLaunchMode, previewLaunchModeError, previewPortError, @@ -21,12 +22,19 @@ import { studioSummaryUrls, waitForStudioChildClose, } from "./preview.js"; +import { openBrowser } from "../utils/openBrowser.js"; + +vi.mock("../utils/openBrowser.js", async (importOriginal) => ({ + ...(await importOriginal()), + openBrowser: vi.fn(), +})); const tempDirs: string[] = []; afterEach(() => { for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true }); vi.restoreAllMocks(); + vi.unstubAllGlobals(); process.exitCode = undefined; }); @@ -493,3 +501,30 @@ describe("waitForStudioChildClose", () => { expect(signalTarget.off).toHaveBeenCalledTimes(2); }); }); + +describe("openStudioBrowser", () => { + it("warms the preview route before the browser launch, and also under --no-open", () => { + const order: string[] = []; + vi.stubGlobal( + "fetch", + vi.fn((url: string) => { + order.push(`fetch ${url}`); + return Promise.resolve({ body: null } as Response); + }), + ); + vi.mocked(openBrowser) + .mockReset() + .mockImplementation(() => { + order.push("openBrowser"); + }); + + openStudioBrowser("http://127.0.0.1:4567", "Launch #1", "/tmp/launch"); + openStudioBrowser("http://127.0.0.1:4567", "demo", "/tmp/demo", { noOpen: true }); + + expect(order).toEqual([ + "fetch http://127.0.0.1:4567/api/projects/Launch%20%231/preview", + "openBrowser", + "fetch http://127.0.0.1:4567/api/projects/demo/preview", + ]); + }); +}); diff --git a/packages/cli/src/commands/preview.ts b/packages/cli/src/commands/preview.ts index 5203d7eead..ddd092786e 100644 --- a/packages/cli/src/commands/preview.ts +++ b/packages/cli/src/commands/preview.ts @@ -1258,12 +1258,20 @@ function previewLifecycleSession(options: { }; } -function openStudioBrowser( +export function openStudioBrowser( url: string, projectName: string, projectDir: string, options?: BrowserLaunchOptions, ): void { + // Pay the route's cold compiler import here so the browser's first request + // doesn't. The shader query params the player appends aren't in the route's + // ETag; the timeout stops an unsettled fetch holding the CLI's event loop. + void fetch(`${url}/api/projects/${encodeURIComponent(projectName)}/preview`, { + signal: AbortSignal.timeout(10_000), + }) + .then((res) => res.body?.cancel()) + .catch(() => {}); if (options?.noOpen) return; openBrowser(studioDeepLink(url, projectName, projectDir), { browserPath: options?.browserPath,