Skip to content

Commit f473e53

Browse files
authored
fix: preserve preview path when proxying tunnel requests (#3)
1 parent f4dad60 commit f473e53

4 files changed

Lines changed: 64 additions & 3 deletions

File tree

src/__tests__/config.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,21 @@ describe("resolveConfig", () => {
5353
expect(config.deviceSecret).toBe("secret123");
5454
expect(config.port).toBe(4010);
5555
});
56+
57+
it("reads previewPath from legacy spawndock.config.json", () => {
58+
const dir = mkdtempSync(join(tmpdir(), "spawndock-config-"));
59+
writeFileSync(
60+
join(dir, "spawndock.config.json"),
61+
JSON.stringify({
62+
controlPlaneUrl: "http://localhost:8787",
63+
projectSlug: "my-app",
64+
deviceSecret: "secret123",
65+
localPort: 4010,
66+
previewPath: "/preview/my-app",
67+
}),
68+
);
69+
70+
const config = resolveConfig([], dir);
71+
expect(config.previewPath).toBe("/preview/my-app");
72+
});
5673
});

src/__tests__/tunnel.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { buildWsUrl } from "../tunnel.js";
4+
5+
describe("buildWsUrl", () => {
6+
it("includes protocol version and token query params", () => {
7+
const url = buildWsUrl({
8+
controlPlane: "https://api.example.com",
9+
projectSlug: "demo",
10+
deviceSecret: "secret123",
11+
port: 3000,
12+
});
13+
14+
expect(url).toBe(
15+
"wss://api.example.com/tunnel/connect?protocolVersion=1&token=secret123",
16+
);
17+
});
18+
});

src/config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface TunnelConfig {
66
projectSlug: string;
77
deviceSecret: string;
88
port: number;
9+
previewPath?: string;
910
}
1011

1112
const PRIMARY_CONFIG_FILE = "spawndock.dev-tunnel.json";
@@ -46,8 +47,10 @@ function normalizeConfig(data: unknown): Partial<TunnelConfig> {
4647
: typeof record.localPort === "number"
4748
? record.localPort
4849
: undefined;
50+
const previewPath =
51+
typeof record.previewPath === "string" ? record.previewPath : undefined;
4952

50-
return { controlPlane, projectSlug, deviceSecret, port };
53+
return { controlPlane, projectSlug, deviceSecret, port, previewPath };
5154
}
5255

5356
function readConfigFile(dir: string): Partial<TunnelConfig> {
@@ -134,10 +137,11 @@ export function resolveConfig(
134137
const projectSlug = args.projectSlug ?? env.projectSlug ?? file.projectSlug;
135138
const deviceSecret = args.deviceSecret ?? env.deviceSecret ?? file.deviceSecret;
136139
const port = args.port ?? env.port ?? file.port ?? 3000;
140+
const previewPath = file.previewPath;
137141

138142
if (!controlPlane) throw new Error("Missing --control-plane or SPAWNDOCK_CONTROL_PLANE");
139143
if (!projectSlug) throw new Error("Missing --project-slug or SPAWNDOCK_PROJECT_SLUG");
140144
if (!deviceSecret) throw new Error("Missing --device-secret or SPAWNDOCK_DEVICE_SECRET");
141145

142-
return { controlPlane, projectSlug, deviceSecret, port };
146+
return { controlPlane, projectSlug, deviceSecret, port, previewPath };
143147
}

src/tunnel.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ export function createTunnel(config: TunnelConfig): void {
1010
connect(wsUrl, config, localOrigin);
1111
}
1212

13+
function resolveForwardedPath(previewPath: string | undefined, path: string): string {
14+
if (!previewPath || previewPath.length === 0) {
15+
return path;
16+
}
17+
18+
const normalizedPreviewPath = previewPath.replace(/\/$/, "");
19+
const [pathname, query = ""] = path.split("?");
20+
21+
const forwardedPath =
22+
pathname === "/"
23+
? `${normalizedPreviewPath}/`
24+
: `${normalizedPreviewPath}${pathname}`;
25+
26+
return query.length > 0 ? `${forwardedPath}?${query}` : forwardedPath;
27+
}
28+
1329
export function buildWsUrl(config: TunnelConfig): string {
1430
if (!URL.canParse(config.controlPlane)) {
1531
throw new Error(`Invalid control plane URL: ${config.controlPlane}`);
@@ -64,7 +80,13 @@ function connect(wsUrl: string, config: TunnelConfig, localOrigin: string): void
6480

6581
if (msg.type === "http-request") {
6682
try {
67-
const response = await proxyRequest(msg.request, localOrigin);
83+
const response = await proxyRequest(
84+
{
85+
...msg.request,
86+
path: resolveForwardedPath(config.previewPath, msg.request.path),
87+
},
88+
localOrigin,
89+
);
6890
ws.send(serialize({ type: "http-response", response }));
6991
} catch (err: any) {
7092
ws.send(serialize({

0 commit comments

Comments
 (0)