From fa1c54908f5ae47d75357be95859070f07bcd553 Mon Sep 17 00:00:00 2001 From: Arsh <69170106+lilnasy@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:00:28 +0000 Subject: [PATCH] fix: improve static server reliability --- package/src/server/index.ts | 69 +++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/package/src/server/index.ts b/package/src/server/index.ts index 642a2dd..1ced1ec 100644 --- a/package/src/server/index.ts +++ b/package/src/server/index.ts @@ -1,5 +1,7 @@ /// - +import path, { relative } from 'node:path'; +import url from 'node:url'; +import { readdir } from 'node:fs/promises'; import cluster from 'node:cluster'; import os from 'node:os'; @@ -78,16 +80,25 @@ function handler( const app = new App(manifest); - return (req: Request, server: Server): Promise => { + // The dist may be copied somewhere after building. + // The build environment's full client path (options.client) can't be relied on in production. + // `resolveClientDir()` finds the full path to the client directory in the current environment + const clientDir = resolveClientDir(options); + + const clientAssetsPromise = getStaticAssets(clientDir); + let clientAssets: Awaited | undefined; + + return async (req: Request, server: Server): Promise => { const routeData = app.match(req); if (!routeData) { const url = new URL(req.url); - - const manifestAssetExists = manifest.assets.has(url.pathname); + const staticAssetExists = (clientAssets ??= await clientAssetsPromise).has( + url.pathname, + ); // If the manifest asset doesn't exist, or the request url ends with a slash // we should serve the index.html file from the respective directory. - if (!manifestAssetExists || req.url.endsWith('/')) { + if (!staticAssetExists || req.url.endsWith('/')) { const localPath = new URL( `./${app.removeBase(url.pathname)}/index.html`, clientRoot, @@ -96,7 +107,7 @@ function handler( } // Otherwise we attempt to serve the static asset from the client directory. - if (manifestAssetExists) { + if (staticAssetExists) { const localPath = new URL(app.removeBase(url.pathname), clientRoot); return serveStaticFile(url.pathname, localPath, clientRoot, options); } @@ -109,3 +120,49 @@ function handler( }); }; } + +async function getStaticAssets(clientDir: string) { + const dirEntries = await readdir(clientDir, { withFileTypes: true, recursive: true }); + const publicPath = new Set(); + for (const entry of dirEntries) { + if (entry.isFile() == false) continue; + publicPath.add( + prependForwardSlash(path.relative(clientDir, entry.parentPath) + '/' + entry.name), + ); + } + return publicPath; +} + +/** + * From https://github.com/withastro/adapters/blob/@astrojs/node@9.0.0/packages/node/src/serve-static.ts#L109-L125 + * + * Copyright of withastro/adapters contributors, Reproduced under MIT License + */ +function resolveClientDir(options: InternalOptions) { + const clientURLRaw = new URL(options.client); + const serverURLRaw = new URL(options.server); + const rel = path.relative( + url.fileURLToPath(serverURLRaw), + url.fileURLToPath(clientURLRaw), + ); + + // walk up the parent folders until you find the one that is the root of the server entry folder. This is how we find the client folder relatively. + const serverFolder = path.basename(options.server); + let serverEntryFolderURL = path.dirname(import.meta.url); + while (!serverEntryFolderURL.endsWith(serverFolder)) { + serverEntryFolderURL = path.dirname(serverEntryFolderURL); + } + + const serverEntryURL = serverEntryFolderURL + '/entry.mjs'; + const clientURL = new URL(appendForwardSlash(rel), serverEntryURL); + const client = url.fileURLToPath(clientURL); + return client; +} + +function prependForwardSlash(pth: string) { + return pth.startsWith('/') ? pth : '/' + pth; +} + +function appendForwardSlash(pth: string) { + return pth.endsWith('/') ? pth : pth + '/'; +}