- commit
- 87c3a377af445c0a4f1498c1c507aac32952d397
- parent
- 3b0087e94a68be164ac7351275b1fd2f05707849
- author
- Dami <dami.etoile@gmail.com>
| M | worker/ | 15 | ++++ |
| A | worker/ | 106 | ++++++++++++++++++++++++++++++ |
@@ -95,6 +95,7 @@ export async function serve(req: Request, env: Env, url: URL): Promise<Response>
9595 // relative to break); multi-file bundles redirect to the trailing slash so9696 // relative asset URLs resolve.9797 let assetPath: string;98+ let explicitIndexPath = false;9899 if (m[2] === undefined) {99100 if (filePaths.length !== 1) {100101 return Response.redirect(new URL(`/${addressStr}/`, url).toString(), 302);@@ -102,9 +103,23 @@ export async function serve(req: Request, env: Env, url: URL): Promise<Response>
102103 assetPath = filePaths[0];103104 } else {104105 assetPath = decodeURIComponent(m[2].slice(1)); // strip leading /106+ explicitIndexPath = /(^|\/)index\.html$/.test(assetPath);105107 if (assetPath === "" || assetPath.endsWith("/")) assetPath += "index.html";106108 }107109 110+ // Keep directory index filenames out of public URLs. Static generators often111+ // emit same-page and anchor links as `index.html` or `index.html#section`;112+ // browsers preserve the fragment while following this redirect.113+ if (explicitIndexPath && manifest.files[assetPath]) {114+ const isRootIndex = assetPath === "index.html";115+ const canonicalPath = isRootIndex && filePaths.length === 1116+ ? `/${addressStr}`117+ : path.slice(0, -"index.html".length);118+ const canonicalUrl = new URL(canonicalPath, url);119+ canonicalUrl.search = url.search;120+ return Response.redirect(canonicalUrl.toString(), 302);121+ }122+108123 let entry = manifest.files[assetPath];109124 if (!entry && manifest.files[`${assetPath}/index.html`]) {110125 // Directory hit without trailing slash: redirect so relative URLs resolve.@@ -0,0 +1,106 @@
1+import assert from "node:assert/strict";2+import test from "node:test";3+4+import { serve } from "../src/serve.ts";5+6+const HASH = "a".repeat(64);7+8+function envFor(files) {9+ const manifest = new TextEncoder().encode(JSON.stringify({ v: 1, files }));10+ const site = {11+ address: 0x2f9b,12+ vault_slot: 2,13+ alias: null,14+ current_manifest: "manifest-hash",15+ created_at: 0,16+ updated_at: 0,17+ expires_at: null,18+ password_hash: null,19+ auth_version: 1,20+ };21+22+ return {23+ DB: {24+ prepare() {25+ return {26+ bind() {27+ return { first: async () => site };28+ },29+ };30+ },31+ },32+ CONTENT: {33+ get: async (key) => {34+ if (key === "manifest/manifest-hash") {35+ return { arrayBuffer: async () => manifest.buffer };36+ }37+ if (key === `blob/${HASH}`) return { body: new Uint8Array() };38+ return null;39+ },40+ },41+ };42+}43+44+const html = { h: HASH, s: 0, ct: "text/html; charset=utf-8" };45+const css = { h: HASH, s: 0, ct: "text/css; charset=utf-8" };46+47+test("single-file root index redirects to the bare site URL", async () => {48+ const response = await serve(49+ new Request("https://n.zip/2f9b/index.html?view=full"),50+ envFor({ "index.html": html }),51+ new URL("https://n.zip/2f9b/index.html?view=full"),52+ );53+54+ assert.equal(response.status, 302);55+ assert.equal(56+ response.headers.get("location"),57+ "https://n.zip/2f9b?view=full",58+ );59+});60+61+test("multi-file root index redirects to the site directory", async () => {62+ const url = new URL("https://n.zip/2f9b/index.html");63+ const response = await serve(64+ new Request(url),65+ envFor({ "index.html": html, "style.css": css }),66+ url,67+ );68+69+ assert.equal(response.status, 302);70+ assert.equal(response.headers.get("location"), "https://n.zip/2f9b/");71+});72+73+test("nested index redirects to its directory", async () => {74+ const url = new URL("https://n.zip/2f9b/docs/index.html");75+ const response = await serve(76+ new Request(url),77+ envFor({ "index.html": html, "docs/index.html": html }),78+ url,79+ );80+81+ assert.equal(response.status, 302);82+ assert.equal(response.headers.get("location"), "https://n.zip/2f9b/docs/");83+});84+85+test("canonical directory URL serves its index without redirecting", async () => {86+ const url = new URL("https://n.zip/2f9b/");87+ const response = await serve(88+ new Request(url),89+ envFor({ "index.html": html, "style.css": css }),90+ url,91+ );92+93+ assert.equal(response.status, 200);94+ assert.equal(response.headers.get("location"), null);95+});96+97+test("missing index path still returns not found", async () => {98+ const url = new URL("https://n.zip/2f9b/index.html");99+ const response = await serve(100+ new Request(url),101+ envFor({ "download.txt": { ...css, ct: "text/plain; charset=utf-8" } }),102+ url,103+ );104+105+ assert.equal(response.status, 404);106+});