diff --git a/integrationTests/components/static-urlpath.test.ts b/integrationTests/components/static-urlpath.test.ts index 2e3fae493..87c15a0bd 100644 --- a/integrationTests/components/static-urlpath.test.ts +++ b/integrationTests/components/static-urlpath.test.ts @@ -9,7 +9,15 @@ * npm run test:integration -- "integrationTests/components/static-urlpath.test.ts" */ import { suite, test, before, after } from 'node:test'; +<<<<<<< HEAD +<<<<<<< HEAD import { strictEqual, ok } from 'node:assert'; +======= +import { strictEqual, ok } from 'node:assert/strict'; +>>>>>>> 21d022b54 (Fix static plugin serving nothing when urlPath is configured (#1583)) +======= +import { strictEqual, ok } from 'node:assert'; +>>>>>>> a2e9a63f1 (fix(lint): import from node:assert, not node:assert/strict) import { resolve } from 'node:path'; import { setupHarperWithFixture, teardownHarper, type ContextWithHarper } from '@harperfast/integration-testing'; @@ -61,6 +69,10 @@ suite('static plugin with urlPath (#1583)', (ctx: ContextWithHarper) => { strictEqual(res.status, 301); strictEqual(res.headers.get('location'), '/assets/'); }); +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> aae9a110d (Preserve query strings on static trailing-slash redirects) test('preserves the query string on trailing-slash redirects', async () => { const root = await fetch(new URL('/assets?foo=bar', ctx.harper.httpURL), { redirect: 'manual' }); @@ -70,4 +82,9 @@ suite('static plugin with urlPath (#1583)', (ctx: ContextWithHarper) => { strictEqual(dir.status, 301); strictEqual(dir.headers.get('location'), '/assets/docs/?foo=bar'); }); +<<<<<<< HEAD +======= +>>>>>>> 21d022b54 (Fix static plugin serving nothing when urlPath is configured (#1583)) +======= +>>>>>>> aae9a110d (Preserve query strings on static trailing-slash redirects) }); diff --git a/server/static.ts b/server/static.ts index 0e20fd594..1e52d78ba 100644 --- a/server/static.ts +++ b/server/static.ts @@ -118,6 +118,8 @@ export function handleApplication(scope: Scope) { staticFile = indexEntries.get(req.pathname); // The router strips both '/assets' and '/assets/' down to '/', so the mount root +<<<<<<< HEAD +<<<<<<< HEAD // must be disambiguated via the unstripped pathname (exposed by stripPrefix): // redirect the no-slash form so relative links on the index page resolve under // the mount (#1583). Query string is preserved across both redirects; compute it @@ -131,6 +133,33 @@ export function handleApplication(scope: Scope) { status: 301, headers: { Location: baseURLPath + query, +======= + // must be disambiguated via the unstripped request: redirect the no-slash form so + // relative links on the index page resolve under the mount (#1583) +======= + // must be disambiguated via the unstripped pathname (exposed by stripPrefix): + // redirect the no-slash form so relative links on the index page resolve under +<<<<<<< HEAD + // the mount (#1583) +>>>>>>> eb6631deb (Expose originalPathname from stripPrefix for runtime-agnostic mount-root disambiguation) +======= + // the mount (#1583). Query string is preserved across both redirects; compute it + // lazily inside each branch so the common (non-redirect) index serve stays allocation-free. +>>>>>>> 24ef8e92d (perf(static): hoist query-string parse into redirect branches) + if (staticFile && req.pathname === '/' && baseURLPath !== '/') { + const originalPathname: string | undefined = (req as any).originalPathname; + if (originalPathname && !originalPathname.endsWith('/')) { + const queryIndex = (req.url as string).indexOf('?'); + const query = queryIndex === -1 ? '' : (req.url as string).slice(queryIndex); + return { + status: 301, + headers: { +<<<<<<< HEAD + Location: baseURLPath, +>>>>>>> 21d022b54 (Fix static plugin serving nothing when urlPath is configured (#1583)) +======= + Location: baseURLPath + query, +>>>>>>> aae9a110d (Preserve query strings on static trailing-slash redirects) }, }; } @@ -140,12 +169,26 @@ export function handleApplication(scope: Scope) { // prefix stripped, so rebuild the external path for the Location header (#1583) if (staticFile === null) { const externalPath = baseURLPath === '/' ? req.pathname : baseURLPath.slice(0, -1) + req.pathname; +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 24ef8e92d (perf(static): hoist query-string parse into redirect branches) const queryIndex = (req.url as string).indexOf('?'); const query = queryIndex === -1 ? '' : (req.url as string).slice(queryIndex); return { status: 301, headers: { Location: externalPath + '/' + query, +======= + return { + status: 301, + headers: { +<<<<<<< HEAD + Location: externalPath + '/', +>>>>>>> 21d022b54 (Fix static plugin serving nothing when urlPath is configured (#1583)) +======= + Location: externalPath + '/' + query, +>>>>>>> aae9a110d (Preserve query strings on static trailing-slash redirects) }, }; } diff --git a/unitTests/server/middlewareChain.test.js b/unitTests/server/middlewareChain.test.js index b91a3c344..bc016d697 100644 --- a/unitTests/server/middlewareChain.test.js +++ b/unitTests/server/middlewareChain.test.js @@ -697,6 +697,24 @@ describe('matchesRoute with slash-less urlPath', () => { assert.strictEqual(matchesRoute(req('/assets'), { urlPath: 'assets' }), true); assert.strictEqual(matchesRoute(req('/assets2/x'), { urlPath: 'assets' }), false); }); +<<<<<<< HEAD +}); + +// --------------------------------------------------------------------------- +// stripPrefix originalPathname passthrough (#1583) +// --------------------------------------------------------------------------- + +describe('stripPrefix originalPathname', () => { + it('exposes the unstripped pathname so handlers can distinguish /mount from /mount/', () => { + const noSlash = stripPrefix(req('/assets'), '/assets'); + assert.strictEqual(noSlash.pathname, '/'); + assert.strictEqual(noSlash.originalPathname, '/assets'); + const withSlash = stripPrefix(req('/assets/'), '/assets'); + assert.strictEqual(withSlash.pathname, '/'); + assert.strictEqual(withSlash.originalPathname, '/assets/'); + }); +======= +>>>>>>> 21d022b54 (Fix static plugin serving nothing when urlPath is configured (#1583)) }); // ---------------------------------------------------------------------------