Unify workspace service URLs (#1288)

* Unify workspace service proxying

* Clean up service proxy review issues
This commit is contained in:
Mohamed Boudra
2026-06-02 19:49:58 +08:00
committed by GitHub
parent 5666b3014a
commit 4f3116d28a
38 changed files with 2664 additions and 1538 deletions
@@ -13,7 +13,7 @@ describe("buildScriptHostname", () => {
branchName: null,
scriptName: "web",
}),
).toBe("web.paseo.localhost");
).toBe("web--paseo.localhost");
});
it("omits the branch label for main and master", () => {
@@ -23,14 +23,14 @@ describe("buildScriptHostname", () => {
branchName: "main",
scriptName: "web",
}),
).toBe("web.paseo.localhost");
).toBe("web--paseo.localhost");
expect(
buildScriptHostname({
projectSlug: "paseo",
branchName: "master",
scriptName: "web",
}),
).toBe("web.paseo.localhost");
).toBe("web--paseo.localhost");
});
it("builds non-default branch hostnames with script, branch, and project labels", () => {
@@ -40,7 +40,7 @@ describe("buildScriptHostname", () => {
branchName: "feature-auth",
scriptName: "web",
}),
).toBe("web.feature-auth.paseo.localhost");
).toBe("web--feature-auth--paseo.localhost");
});
it("slugifies script, default branch project, and non-default branch labels", () => {
@@ -50,7 +50,7 @@ describe("buildScriptHostname", () => {
branchName: "Feature/Auth Flow",
scriptName: "Web/API @ Dev",
}),
).toBe("web-api-dev.feature-auth-flow.paseo-app.localhost");
).toBe("web-api-dev--feature-auth-flow--paseo-app.localhost");
});
it("accepts already slugified labels because slugify is idempotent", () => {
@@ -60,7 +60,7 @@ describe("buildScriptHostname", () => {
branchName: "feature-auth-flow",
scriptName: "web-api-dev",
}),
).toBe("web-api-dev.feature-auth-flow.paseo-app.localhost");
).toBe("web-api-dev--feature-auth-flow--paseo-app.localhost");
});
it("uses untitled as the hostname-label fallback when labels collapse to empty", () => {
@@ -70,7 +70,7 @@ describe("buildScriptHostname", () => {
branchName: "***",
scriptName: "---",
}),
).toBe("untitled.untitled.untitled.localhost");
).toBe("untitled--untitled--untitled.localhost");
});
});
+9 -48
View File
@@ -1,6 +1,10 @@
import { slugify } from "./worktree.js";
import {
buildLocalServiceHostname,
buildPublicServiceHostname,
buildPublicServiceProxyUrl,
} from "../server/service-proxy.js";
const MAX_DNS_LABEL_LENGTH = 63;
// Compatibility boundary for older tests/imports; new service proxy code owns hostname rules.
interface BuildScriptHostnameOptions {
projectSlug: string;
@@ -12,64 +16,21 @@ interface BuildPublicScriptHostnameOptions extends BuildScriptHostnameOptions {
publicBaseUrl: string;
}
function toHostnameLabel(value: string): string {
return slugify(value) || "untitled";
}
function capDnsLabel(label: string): string {
if (label.length <= MAX_DNS_LABEL_LENGTH) {
return label;
}
return label.slice(0, MAX_DNS_LABEL_LENGTH).replace(/-+$/g, "") || "untitled";
}
function toPublicServiceLabel({
projectSlug,
branchName,
scriptName,
}: BuildScriptHostnameOptions): string {
const labels = [toHostnameLabel(scriptName)];
const isDefaultBranch = branchName === null || branchName === "main" || branchName === "master";
if (!isDefaultBranch) {
labels.push(toHostnameLabel(branchName));
}
labels.push(toHostnameLabel(projectSlug));
// Public URLs must keep script/branch/project in one label. The local
// script.branch.project.localhost shape would require multi-level wildcard
// DNS/certificates for arbitrary branch names, while a single label works with
// normal `*.base-domain` DNS and wildcard TLS. Cap it to the DNS 63-octet
// label limit so long branch/project/script names still produce resolvable URLs.
return capDnsLabel(labels.join("--"));
}
export function buildScriptHostname({
projectSlug,
branchName,
scriptName,
}: BuildScriptHostnameOptions): string {
const serviceHostnameLabel = toHostnameLabel(scriptName);
const projectHostnameLabel = toHostnameLabel(projectSlug);
const isDefaultBranch = branchName === null || branchName === "main" || branchName === "master";
if (isDefaultBranch) {
return `${serviceHostnameLabel}.${projectHostnameLabel}.localhost`;
}
return `${serviceHostnameLabel}.${toHostnameLabel(branchName)}.${projectHostnameLabel}.localhost`;
return buildLocalServiceHostname({ projectSlug, branchName, scriptName });
}
export function buildPublicScriptHostname({
publicBaseUrl,
...script
}: BuildPublicScriptHostnameOptions): string {
const base = new URL(publicBaseUrl);
return `${toPublicServiceLabel(script)}.${base.hostname}`;
return buildPublicServiceHostname({ publicBaseUrl, ...script });
}
export function buildPublicScriptProxyUrl(options: BuildPublicScriptHostnameOptions): string {
const base = new URL(options.publicBaseUrl);
const hostname = buildPublicScriptHostname(options);
const port = base.port ? `:${base.port}` : "";
return `${base.protocol}//${hostname}${port}`;
return buildPublicServiceProxyUrl(options);
}