alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
191 lines (190 loc) • 6.35 kB
JavaScript
import { $hook, $inject, $module, Alepha, KIND, Primitive, createPrimitive } from "alepha";
import { AlephaServer, ServerRouterProvider } from "alepha/server";
import { createReadStream } from "node:fs";
import { access, readdir, stat } from "node:fs/promises";
import { basename, isAbsolute, join, sep } from "node:path";
import { DateTimeProvider } from "alepha/datetime";
import { $logger } from "alepha/logger";
import { FileDetector } from "alepha/system";
//#region ../../src/server/static/primitives/$serve.ts
/**
* Create a new static file handler.
*/
const $serve = (options = {}) => {
return createPrimitive(ServePrimitive, options);
};
var ServePrimitive = class extends Primitive {};
$serve[KIND] = ServePrimitive;
//#endregion
//#region ../../src/server/static/providers/ServerStaticProvider.ts
var ServerStaticProvider = class {
alepha = $inject(Alepha);
routerProvider = $inject(ServerRouterProvider);
dateTimeProvider = $inject(DateTimeProvider);
fileDetector = $inject(FileDetector);
log = $logger();
directories = [];
configure = $hook({
on: "configure",
handler: async () => {
await Promise.all(this.alepha.primitives($serve).map((it) => this.createStaticServer(it.options)));
}
});
async createStaticServer(options) {
const prefix = options.path ?? "/";
let root = options.root ?? process.cwd();
if (!isAbsolute(root)) root = join(process.cwd(), root);
this.log.debug("Serve static files", {
prefix,
root
});
await stat(root);
const files = await this.getAllFiles(root, options.ignoreDotEnvFiles);
const routes = await Promise.all(files.map(async (file) => {
const urlPath = file.replace(root, "").replace(/\\/g, "/");
const routePath = `${prefix}${encodeURI(urlPath)}`.replace(/\/+/g, "/");
const filePath = join(root, urlPath.replace(/\//g, sep));
this.log.trace(`Mount ${routePath} -> ${filePath}`);
return {
silent: options.silent,
path: routePath,
handler: await this.createFileHandler(filePath, options)
};
}));
for (const route of routes) {
this.routerProvider.createRoute(route);
if (options.indexFallback !== false && route.path.endsWith("index.html")) this.routerProvider.createRoute({
silent: options.silent,
path: route.path.replace(/index\.html$/, ""),
handler: route.handler
});
}
this.directories.push({
options,
files: files.map((file) => file.replace(root, "").replace(/\\/g, "/"))
});
if (options.historyApiFallback) this.routerProvider.createRoute({
silent: options.silent,
path: join(prefix, "*").replace(/\\/g, "/"),
handler: async (request) => {
const { reply } = request;
if (request.url.pathname.includes(".")) {
reply.headers["content-type"] = "text/plain";
reply.body = "Not Found";
reply.status = 404;
return;
}
reply.headers["content-type"] = "text/html";
reply.status = 200;
return new Promise((resolve, reject) => {
const stream = createReadStream(join(root, "index.html"));
stream.on("open", () => {
resolve(stream);
});
stream.on("error", (err) => {
reject(err);
});
});
}
});
}
async createFileHandler(filepath, options) {
const filename = basename(filepath);
const hasGzip = await access(`${filepath}.gz`).then(() => true).catch(() => false);
const hasBr = await access(`${filepath}.br`).then(() => true).catch(() => false);
const fileStat = await stat(filepath);
const lastModified = fileStat.mtime.toUTCString();
const etag = `"${fileStat.size}-${fileStat.mtime.getTime()}"`;
const contentType = this.fileDetector.getContentType(filename);
const cacheControl = this.getCacheControl(filename, options);
return async (request) => {
const { headers, reply } = request;
let path = filepath;
if (options.path && options.path === request.url.pathname && !options.path.endsWith("/")) {
reply.redirect(`${options.path}/`, 301);
return;
}
const encoding = headers["accept-encoding"];
if (encoding) {
if (hasBr && encoding.includes("br")) {
reply.headers["content-encoding"] = "br";
path += ".br";
} else if (hasGzip && encoding.includes("gzip")) {
reply.headers["content-encoding"] = "gzip";
path += ".gz";
}
}
reply.headers["content-type"] = contentType;
reply.headers["accept-ranges"] = "bytes";
reply.headers["last-modified"] = lastModified;
if (cacheControl) {
reply.headers["cache-control"] = `public, max-age=${cacheControl.maxAge}`;
if (cacheControl.immutable) reply.headers["cache-control"] += ", immutable";
}
reply.headers.etag = etag;
if (headers["if-none-match"] === etag || headers["if-modified-since"] === lastModified) {
reply.status = 304;
return;
}
return new Promise((resolve, reject) => {
const stream = createReadStream(path);
stream.on("open", () => {
resolve(stream);
});
stream.on("error", (err) => {
reject(err);
});
});
};
}
getCacheFileTypes() {
return [
".js",
".css",
".woff",
".woff2",
".ttf",
".eot",
".otf",
".jpg",
".jpeg",
".png",
".svg",
".gif"
];
}
getCacheControl(filename, options) {
if (!options.cacheControl) return;
const fileTypes = options.cacheControl.fileTypes ?? this.getCacheFileTypes();
for (const type of fileTypes) if (filename.endsWith(type)) return {
immutable: options.cacheControl.immutable ?? true,
maxAge: this.dateTimeProvider.duration(options.cacheControl.maxAge ?? [30, "days"]).as("seconds")
};
}
async getAllFiles(dir, ignoreDotEnvFiles = true) {
const entries = await readdir(dir, { withFileTypes: true });
return (await Promise.all(entries.map((dirent) => {
if (ignoreDotEnvFiles && dirent.name.startsWith(".")) return [];
const fullPath = join(dir, dirent.name);
return dirent.isDirectory() ? this.getAllFiles(fullPath) : fullPath;
}))).flat();
}
};
//#endregion
//#region ../../src/server/static/index.ts
/**
* Static file serving.
*
* **Features:**
* - Serve static files from directory
*
* @module alepha.server.static
*/
const AlephaServerStatic = $module({
name: "alepha.server.static",
primitives: [$serve],
services: [AlephaServer, ServerStaticProvider]
});
//#endregion
export { $serve, AlephaServerStatic, ServePrimitive, ServerStaticProvider };
//# sourceMappingURL=index.js.map