feat(server): serve static assets without bun
This commit is contained in:
parent
55383cef3e
commit
837c87fba4
4 changed files with 130 additions and 29 deletions
|
@ -18,7 +18,6 @@
|
|||
"@elysiajs/html": "^1.3.1",
|
||||
"@elysiajs/node": "^1.4.1",
|
||||
"@elysiajs/openapi": "^1.4.11",
|
||||
"@elysiajs/static": "^1.3.0",
|
||||
"@std/async": "npm:@jsr/std__async@^1.0.13",
|
||||
"@zip.js/zip.js": "^2.7.62",
|
||||
"better-sqlite3": "^9.6.0",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Elysia, t } from "elysia";
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { staticPlugin } from "@elysiajs/static";
|
||||
import { staticPlugin } from "./util/static.ts";
|
||||
import { html } from "@elysiajs/html";
|
||||
|
||||
import { connectDB } from "./database.ts";
|
||||
|
|
129
packages/server/src/util/static.ts
Normal file
129
packages/server/src/util/static.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
import { Elysia, NotFoundError, type Context } from "elysia";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { extname, resolve } from "node:path";
|
||||
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
html: "text/html; charset=utf-8",
|
||||
css: "text/css; charset=utf-8",
|
||||
js: "application/javascript; charset=utf-8",
|
||||
mjs: "application/javascript; charset=utf-8",
|
||||
map: "application/json; charset=utf-8",
|
||||
json: "application/json; charset=utf-8",
|
||||
txt: "text/plain; charset=utf-8",
|
||||
svg: "image/svg+xml",
|
||||
png: "image/png",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
gif: "image/gif",
|
||||
webp: "image/webp",
|
||||
avif: "image/avif",
|
||||
ico: "image/x-icon",
|
||||
woff: "font/woff",
|
||||
woff2: "font/woff2",
|
||||
ttf: "font/ttf",
|
||||
otf: "font/otf",
|
||||
};
|
||||
|
||||
const toPosix = (value: string) => value.replace(/\\/g, "/");
|
||||
|
||||
const isPathWithinRoot = (candidate: string, root: string) => {
|
||||
const normalizedCandidate = toPosix(resolve(candidate));
|
||||
const normalizedRoot = toPosix(resolve(root));
|
||||
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
|
||||
};
|
||||
|
||||
const getMimeType = (path: string) => {
|
||||
const ext = extname(path).slice(1).toLowerCase();
|
||||
return MIME_TYPES[ext] ?? "application/octet-stream";
|
||||
};
|
||||
|
||||
const generateETag = (mtimeMs: number, size: number) => `W/"${size.toString(16)}-${Math.round(mtimeMs).toString(16)}"`;
|
||||
|
||||
export type StaticPluginOptions = {
|
||||
assets: string;
|
||||
prefix?: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
const handleStaticRequest = async (
|
||||
rootDir: string,
|
||||
headersTemplate: Record<string, string>,
|
||||
ctx: Context,
|
||||
sendBody: boolean,
|
||||
) => {
|
||||
const wildcard = ctx.params?.["*"] ?? "";
|
||||
if (wildcard.length === 0) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
let decoded: string;
|
||||
try {
|
||||
decoded = decodeURI(wildcard);
|
||||
} catch {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
if (decoded.includes("\0")) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const absolutePath = resolve(rootDir, decoded);
|
||||
if (!isPathWithinRoot(absolutePath, rootDir)) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const fileStat = await stat(absolutePath).catch(() => undefined);
|
||||
if (!fileStat || fileStat.isDirectory()) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const responseHeaders: Record<string, string> = {
|
||||
...headersTemplate,
|
||||
"Last-Modified": fileStat.mtime.toUTCString(),
|
||||
};
|
||||
|
||||
const etag = generateETag(fileStat.mtimeMs, fileStat.size);
|
||||
responseHeaders.ETag = etag;
|
||||
|
||||
const ifNoneMatch = ctx.request.headers.get("if-none-match");
|
||||
if (ifNoneMatch && ifNoneMatch === etag) {
|
||||
ctx.set.status = 304;
|
||||
ctx.set.headers = responseHeaders;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ifModifiedSince = ctx.request.headers.get("if-modified-since");
|
||||
if (ifModifiedSince) {
|
||||
const since = new Date(ifModifiedSince);
|
||||
if (!Number.isNaN(since.getTime()) && fileStat.mtime <= since) {
|
||||
ctx.set.status = 304;
|
||||
ctx.set.headers = responseHeaders;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
responseHeaders["Content-Type"] = getMimeType(absolutePath);
|
||||
responseHeaders["Content-Length"] = fileStat.size.toString();
|
||||
|
||||
ctx.set.status = 200;
|
||||
ctx.set.headers = responseHeaders;
|
||||
|
||||
if (!sendBody) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return createReadStream(absolutePath);
|
||||
};
|
||||
|
||||
export const staticPlugin = ({ assets, prefix = "/public", headers = {} }: StaticPluginOptions) => {
|
||||
const trimmedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
||||
const normalizedPrefix = trimmedPrefix.startsWith("/") ? trimmedPrefix : `/${trimmedPrefix}`;
|
||||
const wildcardRoute = normalizedPrefix === "/" ? "/*" : `${normalizedPrefix}/*`;
|
||||
const rootDir = resolve(process.cwd(), assets);
|
||||
const headersTemplate = { ...headers };
|
||||
|
||||
return new Elysia({ name: "node-static" })
|
||||
.get(wildcardRoute, async (ctx) => handleStaticRequest(rootDir, headersTemplate, ctx, true))
|
||||
.head(wildcardRoute, async (ctx) => handleStaticRequest(rootDir, headersTemplate, ctx, false));
|
||||
};
|
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
|
@ -181,9 +181,6 @@ importers:
|
|||
'@elysiajs/openapi':
|
||||
specifier: ^1.4.11
|
||||
version: 1.4.11(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))
|
||||
'@elysiajs/static':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))
|
||||
'@std/async':
|
||||
specifier: npm:@jsr/std__async@^1.0.13
|
||||
version: '@jsr/std__async@1.0.13'
|
||||
|
@ -438,11 +435,6 @@ packages:
|
|||
peerDependencies:
|
||||
elysia: '>= 1.4.0'
|
||||
|
||||
'@elysiajs/static@1.3.0':
|
||||
resolution: {integrity: sha512-7mWlj2U/AZvH27IfRKqpUjDP1W9ZRldF9NmdnatFEtx0AOy7YYgyk0rt5hXrH6wPcR//2gO2Qy+k5rwswpEhJA==}
|
||||
peerDependencies:
|
||||
elysia: '>= 1.3.0'
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -1783,10 +1775,6 @@ packages:
|
|||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
clone@2.1.2:
|
||||
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -2622,10 +2610,6 @@ packages:
|
|||
resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
node-cache@5.1.2:
|
||||
resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
|
@ -3641,11 +3625,6 @@ snapshots:
|
|||
dependencies:
|
||||
elysia: 1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3)
|
||||
|
||||
'@elysiajs/static@1.3.0(elysia@1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3))':
|
||||
dependencies:
|
||||
elysia: 1.4.9(@sinclair/typebox@0.34.41)(exact-mirror@0.2.0(@sinclair/typebox@0.34.41))(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.8.3)
|
||||
node-cache: 5.1.2
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
optional: true
|
||||
|
||||
|
@ -4841,8 +4820,6 @@ snapshots:
|
|||
|
||||
clone@1.0.4: {}
|
||||
|
||||
clone@2.1.2: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
code-block-writer@12.0.0: {}
|
||||
|
@ -5604,10 +5581,6 @@ snapshots:
|
|||
dependencies:
|
||||
semver: 7.7.2
|
||||
|
||||
node-cache@5.1.2:
|
||||
dependencies:
|
||||
clone: 2.1.2
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@3.3.2:
|
||||
|
|
Loading…
Add table
Reference in a new issue