diff options
author | 2024-01-25 11:23:27 +0100 | |
---|---|---|
committer | 2024-01-25 10:23:27 +0000 | |
commit | f7f19bfa5b6b8d41a7ba3884d455961e817ec676 (patch) | |
tree | c3c13fbb65cb4a290857780af582f558293ccadd /packages/integrations/node/src | |
parent | 1901ed3ef5e798dd3b69e1b6c05db94b6a471ad2 (diff) | |
download | astro-f7f19bfa5b6b8d41a7ba3884d455961e817ec676.tar.gz astro-f7f19bfa5b6b8d41a7ba3884d455961e817ec676.tar.zst astro-f7f19bfa5b6b8d41a7ba3884d455961e817ec676.zip |
feat(node): add trailingSlash support (#9080)
* feat(node): add trailing slash support
* add changeset
* test(node): add base route test in trailing-slash.js
detected an infinite loop in base path when trailingSlash: never
* fix(node): avoid infinite redirect when trailingSlash: never
* address test failures after rebase pt.1
* address test failures after rebase pt.2
---------
Co-authored-by: lilnasy <69170106+lilnasy@users.noreply.github.com>
Diffstat (limited to 'packages/integrations/node/src')
-rw-r--r-- | packages/integrations/node/src/serve-static.ts | 62 | ||||
-rw-r--r-- | packages/integrations/node/src/server.ts | 1 | ||||
-rw-r--r-- | packages/integrations/node/src/types.ts | 2 |
3 files changed, 49 insertions, 16 deletions
diff --git a/packages/integrations/node/src/serve-static.ts b/packages/integrations/node/src/serve-static.ts index 77de9b358..a88b1332f 100644 --- a/packages/integrations/node/src/serve-static.ts +++ b/packages/integrations/node/src/serve-static.ts @@ -1,5 +1,6 @@ import path from 'node:path'; import url from 'node:url'; +import fs from 'node:fs'; import send from 'send'; import type { IncomingMessage, ServerResponse } from 'node:http'; import type { Options } from './types.js'; @@ -18,8 +19,47 @@ export function createStaticHandler(app: NodeApp, options: Options) { */ return (req: IncomingMessage, res: ServerResponse, ssr: () => unknown) => { if (req.url) { - let pathname = app.removeBase(req.url); - pathname = decodeURI(new URL(pathname, 'http://host').pathname); + const [urlPath, urlQuery] = req.url.split('?'); + const filePath = path.join(client, app.removeBase(urlPath)); + + let pathname: string; + let isDirectory = false; + try { + isDirectory = fs.lstatSync(filePath).isDirectory(); + } catch {} + + const { trailingSlash = 'ignore' } = options; + + const hasSlash = urlPath.endsWith('/'); + switch (trailingSlash) { + case "never": + if (isDirectory && (urlPath != '/') && hasSlash) { + pathname = urlPath.slice(0, -1) + (urlQuery ? "?" + urlQuery : ""); + res.statusCode = 301; + res.setHeader('Location', pathname); + return res.end(); + } else pathname = urlPath; + // intentionally fall through + case "ignore": + { + if (isDirectory && !hasSlash) { + pathname = urlPath + "/index.html"; + } else + pathname = urlPath; + } + break; + case "always": + if (!hasSlash) { + pathname = urlPath + '/' +(urlQuery ? "?" + urlQuery : ""); + res.statusCode = 301; + res.setHeader('Location', pathname); + return res.end(); + } else + pathname = urlPath; + break; + } + // app.removeBase sometimes returns a path without a leading slash + pathname = prependForwardSlash(app.removeBase(pathname)); const stream = send(req, pathname, { root: client, @@ -47,20 +87,6 @@ export function createStaticHandler(app: NodeApp, options: Options) { _res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); } }); - stream.on('directory', () => { - // On directory find, redirect to the trailing slash - let location: string; - if (req.url!.includes('?')) { - const [url1 = '', search] = req.url!.split('?'); - location = `${url1}/?${search}`; - } else { - location = appendForwardSlash(req.url!); - } - - res.statusCode = 301; - res.setHeader('Location', location); - res.end(location); - }); stream.on('file', () => { forwardError = true; }); @@ -81,6 +107,10 @@ function resolveClientDir(options: Options) { return client; } +function prependForwardSlash(pth: string) { + return pth.startsWith('/') ? pth : '/' + pth; +} + function appendForwardSlash(pth: string) { return pth.endsWith('/') ? pth : pth + '/'; } diff --git a/packages/integrations/node/src/server.ts b/packages/integrations/node/src/server.ts index d9f24cca5..73b59c53f 100644 --- a/packages/integrations/node/src/server.ts +++ b/packages/integrations/node/src/server.ts @@ -8,6 +8,7 @@ import type { Options } from './types.js'; applyPolyfills(); export function createExports(manifest: SSRManifest, options: Options) { const app = new NodeApp(manifest); + options.trailingSlash = manifest.trailingSlash; return { options: options, handler: diff --git a/packages/integrations/node/src/types.ts b/packages/integrations/node/src/types.ts index 9e4f4ce91..3c03dffac 100644 --- a/packages/integrations/node/src/types.ts +++ b/packages/integrations/node/src/types.ts @@ -1,5 +1,6 @@ import type { NodeApp } from 'astro/app/node'; import type { IncomingMessage, ServerResponse } from 'node:http'; +import type { SSRManifest } from 'astro'; export interface UserOptions { /** @@ -17,6 +18,7 @@ export interface Options extends UserOptions { server: string; client: string; assets: string; + trailingSlash?: SSRManifest['trailingSlash']; } export interface CreateServerOptions { |