Skip to content

Commit 1a3d03b

Browse files
committed
Fix preview browse URL including output filename for single files
PR #13804 made project always non-null via singleFileProjectContext(), causing the initialPath computation to always take the project branch. This produced URLs like http://localhost:PORT/hello.html instead of http://localhost:PORT/ for standalone files, breaking Posit Workbench proxy access. Guard with project.isSingleFile so single-file previews use root path. Extract computation into previewInitialPath() for testability. Fixes #14298
1 parent 3fa22f9 commit 1a3d03b

2 files changed

Lines changed: 89 additions & 7 deletions

File tree

src/command/preview/preview.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,21 @@ interface PreviewOptions {
143143
presentation: boolean;
144144
}
145145

146+
export function previewInitialPath(
147+
outputFile: string,
148+
project: ProjectContext | undefined,
149+
): string {
150+
if (isPdfContent(outputFile)) {
151+
return kPdfJsInitialPath;
152+
}
153+
if (project && !project.isSingleFile) {
154+
return pathWithForwardSlashes(
155+
relative(projectOutputDir(project), outputFile),
156+
);
157+
}
158+
return "";
159+
}
160+
146161
export async function preview(
147162
file: string,
148163
flags: RenderFlags,
@@ -253,13 +268,7 @@ export async function preview(
253268
);
254269

255270
// open browser if this is a browseable format
256-
const initialPath = isPdfContent(result.outputFile)
257-
? kPdfJsInitialPath
258-
: project
259-
? pathWithForwardSlashes(
260-
relative(projectOutputDir(project), result.outputFile),
261-
)
262-
: "";
271+
const initialPath = previewInitialPath(result.outputFile, project);
263272
if (
264273
options.browser &&
265274
!isServerSession() &&
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* preview-initial-path.test.ts
3+
*
4+
* Tests that previewInitialPath computes the correct browse URL path
5+
* for different project types. Regression test for #14298.
6+
*
7+
* Copyright (C) 2026 Posit Software, PBC
8+
*/
9+
10+
import { unitTest } from "../test.ts";
11+
import { assertEquals } from "testing/asserts";
12+
import { join } from "../../src/deno_ral/path.ts";
13+
import { normalizePath } from "../../src/core/path.ts";
14+
import { previewInitialPath } from "../../src/command/preview/preview.ts";
15+
import { ProjectContext } from "../../src/project/types.ts";
16+
17+
function mockProjectContext(
18+
dir: string,
19+
isSingleFile: boolean,
20+
): ProjectContext {
21+
return {
22+
dir: normalizePath(dir),
23+
isSingleFile,
24+
engines: [],
25+
files: { input: [], resources: [], config: [], configResources: [] },
26+
config: { project: {} },
27+
notebookContext: () => ({ resolve: () => undefined, get: () => undefined }),
28+
resolveFullMarkdownForFile: () => Promise.resolve(undefined),
29+
cleanup: () => {},
30+
} as unknown as ProjectContext;
31+
}
32+
33+
// deno-lint-ignore require-await
34+
unitTest("previewInitialPath - single file returns empty path (#14298)", async () => {
35+
const dir = normalizePath(Deno.makeTempDirSync({ prefix: "quarto-test" }));
36+
const outputFile = join(dir, "hello.html");
37+
const project = mockProjectContext(dir, true);
38+
39+
const result = previewInitialPath(outputFile, project);
40+
assertEquals(result, "", "Single-file preview should use root path, not filename");
41+
42+
Deno.removeSync(dir, { recursive: true });
43+
});
44+
45+
// deno-lint-ignore require-await
46+
unitTest("previewInitialPath - project file returns relative path", async () => {
47+
const dir = normalizePath(Deno.makeTempDirSync({ prefix: "quarto-test" }));
48+
const outputFile = join(dir, "chapter.html");
49+
const project = mockProjectContext(dir, false);
50+
51+
const result = previewInitialPath(outputFile, project);
52+
assertEquals(result, "chapter.html", "Project preview should include relative path");
53+
54+
Deno.removeSync(dir, { recursive: true });
55+
});
56+
57+
// deno-lint-ignore require-await
58+
unitTest("previewInitialPath - project subdir returns relative path", async () => {
59+
const dir = normalizePath(Deno.makeTempDirSync({ prefix: "quarto-test" }));
60+
const outputFile = join(dir, "pages", "about.html");
61+
const project = mockProjectContext(dir, false);
62+
63+
const result = previewInitialPath(outputFile, project);
64+
assertEquals(result, "pages/about.html", "Project preview should include subdirectory path");
65+
66+
Deno.removeSync(dir, { recursive: true });
67+
});
68+
69+
// deno-lint-ignore require-await
70+
unitTest("previewInitialPath - undefined project returns empty path", async () => {
71+
const result = previewInitialPath("/tmp/hello.html", undefined);
72+
assertEquals(result, "", "No project should use root path");
73+
});

0 commit comments

Comments
 (0)