Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Improve Static Server Reliability #11

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 64 additions & 6 deletions package/src/server/index.ts
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';

Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
Expand All @@ -109,3 +120,50 @@ function handler(
});
};
}

async function getStaticAssets(clientDir: string) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async function getStaticAssets(clientDir: string) {
async function getStaticAssets(clientDir: string): Promise<Set<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
*/
// @ts-expect-error client and server fields are always present
function resolveClientDir(options: InternalOptions) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function resolveClientDir(options: InternalOptions) {
function resolveClientDir(options: InternalOptions): string {

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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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.
// 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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function prependForwardSlash(pth: string) {
function prependForwardSlash(pth: string): string {

return pth.startsWith('/') ? pth : '/' + pth;
}

function appendForwardSlash(pth: string) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function appendForwardSlash(pth: string) {
function appendForwardSlash(pth: string): string {

return pth.endsWith('/') ? pth : pth + '/';
}