fix(http-static): use HTTP paths in listings
CI / lint (pull_request) Successful in 1m45s
CI / format (pull_request) Successful in 2m27s
CI / install (pull_request) Successful in 5m53s
CI / typetest (pull_request) Successful in 2m3s
CI / node-tests (pull_request) Successful in 2m23s
CI / typecheck (pull_request) Successful in 2m29s
CI / browser-tests (pull_request) Successful in 4m3s
CI / lint (pull_request) Successful in 1m45s
CI / format (pull_request) Successful in 2m27s
CI / install (pull_request) Successful in 5m53s
CI / typetest (pull_request) Successful in 2m3s
CI / node-tests (pull_request) Successful in 2m23s
CI / typecheck (pull_request) Successful in 2m29s
CI / browser-tests (pull_request) Successful in 4m3s
Co-Authored-By: GPT-5.6 Luna <noreply@openai.com>
This commit was merged in pull request #100.
This commit is contained in:
@@ -10,6 +10,7 @@ This project was set up with the assistance of [Claude Code](https://claude.ai/c
|
||||
- **`@webnet/http-static` — VFS-backed static HTTP handler**: GPT-5.6 Luna added a new workspace package exposing `createStaticHandler`, with request/context path resolution, prefix and suffix lookup, index files, HTML/JSON directory listings, fallback paths, metadata and conditional responses, HEAD support, and single-byte range streaming. Added focused unit coverage and package build/test/typecheck scaffolding.
|
||||
- **`@webnet/http-static` — review fixes**: GPT-5.6 Luna addressed review findings by rejecting requests outside a configured prefix, returning 416 for ranges on empty files, normalizing method matching, and honoring `If-None-Match: *`.
|
||||
- **`@webnet/http-static` — precedence test vectors**: GPT-5.6 Luna added a seeded `MemoryVFS` matrix covering every requested path across default/custom/disabled indexes, suffix probing, and fallback configurations.
|
||||
- **`@webnet/http-static` — HTTP-relative directory listings**: GPT-5.6 Luna updated generated JSON paths and HTML links to use the request URL pathname, including configured HTTP prefixes, and added prefixed listing coverage.
|
||||
|
||||
- The `tailscale` submodule fork and its Go-side `tsconnect` patches (the `webnet` branch)
|
||||
- The `packages/tsconnect` TypeScript SDK
|
||||
|
||||
@@ -297,7 +297,7 @@ suite("createStaticHandler", () => {
|
||||
assert.equal(entries.length, 1)
|
||||
assert.deepEqual(entries[0], {
|
||||
name: "<read me>.txt",
|
||||
path: "/docs/<read me>.txt",
|
||||
path: "/docs/%3Cread%20me%3E.txt",
|
||||
isDirectory: false,
|
||||
size: "1",
|
||||
modifiedAt: entries[0].modifiedAt,
|
||||
@@ -305,6 +305,23 @@ suite("createStaticHandler", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("uses the HTTP path, including prefixes, in directory listings", async () => {
|
||||
const vfs = new MemoryVFS()
|
||||
await vfs.mkdir("/app")
|
||||
await write(vfs, "/app/<read me>.txt", "x")
|
||||
const handler = createStaticHandler(vfs, { prefix: "/static" })
|
||||
|
||||
const html = makeContext("GET", "/static/app/", { accept: "text/html" })
|
||||
await handler(html)
|
||||
const htmlBody = await readBody(html.res.body)
|
||||
assert.match(htmlBody, /href="\/static\/app\/%3Cread%20me%3E\.txt"/)
|
||||
|
||||
const json = makeContext("GET", "/static/app/", { accept: "application/json" })
|
||||
await handler(json)
|
||||
const entries = JSON.parse(await readBody(json.res.body))
|
||||
assert.equal(entries[0].path, "/static/app/%3Cread%20me%3E.txt")
|
||||
})
|
||||
|
||||
test("prefers index files, supports suffixes, and uses fallback", async () => {
|
||||
const vfs = new MemoryVFS()
|
||||
await vfs.mkdir("/site")
|
||||
|
||||
@@ -179,12 +179,22 @@ function escapeHtml(value: string): string {
|
||||
)
|
||||
}
|
||||
|
||||
function listingBody(path: string, entries: Stat[], type: "html" | "json"): string {
|
||||
function httpDirectoryPath(pathname: string): string {
|
||||
if (pathname === "/") return "/"
|
||||
return "/" + pathname.replace(/^\/+|\/+$/g, "")
|
||||
}
|
||||
|
||||
function httpEntryPath(path: string, name: string, isDirectory: boolean): string {
|
||||
const base = path === "/" ? "/" : path + "/"
|
||||
return base + encodeURIComponent(name) + (isDirectory ? "/" : "")
|
||||
}
|
||||
|
||||
function listingBody(httpPath: string, entries: Stat[], type: "html" | "json"): string {
|
||||
if (type === "json") {
|
||||
return JSON.stringify(
|
||||
entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
path: entry.path,
|
||||
path: httpEntryPath(httpPath, entry.name, entry.isDirectory),
|
||||
isDirectory: entry.isDirectory,
|
||||
size: entry.size.toString(),
|
||||
modifiedAt: entry.modifiedAt?.toISOString(),
|
||||
@@ -193,15 +203,14 @@ function listingBody(path: string, entries: Stat[], type: "html" | "json"): stri
|
||||
})),
|
||||
)
|
||||
}
|
||||
const base = path === "/" ? "/" : path + "/"
|
||||
const rows = entries
|
||||
.map((entry) => {
|
||||
const name = entry.name + (entry.isDirectory ? "/" : "")
|
||||
const href = base + encodeURIComponent(entry.name) + (entry.isDirectory ? "/" : "")
|
||||
const href = httpEntryPath(httpPath, entry.name, entry.isDirectory)
|
||||
return `<li><a href="${escapeHtml(href)}">${escapeHtml(name)}</a></li>`
|
||||
})
|
||||
.join("")
|
||||
return `<!doctype html><html><head><title>Index of ${escapeHtml(path)}</title></head><body><h1>Index of ${escapeHtml(path)}</h1><ul>${rows}</ul></body></html>`
|
||||
return `<!doctype html><html><head><title>Index of ${escapeHtml(httpPath)}</title></head><body><h1>Index of ${escapeHtml(httpPath)}</h1><ul>${rows}</ul></body></html>`
|
||||
}
|
||||
|
||||
async function statFile(vfs: AsyncVFS, path: string): Promise<Lookup | null> {
|
||||
@@ -283,7 +292,7 @@ async function serveDirectory(
|
||||
const type = listingType(ctx)
|
||||
if (!type) return false
|
||||
const entries = await vfs.readdir(lookup.path)
|
||||
const body = listingBody(lookup.path, entries, type)
|
||||
const body = listingBody(httpDirectoryPath(ctx.req.url.pathname), entries, type)
|
||||
ctx.res.setStatus(200, "OK")
|
||||
ctx.res.setHeader(
|
||||
"Content-Type",
|
||||
|
||||
Reference in New Issue
Block a user