0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-24 23:21:57 -05:00
astro/packages/integrations/node/src/serve-static.ts
Emanuele Stoppa 9680cf2780 [ci] format
2024-01-17 13:11:46 +00:00

86 lines
2.9 KiB
TypeScript

import path from 'node:path';
import url from 'node:url';
import send from 'send';
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { Options } from './types.js';
import type { NodeApp } from 'astro/app/node';
/**
* Creates a Node.js http listener for static files and prerendered pages.
* In standalone mode, the static handler is queried first for the static files.
* If one matching the request path is not found, it relegates to the SSR handler.
* Intended to be used only in the standalone mode.
*/
export function createStaticHandler(app: NodeApp, options: Options) {
const client = resolveClientDir(options);
/**
* @param ssr The SSR handler to be called if the static handler does not find a matching file.
*/
return (req: IncomingMessage, res: ServerResponse, ssr: () => unknown) => {
if (req.url) {
let pathname = app.removeBase(req.url);
pathname = decodeURI(new URL(pathname, 'http://host').pathname);
const stream = send(req, pathname, {
root: client,
dotfiles: pathname.startsWith('/.well-known/') ? 'allow' : 'deny',
});
let forwardError = false;
stream.on('error', (err) => {
if (forwardError) {
console.error(err.toString());
res.writeHead(500);
res.end('Internal server error');
return;
}
// File not found, forward to the SSR handler
ssr();
});
stream.on('headers', (_res: ServerResponse) => {
// assets in dist/_astro are hashed and should get the immutable header
if (pathname.startsWith(`/${options.assets}/`)) {
// This is the "far future" cache header, used for static files whose name includes their digest hash.
// 1 year (31,536,000 seconds) is convention.
// Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable
_res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
});
stream.on('directory', () => {
// On directory find, redirect to the trailing slash
let location: string;
if (req.url!.includes('?')) {
const [url1 = '', search] = req.url!.split('?');
location = `${url1}/?${search}`;
} else {
location = appendForwardSlash(req.url!);
}
res.statusCode = 301;
res.setHeader('Location', location);
res.end(location);
});
stream.on('file', () => {
forwardError = true;
});
stream.pipe(res);
} else {
ssr();
}
};
}
function resolveClientDir(options: Options) {
const clientURLRaw = new URL(options.client);
const serverURLRaw = new URL(options.server);
const rel = path.relative(url.fileURLToPath(serverURLRaw), url.fileURLToPath(clientURLRaw));
const serverEntryURL = new URL(import.meta.url);
const clientURL = new URL(appendForwardSlash(rel), serverEntryURL);
const client = url.fileURLToPath(clientURL);
return client;
}
function appendForwardSlash(pth: string) {
return pth.endsWith('/') ? pth : pth + '/';
}