Skip to content

Adding support for hosting with URL prefix #1357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion cmd/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ export async function serveCommand(
);
}

let hostUrlPrefix = Deno.env.get("SB_URL_PREFIX");
if (hostUrlPrefix) {
if (!hostUrlPrefix.startsWith("/")) {
hostUrlPrefix = "/" + hostUrlPrefix;
}
if (hostUrlPrefix.endsWith("/")) {
hostUrlPrefix = hostUrlPrefix.replace(/\/*$/, "");
}

if (hostUrlPrefix !== "") {
console.log(`Host URL Prefix: ${hostUrlPrefix}`);
} else {
hostUrlPrefix = undefined;
}
}

const userAuth = options.user ?? Deno.env.get("SB_USER");

let userCredentials: AuthOptions | undefined;
Expand Down Expand Up @@ -100,7 +116,7 @@ export async function serveCommand(
const manifestName = Deno.env.get("SB_NAME");
const manifestDescription = Deno.env.get("SB_DESCRIPTION");

if (manifestName || manifestDescription) {
if (manifestName || manifestDescription || hostUrlPrefix) {
const manifestData = JSON.parse(
clientAssets.readTextFileSync(".client/manifest.json"),
);
Expand All @@ -110,6 +126,13 @@ export async function serveCommand(
if (manifestDescription) {
manifestData.description = manifestDescription;
}
if (hostUrlPrefix) {
for (const icon of manifestData.icons) {
if (icon.src) icon.src = hostUrlPrefix + icon.src;
}
manifestData.start_url = hostUrlPrefix + manifestData.start_url;
manifestData.scope = hostUrlPrefix + manifestData.scope;
}
clientAssets.writeTextFileSync(
".client/manifest.json",
"application/json",
Expand All @@ -131,6 +154,7 @@ export async function serveCommand(
shellBackend: backendConfig,
enableSpaceScript,
pagesPath: folder,
hostUrlPrefix,
});
await httpServer.start();

Expand Down
3 changes: 2 additions & 1 deletion common/space_lua/stdlib/space_lua.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ export const spaceluaApi = new LuaTable({
if (typeof location === "undefined") {
return null;
} else {
return location.protocol + "//" + location.host;
//NOTE: Removing trailing slash to stay compatible with original code: `location.protocol + "//" + location.host;`
return document.baseURI.replace(/\/*$/, "");
}
},
),
Expand Down
2 changes: 1 addition & 1 deletion common/syscalls/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export function systemSyscalls(
await client.ds.batchDelete(allKeys);
}
if (logout) {
location.href = "/.logout";
location.href = ".logout";
} else {
alert("Client wiped, feel free to navigate elsewhere");
}
Expand Down
204 changes: 204 additions & 0 deletions lib/url_prefix.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { assertEquals } from "@std/assert";
import { applyUrlPrefix, removeUrlPrefix } from "$lib/url_prefix.ts";

Deno.test("url_prefix - removeUrlPrefix - with value", async (t) => {
await t.step("Absolute URL, present, should be removed", () => {
assertEquals(
removeUrlPrefix("http://myserver/sb/relevant", "/sb"),
"http://myserver/relevant",
);
assertEquals(
removeUrlPrefix("https://myserver/sb/relevant", "/sb"),
"https://myserver/relevant",
);
});

await t.step("Absolute URL, present, should only remove leading", () => {
assertEquals(
removeUrlPrefix("http://myserver/sb/sb/relevant/sb", "/sb"),
"http://myserver/sb/relevant/sb",
);
assertEquals(
removeUrlPrefix("http://myserver/relevant/sb", "/sb"),
"http://myserver/relevant/sb",
);
});

await t.step("Absolute URL, absent, should be untouched", () => {
assertEquals(
removeUrlPrefix("http://myserver/other/relevant", "/sb"),
"http://myserver/other/relevant",
);
assertEquals(
removeUrlPrefix("https://myserver/other/relevant", "/sb"),
"https://myserver/other/relevant",
);
});

await t.step("Absolute URL, queryString, should be preserved", () => {
assertEquals(
removeUrlPrefix("http://myserver/sb/sb/relevant/sb?param=arg", "/sb"),
"http://myserver/sb/relevant/sb?param=arg",
);
});

await t.step("Absolute URL, unsupported, should be untouched", () => {
assertEquals(
removeUrlPrefix("ftp://myserver/sb/relevant", "/sb"),
"ftp://myserver/sb/relevant",
);
});

await t.step("Host-Relative URL, present, should be removed", () => {
assertEquals(removeUrlPrefix("/sb/relevant", "/sb"), "/relevant");
});

await t.step("Host-Relative URL, present, should only remove leading", () => {
assertEquals(
removeUrlPrefix("/sb/sb/relevant/sb", "/sb"),
"/sb/relevant/sb",
);
assertEquals(removeUrlPrefix("/relevant/sb", "/sb"), "/relevant/sb");
});

await t.step("Host-Relative URL, queryString, should be preserved", () => {
assertEquals(
removeUrlPrefix("/sb/sb/relevant/sb?param=arg", "/sb"),
"/sb/relevant/sb?param=arg",
);
assertEquals(
removeUrlPrefix("/relevant/sb?param=arg", "/sb"),
"/relevant/sb?param=arg",
);
});

await t.step("Host-Relative URL, absent, should be untouched", () => {
assertEquals(removeUrlPrefix("/other/relevant", "/sb"), "/other/relevant");
});

await t.step("Page-Relative URL, should be untouched", () => {
assertEquals(removeUrlPrefix("sb/relevant", "/sb"), "sb/relevant");
});
});

Deno.test("url_prefix - removeUrlPrefix - no value", async (t) => {
await t.step("Absolute URL, should be untouched", () => {
assertEquals(
removeUrlPrefix("http://myserver/sb/relevant", ""),
"http://myserver/sb/relevant",
);
assertEquals(
removeUrlPrefix("https://myserver/sb/relevant"),
"https://myserver/sb/relevant",
);
});

await t.step("Host-Relative URL, should be untouched", () => {
assertEquals(removeUrlPrefix("/sb/relevant", ""), "/sb/relevant");
assertEquals(removeUrlPrefix("/sb/relevant"), "/sb/relevant");
});

await t.step("Page-Relative URL, should be untouched", () => {
assertEquals(removeUrlPrefix("sb/relevant", ""), "sb/relevant");
assertEquals(removeUrlPrefix("sb/relevant"), "sb/relevant");
});
});

Deno.test("url_prefix - applyUrlPrefix - with value", async (t) => {
await t.step("string, Absolute URL, should be prefixed", () => {
assertEquals(
applyUrlPrefix("http://myserver/relevant", "/sb"),
"http://myserver/sb/relevant",
);
assertEquals(
applyUrlPrefix("https://myserver/relevant", "/sb"),
"https://myserver/sb/relevant",
);
});

await t.step("string, Absolute URL, should not care about dups", () => {
assertEquals(
applyUrlPrefix("http://myserver/sb/relevant/sb", "/sb"),
"http://myserver/sb/sb/relevant/sb",
);
});

await t.step("string, Absolute URL, queryString should be preserved", () => {
assertEquals(
applyUrlPrefix("http://myserver/sb/relevant/sb?param=arg", "/sb"),
"http://myserver/sb/sb/relevant/sb?param=arg",
);
});

await t.step("string, Absolute URL, unsupported, should be untouched", () => {
assertEquals(
applyUrlPrefix("ftp://myserver/relevant", "/sb"),
"ftp://myserver/relevant",
);
});

await t.step("string, Host-Relative URL, should be prefixed", () => {
assertEquals(applyUrlPrefix("/relevant", "/sb"), "/sb/relevant");
});

await t.step("string, Host-Relative URL, should not care about dups", () => {
assertEquals(
applyUrlPrefix("/sb/relevant/sb", "/sb"),
"/sb/sb/relevant/sb",
);
});

await t.step(
"string, Host-Relative URL, queryString should be preserved",
() => {
assertEquals(
applyUrlPrefix("/sb/relevant/sb?param=arg", "/sb"),
"/sb/sb/relevant/sb?param=arg",
);
},
);

await t.step("string, Page-Relative URL, should be untouched", () => {
assertEquals(applyUrlPrefix("relevant", "/sb"), "relevant");
});

await t.step("URL object, Absolute URL, should be prefixed", () => {
assertEquals(
applyUrlPrefix(new URL("http://myserver/relevant"), "/sb"),
new URL("http://myserver/sb/relevant"),
);
});

await t.step(
"URL object, Absolute URL, queryString should be preserved",
() => {
assertEquals(
applyUrlPrefix(new URL("http://myserver/relevant?param=arg"), "/sb"),
new URL("http://myserver/sb/relevant?param=arg"),
);
},
);
});

Deno.test("url_prefix - applyUrlPrefix - no value", async (t) => {
await t.step("Absolute URL, should be untouched", () => {
assertEquals(
applyUrlPrefix("http://myserver/relevant", ""),
"http://myserver/relevant",
);
assertEquals(
applyUrlPrefix("https://myserver/relevant"),
"https://myserver/relevant",
);
});

await t.step("Host-Relative URL, should be untouched", () => {
assertEquals(applyUrlPrefix("/relevant", ""), "/relevant");
assertEquals(applyUrlPrefix("/relevant"), "/relevant");
});

await t.step("Page-Relative URL, should be untouched", () => {
assertEquals(applyUrlPrefix("relevant", ""), "relevant");
assertEquals(applyUrlPrefix("relevant"), "relevant");
});
});
44 changes: 44 additions & 0 deletions lib/url_prefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export function removeUrlPrefix(url: string, prefix?: string): string {
if (!prefix || prefix === "") return url;

if (url.startsWith("http://") || url.startsWith("https://")) {
const parsedUrl = new URL(url);
if (parsedUrl.pathname.startsWith(prefix)) {
parsedUrl.pathname = parsedUrl.pathname.substring(
prefix.length,
);
return parsedUrl.href;
} else {
return url;
}
} else if (url.startsWith(prefix)) {
return url.substring(prefix.length);
} else {
return url;
}
}

export function applyUrlPrefix<T extends (string | URL)>(
url: T,
prefix?: string,
): T {
if (!prefix || prefix === "") return url;

if (typeof url === "string") {
const urlString = url as string;

if (urlString.startsWith("http://") || urlString.startsWith("https://")) {
return applyUrlPrefix(new URL(urlString), prefix).href as T;
} else if (urlString.startsWith("/")) {
return (prefix + urlString) as T;
} else {
return url; //return page-relative paths as-is
}
} else if (url.protocol === "http:" || url.protocol === "https:") {
const urlObj = new URL(url);
urlObj.pathname = prefix + urlObj.pathname;
return urlObj as T;
} else {
return url;
}
}
2 changes: 1 addition & 1 deletion plugs/markdown/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export async function updateMarkdownPreview() {
"rhs",
2,
`
<link rel="stylesheet" href="/.client/main.css" />
<link rel="stylesheet" href=".client/main.css" />
<style>
${css}
${customStyles ?? ""}
Expand Down
Loading