diff --git a/.changeset/warm-bananas-sneeze.md b/.changeset/warm-bananas-sneeze.md new file mode 100644 index 000000000000..11b8fd7b937e --- /dev/null +++ b/.changeset/warm-bananas-sneeze.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +fix: correctly redirect trailing slashes for `adapter-node` diff --git a/packages/adapter-node/ambient.d.ts b/packages/adapter-node/ambient.d.ts index c424cbb1c9f4..7d45ea6dc64a 100644 --- a/packages/adapter-node/ambient.d.ts +++ b/packages/adapter-node/ambient.d.ts @@ -8,7 +8,9 @@ declare module 'HANDLER' { declare module 'MANIFEST' { import { SSRManifest } from '@sveltejs/kit'; + export const manifest: SSRManifest; + export const prerendered: Set; } declare module 'SERVER' { diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index 7dc13e34d550..ac16008bc176 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -1,5 +1,5 @@ -import { readFileSync, writeFileSync } from 'fs'; -import { fileURLToPath } from 'url'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; import { rollup } from 'rollup'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; @@ -39,7 +39,8 @@ export default function (opts = {}) { writeFileSync( `${tmp}/manifest.js`, - `export const manifest = ${builder.generateManifest({ relativePath: './' })};` + `export const manifest = ${builder.generateManifest({ relativePath: './' })};\n\n` + + `export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n` ); const pkg = JSON.parse(readFileSync('package.json', 'utf8')); diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index 8166e654f328..650ab3ce6018 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -1,11 +1,11 @@ import './shims'; -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import sirv from 'sirv'; -import { fileURLToPath } from 'url'; +import { fileURLToPath } from 'node:url'; import { getRequest, setResponse } from '@sveltejs/kit/node'; import { Server } from 'SERVER'; -import { manifest } from 'MANIFEST'; +import { manifest, prerendered } from 'MANIFEST'; import { env } from 'ENV'; /* global ENV_PREFIX */ @@ -44,8 +44,39 @@ function serve(path, client = false) { ); } +// required because the static file server ignores trailing slashes +/** @returns {import('polka').Middleware} */ +function serve_prerendered() { + const handler = serve(path.join(dir, 'prerendered')); + + return (req, res, next) => { + let pathname = req.path; + + try { + pathname = decodeURIComponent(pathname); + } catch { + // ignore invalid URI + } + + if (prerendered.has(pathname)) { + return handler(req, res, next); + } + + // remove or add trailing slash as appropriate + let location = pathname.at(-1) === '/' ? pathname.slice(0, -1) : pathname + '/'; + if (prerendered.has(location)) { + const search = req.url.split('?')[1]; + if (search) location += `?${search}`; + res.writeHead(308, { location }).end(); + } else { + next(); + } + }; +} + /** @type {import('polka').Middleware} */ const ssr = async (req, res) => { + /** @type {Request | undefined} */ let request; try { @@ -139,7 +170,7 @@ export const handler = sequence( [ serve(path.join(dir, 'client'), true), serve(path.join(dir, 'static')), - serve(path.join(dir, 'prerendered')), + serve_prerendered(), ssr ].filter(Boolean) );