-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: improve static server reliability
- Loading branch information
Showing
1 changed file
with
63 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
/// <reference types="astro/client" /> | ||
|
||
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<Response> => { | ||
// 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<typeof clientAssetsPromise> | undefined; | ||
|
||
return async (req: Request, server: Server): Promise<Response> => { | ||
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<string>(); | ||
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/[email protected]/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 + '/'; | ||
} |