From 357921d2aa043f60b698706c121dc85324f4a0c1 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 5 Nov 2026 18:11:37 +0100 Subject: [PATCH] refactor(core): extract the serve request handler into its own module (AI-assisted) createServeRequestHandler({outDir, baseUrl, trailingSlash}) receives explicit args instead of closing over serve() variables, so it can be unit tested. The handler body is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/commands/serve/requestHandler.ts | 74 +++++++++++++++++++ .../docusaurus/src/commands/serve/serve.ts | 54 +------------- 2 files changed, 78 insertions(+), 50 deletions(-) create mode 100644 packages/docusaurus/src/commands/serve/requestHandler.ts diff --git a/packages/docusaurus/src/commands/serve/requestHandler.ts b/packages/docusaurus/src/commands/serve/requestHandler.ts new file mode 100644 index 000000000000..8437b0ccca2c --- /dev/null +++ b/packages/docusaurus/src/commands/serve/requestHandler.ts @@ -0,0 +1,74 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import serveHandler from 'serve-handler'; +import {applyTrailingSlash} from '@docusaurus/utils-common'; +import type http from 'node:http'; +import type {DocusaurusConfig} from '@docusaurus/types'; + +type ServeRequestHandler = ( + req: http.IncomingMessage, + res: http.ServerResponse, +) => Promise; + +export function createServeRequestHandler({ + outDir, + baseUrl, + trailingSlash, +}: {outDir: string} & Pick< + DocusaurusConfig, + 'baseUrl' | 'trailingSlash' +>): ServeRequestHandler { + return async (req, res) => { + // Automatically redirect requests to /baseUrl/ + if (!req.url?.startsWith(baseUrl)) { + redirect(res, baseUrl); + return; + } + + // We do the redirect ourselves for a good reason + // server-handler is annoying and won't include /baseUrl/ in redirects + // See https://github.com/meta/docusaurus/issues/10078#issuecomment-2084932934 + if (baseUrl !== '/') { + // Not super robust, but should be good enough for our use case + // See https://github.com/meta/docusaurus/pull/10090 + const looksLikeAsset = !!req.url.match(/\.[a-z\d]{1,4}$/i); + if (!looksLikeAsset) { + const normalizedUrl = applyTrailingSlash(req.url, { + trailingSlash, + baseUrl, + }); + if (req.url !== normalizedUrl) { + redirect(res, normalizedUrl); + return; + } + } + } + + // Remove baseUrl before calling serveHandler, because /baseUrl/ should + // serve /build/index.html, not /build/baseUrl/index.html (does not exist) + // Note server-handler is really annoying here: + // - no easy way to do rewrites such as "/baseUrl/:path" => "/:path" + // - no easy way to "reapply" the baseUrl to the redirect "Location" header + // See also https://github.com/meta/docusaurus/pull/10090 + req.url = req.url.replace(baseUrl, '/'); + + await serveHandler(req, res, { + cleanUrls: true, + public: outDir, + trailingSlash, + directoryListing: false, + }); + }; +} + +function redirect(res: http.ServerResponse, location: string) { + res.writeHead(302, { + Location: location, + }); + res.end(); +} diff --git a/packages/docusaurus/src/commands/serve/serve.ts b/packages/docusaurus/src/commands/serve/serve.ts index 09a4dfb92989..e60534c405df 100644 --- a/packages/docusaurus/src/commands/serve/serve.ts +++ b/packages/docusaurus/src/commands/serve/serve.ts @@ -9,23 +9,15 @@ import http from 'node:http'; import path from 'node:path'; import logger from '@docusaurus/logger'; import {DEFAULT_BUILD_DIR_NAME} from '@docusaurus/utils'; -import serveHandler from 'serve-handler'; -import {applyTrailingSlash} from '@docusaurus/utils-common'; import {realpath} from '@docusaurus/fs'; import openBrowser from '../utils/openBrowser/openBrowser'; import {loadSiteConfig} from '../../server/config'; import {build} from '../build/build'; import {getHostPort, type HostPortOptions} from '../../server/getHostPort'; import {listenToServer} from './listenToServer'; +import {createServeRequestHandler} from './requestHandler'; import type {LoadContextParams} from '../../server/site'; -function redirect(res: http.ServerResponse, location: string) { - res.writeHead(302, { - Location: location, - }); - res.end(); -} - export type ServeCLIOptions = HostPortOptions & Pick & { dir?: string; @@ -64,47 +56,9 @@ export async function serve( const servingUrl = `http://${host}:${port}`; - const server = http.createServer((req, res) => { - // Automatically redirect requests to /baseUrl/ - if (!req.url?.startsWith(baseUrl)) { - redirect(res, baseUrl); - return; - } - - // We do the redirect ourselves for a good reason - // server-handler is annoying and won't include /baseUrl/ in redirects - // See https://github.com/meta/docusaurus/issues/10078#issuecomment-2084932934 - if (baseUrl !== '/') { - // Not super robust, but should be good enough for our use case - // See https://github.com/meta/docusaurus/pull/10090 - const looksLikeAsset = !!req.url.match(/\.[a-z\d]{1,4}$/i); - if (!looksLikeAsset) { - const normalizedUrl = applyTrailingSlash(req.url, { - trailingSlash, - baseUrl, - }); - if (req.url !== normalizedUrl) { - redirect(res, normalizedUrl); - return; - } - } - } - - // Remove baseUrl before calling serveHandler, because /baseUrl/ should - // serve /build/index.html, not /build/baseUrl/index.html (does not exist) - // Note server-handler is really annoying here: - // - no easy way to do rewrites such as "/baseUrl/:path" => "/:path" - // - no easy way to "reapply" the baseUrl to the redirect "Location" header - // See also https://github.com/meta/docusaurus/pull/10090 - req.url = req.url.replace(baseUrl, '/'); - - serveHandler(req, res, { - cleanUrls: true, - public: outDir, - trailingSlash, - directoryListing: false, - }); - }); + const server = http.createServer( + createServeRequestHandler({outDir, baseUrl, trailingSlash}), + ); const url = servingUrl + baseUrl; logger.success`Serving path=${buildDir} directory at: url=${url}`;