jeasx 2.7.5 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.js +37 -16
- package/package.json +3 -3
- package/serverless.js +61 -38
- package/serverless.js.map +2 -2
- package/serverless.ts +95 -51
package/build.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as esbuild from "esbuild";
|
|
2
|
-
import { writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { glob, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, sep } from "node:path";
|
|
4
4
|
import env from "./env.js";
|
|
5
5
|
|
|
6
6
|
env();
|
|
@@ -54,22 +54,43 @@ const BROWSER_OPTIONS = {
|
|
|
54
54
|
...CONFIG.ESBUILD_BROWSER_OPTIONS?.(),
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
[SERVER_OPTIONS, BROWSER_OPTIONS]
|
|
57
|
+
for (const options of [SERVER_OPTIONS, BROWSER_OPTIONS]) {
|
|
58
58
|
if (NODE_ENV_IS_DEVELOPMENT) {
|
|
59
59
|
(await esbuild.context(options)).watch();
|
|
60
60
|
} else {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
await esbuild.build(options);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Export path mappings for routes and files
|
|
66
|
+
if (!NODE_ENV_IS_DEVELOPMENT) {
|
|
67
|
+
/** @type Record<string,string> */
|
|
68
|
+
const routes = {};
|
|
69
|
+
/** @type Record<string,string> */
|
|
70
|
+
const files = {};
|
|
71
|
+
|
|
72
|
+
for await (const entry of glob("{dist,public}/**/*")) {
|
|
73
|
+
const path = entry.split(sep).join("/");
|
|
74
|
+
|
|
75
|
+
// Handle server routes.
|
|
76
|
+
if (/^dist\/.*\[.+\]\.js$/.test(path)) {
|
|
77
|
+
routes[path.slice("dist".length, -".js".length)] = path;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Skip sourcemaps for server routes.
|
|
82
|
+
if (/^dist\/.*\[.+\]\.js\.map$/.test(path)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Treat all other entries as static files.
|
|
87
|
+
if ((await stat(join(CWD, path))).isFile()) {
|
|
88
|
+
files[path.slice(path.indexOf("/"))] = path;
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
|
-
|
|
91
|
+
|
|
92
|
+
await writeFile(
|
|
93
|
+
join(CWD, "dist", "[--metadata--].js"),
|
|
94
|
+
`export default ${JSON.stringify({ routes, files })};`,
|
|
95
|
+
);
|
|
96
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jeasx",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Jeasx - the ease of JSX with the power of SSR",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"async",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"@fastify/cookie": "11.0.2",
|
|
33
33
|
"@fastify/formbody": "8.0.2",
|
|
34
34
|
"@fastify/multipart": "10.0.0",
|
|
35
|
-
"@fastify/
|
|
36
|
-
"@types/node": "26.0
|
|
35
|
+
"@fastify/send": "4.1.0",
|
|
36
|
+
"@types/node": "26.1.0",
|
|
37
37
|
"esbuild": "0.28.1",
|
|
38
38
|
"fastify": "5.9.0",
|
|
39
39
|
"jsx-async-runtime": "2.1.3"
|
package/serverless.js
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import fastifyCookie from "@fastify/cookie";
|
|
2
2
|
import fastifyFormbody from "@fastify/formbody";
|
|
3
3
|
import fastifyMultipart from "@fastify/multipart";
|
|
4
|
-
import
|
|
4
|
+
import fastifySend from "@fastify/send";
|
|
5
5
|
import fastify from "fastify";
|
|
6
6
|
import { jsxToString } from "jsx-async-runtime";
|
|
7
7
|
import { stat } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { Readable } from "node:stream";
|
|
9
10
|
import env from "./env.js";
|
|
10
11
|
env();
|
|
11
12
|
const CWD = process.cwd();
|
|
12
13
|
const CONFIG = (await import(`file://${join(CWD, "jeasx.config.js")}`)).default;
|
|
13
14
|
const NODE_ENV_IS_DEVELOPMENT = process.env.NODE_ENV === "development";
|
|
14
|
-
const MODULE_BY_ROUTE = {};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
MODULE_BY_ROUTE[route] = join(CWD, "dist", `${route}.js`);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
15
|
+
const { routes: MODULE_BY_ROUTE, files: FILE_BY_PATH } = NODE_ENV_IS_DEVELOPMENT ? { routes: {}, files: {} } : (await import(`file://${join(CWD, "dist", "[--metadata--].js")}`)).default;
|
|
16
|
+
const FASTIFY_SEND_OPTIONS = {
|
|
17
|
+
...CONFIG.FASTIFY_SEND_OPTIONS?.()
|
|
18
|
+
};
|
|
21
19
|
const FASTIFY_SERVER = CONFIG.FASTIFY_SERVER ?? ((fastify2) => fastify2);
|
|
22
20
|
var serverless_default = FASTIFY_SERVER(
|
|
23
21
|
fastify({
|
|
@@ -30,11 +28,6 @@ var serverless_default = FASTIFY_SERVER(
|
|
|
30
28
|
...CONFIG.FASTIFY_FORMBODY_OPTIONS?.()
|
|
31
29
|
}).register(fastifyMultipart, {
|
|
32
30
|
...CONFIG.FASTIFY_MULTIPART_OPTIONS?.()
|
|
33
|
-
}).register(fastifyStatic, {
|
|
34
|
-
root: ["public", "dist"].map((dir) => join(CWD, dir)),
|
|
35
|
-
wildcard: false,
|
|
36
|
-
globIgnore: ["/**/\\[*\\].js?(.map)"],
|
|
37
|
-
...CONFIG.FASTIFY_STATIC_OPTIONS?.()
|
|
38
31
|
}).decorateRequest("route", "").decorateRequest("path", "").addHook("onRequest", async (request) => {
|
|
39
32
|
const index = request.url.indexOf("?");
|
|
40
33
|
request.path = index === -1 ? request.url : request.url.slice(0, index);
|
|
@@ -57,13 +50,23 @@ async function handler(request, reply) {
|
|
|
57
50
|
const props = { request, reply };
|
|
58
51
|
try {
|
|
59
52
|
for (const route of generateRoutes(request.path)) {
|
|
53
|
+
if (route === request.path) {
|
|
54
|
+
const sendResult = await tryFile(request);
|
|
55
|
+
if (sendResult) {
|
|
56
|
+
reply.status(sendResult.statusCode);
|
|
57
|
+
reply.headers(sendResult.headers);
|
|
58
|
+
response = sendResult.stream;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
60
63
|
let module = MODULE_BY_ROUTE[route];
|
|
61
64
|
if (module === void 0 && !NODE_ENV_IS_DEVELOPMENT) {
|
|
62
65
|
continue;
|
|
63
66
|
}
|
|
64
67
|
try {
|
|
65
68
|
if (typeof module === "string") {
|
|
66
|
-
module = MODULE_BY_ROUTE[route] = await import(`file://${module}`);
|
|
69
|
+
module = MODULE_BY_ROUTE[route] = await import(`file://${join(CWD, module)}`);
|
|
67
70
|
} else if (module === void 0 && NODE_ENV_IS_DEVELOPMENT) {
|
|
68
71
|
const modulePath = join(CWD, "dist", `${route}.js`);
|
|
69
72
|
if (typeof require === "function" && require.cache[modulePath]) {
|
|
@@ -82,17 +85,16 @@ async function handler(request, reply) {
|
|
|
82
85
|
throw e;
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
|
-
if (!module || typeof module !== "object") {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
88
|
request.route = route;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
if (module && typeof module === "object") {
|
|
90
|
+
response = typeof module.default === "function" ? (
|
|
91
|
+
// Call functions with context as `this` and props as parameters,
|
|
92
|
+
await module.default.call(context, props)
|
|
93
|
+
) : (
|
|
94
|
+
// otherwise return default export.
|
|
95
|
+
module.default
|
|
96
|
+
);
|
|
97
|
+
}
|
|
96
98
|
if (reply.sent) {
|
|
97
99
|
return;
|
|
98
100
|
} else if (route.endsWith("/[404]")) {
|
|
@@ -100,24 +102,24 @@ async function handler(request, reply) {
|
|
|
100
102
|
reply.status(404);
|
|
101
103
|
}
|
|
102
104
|
break;
|
|
103
|
-
} else if (typeof response === "string" || Buffer.isBuffer(response) || isJSX(response)) {
|
|
105
|
+
} else if (typeof response === "string" || response instanceof Readable || Buffer.isBuffer(response) || isJSX(response)) {
|
|
104
106
|
break;
|
|
105
|
-
} else if (route.endsWith("/[...guard]") &&
|
|
107
|
+
} else if (route.endsWith("/[...guard]") && typeof response === "object") {
|
|
106
108
|
Object.assign(props, response);
|
|
107
109
|
continue;
|
|
108
|
-
} else if (reply.statusCode === 404) {
|
|
110
|
+
} else if (response === void 0 || reply.statusCode === 404) {
|
|
109
111
|
continue;
|
|
110
112
|
} else {
|
|
111
113
|
break;
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
|
-
return await
|
|
116
|
+
return await renderResponse(context, response);
|
|
115
117
|
} catch (error) {
|
|
116
118
|
const errorHandler = context["errorHandler"];
|
|
117
119
|
if (typeof errorHandler === "function") {
|
|
118
120
|
reply.status(500);
|
|
119
121
|
response = await errorHandler.call(context, error);
|
|
120
|
-
return await
|
|
122
|
+
return await renderResponse(context, response);
|
|
121
123
|
} else {
|
|
122
124
|
throw error;
|
|
123
125
|
}
|
|
@@ -126,27 +128,26 @@ async function handler(request, reply) {
|
|
|
126
128
|
function generateRoutes(path) {
|
|
127
129
|
const routes = [];
|
|
128
130
|
const segments = [""];
|
|
129
|
-
let
|
|
131
|
+
let edgeSegment = "";
|
|
130
132
|
for (const segment of path.split("/")) {
|
|
131
133
|
if (segment !== "") {
|
|
132
|
-
|
|
133
|
-
segments.push(
|
|
134
|
+
edgeSegment += `/${segment}`;
|
|
135
|
+
segments.push(edgeSegment);
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
|
-
segments.
|
|
137
|
-
for (let i = segments.length - 1; i >= 0; i--) {
|
|
138
|
+
for (let i = 0; i < segments.length; i++) {
|
|
138
139
|
routes.push(`${segments[i]}/[...guard]`);
|
|
139
140
|
}
|
|
140
|
-
|
|
141
|
+
routes.push(path);
|
|
141
142
|
const lastSlash = edgeSegment.lastIndexOf("/") + 1;
|
|
142
143
|
if (lastSlash > 0) {
|
|
143
144
|
routes.push(`${edgeSegment.substring(0, lastSlash)}[${edgeSegment.substring(lastSlash)}]`);
|
|
144
145
|
}
|
|
145
146
|
routes.push(`${edgeSegment}/[index]`);
|
|
146
|
-
for (let i =
|
|
147
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
147
148
|
routes.push(`${segments[i]}/[...path]`);
|
|
148
149
|
}
|
|
149
|
-
for (let i =
|
|
150
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
150
151
|
routes.push(`${segments[i]}/[404]`);
|
|
151
152
|
}
|
|
152
153
|
return routes;
|
|
@@ -154,11 +155,33 @@ function generateRoutes(path) {
|
|
|
154
155
|
function isJSX(obj) {
|
|
155
156
|
return !!obj && typeof obj === "object" && "type" in obj && "props" in obj;
|
|
156
157
|
}
|
|
157
|
-
async function
|
|
158
|
+
async function renderResponse(context, response) {
|
|
158
159
|
const payload = isJSX(response) ? await jsxToString.call(context, response) : response;
|
|
159
160
|
const responseHandler = context["responseHandler"];
|
|
160
161
|
return typeof responseHandler === "function" ? await responseHandler.call(context, payload) : payload;
|
|
161
162
|
}
|
|
163
|
+
async function tryFile(request) {
|
|
164
|
+
const file = FILE_BY_PATH[request.path];
|
|
165
|
+
if (file) {
|
|
166
|
+
return await fastifySend(request.raw, file, FASTIFY_SEND_OPTIONS);
|
|
167
|
+
}
|
|
168
|
+
if (NODE_ENV_IS_DEVELOPMENT) {
|
|
169
|
+
for (const directory of ["dist", "public"]) {
|
|
170
|
+
try {
|
|
171
|
+
if ((await stat(join(CWD, directory, request.path))).isFile()) {
|
|
172
|
+
return await fastifySend(
|
|
173
|
+
request.raw,
|
|
174
|
+
`${directory}${request.path}`,
|
|
175
|
+
FASTIFY_SEND_OPTIONS
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return void 0;
|
|
184
|
+
}
|
|
162
185
|
export {
|
|
163
186
|
serverless_default as default
|
|
164
187
|
};
|
package/serverless.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["serverless.ts"],
|
|
4
|
-
"sourcesContent": ["import fastifyCookie, { FastifyCookieOptions } from \"@fastify/cookie\";\nimport fastifyFormbody, { FastifyFormbodyOptions } from \"@fastify/formbody\";\nimport fastifyMultipart, { FastifyMultipartOptions } from \"@fastify/multipart\";\nimport fastifyStatic, { FastifyStaticOptions } from \"@fastify/static\";\nimport fastify, {\n FastifyInstance,\n FastifyReply,\n FastifyRequest,\n FastifyServerOptions,\n} from \"fastify\";\nimport { jsxToString } from \"jsx-async-runtime\";\nimport { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport env from \"./env.js\";\n\nenv();\n\nconst CWD = process.cwd();\nconst CONFIG = (await import(`file://${join(CWD, \"jeasx.config.js\")}`)).default;\nconst NODE_ENV_IS_DEVELOPMENT = process.env.NODE_ENV === \"development\";\n\n// Mapping for route modules used in non-development environments.\nconst MODULE_BY_ROUTE: Record<string, { default: Function } | string> = {};\n\n// Initialize the mapping with absolute paths for all known modules.\n// Modules are lazily loaded on their first request for a specific route.\nif (!NODE_ENV_IS_DEVELOPMENT) {\n const { routes } = (await import(`file://${join(CWD, \"dist\", \"[--metadata--].js\")}`)).default as {\n routes: string[];\n };\n // Map route identifiers to their absolute file system paths in the build directory.\n for (const route of routes) {\n MODULE_BY_ROUTE[route] = join(CWD, \"dist\", `${route}.js`);\n }\n}\n\ndeclare module \"fastify\" {\n interface FastifyRequest {\n path: string; // Path without query parameters\n route: string; // Path to resolved route handler\n }\n}\n\n// Enhance Fastify server from userland\nconst FASTIFY_SERVER = (CONFIG.FASTIFY_SERVER ?? ((fastify) => fastify)) as (\n fastify: FastifyInstance,\n) => FastifyInstance;\n\n// Create and export a Fastify instance\nexport default FASTIFY_SERVER(\n fastify({\n ...(CONFIG.FASTIFY_SERVER_OPTIONS?.() as FastifyServerOptions),\n }),\n)\n // Create encapsulation context\n .register((fastify) => {\n fastify\n .register(fastifyCookie, {\n ...(CONFIG.FASTIFY_COOKIE_OPTIONS?.() as FastifyCookieOptions),\n })\n .register(fastifyFormbody, {\n ...(CONFIG.FASTIFY_FORMBODY_OPTIONS?.() as FastifyFormbodyOptions),\n })\n .register(fastifyMultipart, {\n ...(CONFIG.FASTIFY_MULTIPART_OPTIONS?.() as FastifyMultipartOptions),\n })\n .register(fastifyStatic, {\n root: [\"public\", \"dist\"].map((dir) => join(CWD, dir)),\n wildcard: false,\n globIgnore: [\"/**/\\\\[*\\\\].js?(.map)\"],\n ...(CONFIG.FASTIFY_STATIC_OPTIONS?.() as FastifyStaticOptions),\n })\n .decorateRequest(\"route\", \"\")\n .decorateRequest(\"path\", \"\")\n .addHook(\"onRequest\", async (request) => {\n // Extract path from url\n const index = request.url.indexOf(\"?\");\n request.path = index === -1 ? request.url : request.url.slice(0, index);\n })\n .all(\"*\", async (request: FastifyRequest, reply: FastifyReply) => {\n try {\n const payload = await handler(request, reply);\n if (\n reply.getHeader(\"content-type\") === undefined &&\n (typeof payload === \"string\" || Buffer.isBuffer(payload))\n ) {\n reply.type(\"text/html; charset=utf-8\");\n }\n return payload;\n } catch (error) {\n request.log.error(error);\n throw error;\n }\n });\n });\n\n/**\n * Resolves route module based on the request path and execute it.\n */\nasync function handler(request: FastifyRequest, reply: FastifyReply) {\n let response: unknown;\n\n // Global context object for route handlers\n const context = {};\n\n // Default props for route handlers\n const props = { request, reply };\n\n try {\n // Execute route handlers for current request\n for (const route of generateRoutes(request.path)) {\n // Resolve module or path to module\n let module = MODULE_BY_ROUTE[route];\n\n // Skip processing if the route path was not initialized.\n if (module === undefined && !NODE_ENV_IS_DEVELOPMENT) {\n continue;\n }\n\n // Module was not loaded yet?\n try {\n if (typeof module === \"string\") {\n // Production: Load and cache module only via pre-calculated path.\n // This avoids potential path traversal vulnerabilities caused\n // by unexpected `route` values.\n module = MODULE_BY_ROUTE[route] = await import(`file://${module}`);\n } else if (module === undefined && NODE_ENV_IS_DEVELOPMENT) {\n // Only map module paths depending on `route` during development.\n const modulePath = join(CWD, \"dist\", `${route}.js`);\n if (typeof require === \"function\" && require.cache[modulePath]) {\n // Bun: Remove module from cache before importing\n // as query parameter for import is ignored.\n delete require.cache[modulePath];\n }\n // Use timestamp as query parameter to update modules.\n const mtime = (await stat(modulePath)).mtime.getTime();\n // Dynamic imports are restricted to development environments;\n // therefore, production-level path validation is not required here.\n module = await import(`file://${modulePath}?${mtime}`);\n }\n } catch (e) {\n switch (e.code) {\n case \"ENOENT\":\n case \"ENOTDIR\":\n case \"ERR_MODULE_NOT_FOUND\":\n continue;\n default:\n // Module exists, but fails to load.\n throw e;\n }\n }\n\n // Ensure module is a valid object before processing.\n if (!module || typeof module !== \"object\") {\n continue;\n }\n\n // Store current route in request.\n request.route = route;\n\n response =\n typeof module.default === \"function\"\n ? // Call functions with context as `this` and props as parameters,\n await module.default.call(context, props)\n : // otherwise return default export.\n module.default;\n\n if (reply.sent) {\n return;\n } else if (route.endsWith(\"/[404]\")) {\n // Preserve existing status if a 404 page is requested directly.\n // If no status is defined, set status to 404 automatically.\n if (reply.statusCode === 200 && !request.path.endsWith(\"/404\")) {\n reply.status(404);\n }\n break;\n } else if (typeof response === \"string\" || Buffer.isBuffer(response) || isJSX(response)) {\n break;\n } else if (\n route.endsWith(\"/[...guard]\") &&\n (response === undefined || typeof response === \"object\")\n ) {\n // Add object entries from guard to props\n Object.assign(props, response);\n continue;\n } else if (reply.statusCode === 404) {\n continue;\n } else {\n break;\n }\n }\n return await renderJSX(context, response);\n } catch (error) {\n const errorHandler = context[\"errorHandler\"];\n if (typeof errorHandler === \"function\") {\n reply.status(500);\n response = await errorHandler.call(context, error);\n return await renderJSX(context, response);\n } else {\n throw error;\n }\n }\n}\n\n/**\n * Generates all possible routes based on the given input path.\n *\n * Example routes for \"/a/b/c\":\n *\n * [\n * \"/[...guard]\",\"/a/[...guard]\",\"/a/b/[...guard]\",\"/a/b/c/[...guard]\",\n * \"/a/b/[c]\",\"/a/b/c/[index]\",\n * \"/a/b/c/[...path]\",\"/a/b/[...path]\",\"/a/[...path]\",\"/[...path]\",\n * \"/a/b/c/[404]\",\"/a/b/[404]\",\"/a/[404]\",\"/[404]\"\n * ]\n */\nfunction generateRoutes(path: string): string[] {\n const routes = [];\n\n // Transform given path into array of all its segments.\n // \"/a/b/c\" => [\"/a/b/c\", \"/a/b\", \"/a\", \"\"]\n const segments = [\"\"];\n let current = \"\";\n for (const segment of path.split(\"/\")) {\n // Ignore redundant slashes.\n if (segment !== \"\") {\n current += `/${segment}`;\n segments.push(current);\n }\n }\n segments.reverse();\n\n // [...guard]s are evaluated from top to bottom\n for (let i = segments.length - 1; i >= 0; i--) {\n routes.push(`${segments[i]}/[...guard]`);\n }\n\n // \"/a/b/c\" => [\"/a/b/[c]\", \"/a/b/c/[index]\"]\n const edgeSegment = segments[0];\n const lastSlash = edgeSegment.lastIndexOf(\"/\") + 1;\n if (lastSlash > 0) {\n routes.push(`${edgeSegment.substring(0, lastSlash)}[${edgeSegment.substring(lastSlash)}]`);\n }\n routes.push(`${edgeSegment}/[index]`);\n\n for (let i = 0; i < segments.length; i++) {\n routes.push(`${segments[i]}/[...path]`);\n }\n\n for (let i = 0; i < segments.length; i++) {\n routes.push(`${segments[i]}/[404]`);\n }\n\n return routes;\n}\n\n/**\n * Determines if a given object is a JSX element.\n */\nfunction isJSX(obj: unknown): boolean {\n return !!obj && typeof obj === \"object\" && \"type\" in obj && \"props\" in obj;\n}\n\n/**\n * Renders JSX to string and applies optional response handler.\n */\nasync function renderJSX(context: object, response: unknown) {\n const payload = isJSX(response) ? await jsxToString.call(context, response) : response;\n\n // Post-process the payload with an optional response handler\n const responseHandler = context[\"responseHandler\"];\n return typeof responseHandler === \"function\"\n ? await responseHandler.call(context, payload)\n : payload;\n}\n"],
|
|
5
|
-
"mappings": "AAAA,OAAO,mBAA6C;AACpD,OAAO,qBAAiD;AACxD,OAAO,sBAAmD;AAC1D,OAAO,
|
|
4
|
+
"sourcesContent": ["import fastifyCookie, { FastifyCookieOptions } from \"@fastify/cookie\";\nimport fastifyFormbody, { FastifyFormbodyOptions } from \"@fastify/formbody\";\nimport fastifyMultipart, { FastifyMultipartOptions } from \"@fastify/multipart\";\nimport fastifySend, { SendOptions, SendResult } from \"@fastify/send\";\nimport fastify, {\n FastifyInstance,\n FastifyReply,\n FastifyRequest,\n FastifyServerOptions,\n} from \"fastify\";\nimport { jsxToString } from \"jsx-async-runtime\";\nimport { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport env from \"./env.js\";\n\nenv();\n\nconst CWD = process.cwd();\nconst CONFIG = (await import(`file://${join(CWD, \"jeasx.config.js\")}`)).default;\nconst NODE_ENV_IS_DEVELOPMENT = process.env.NODE_ENV === \"development\";\n\n// Map routes and files for non-development environments from metadata export.\n// Module paths are initialized at startup but overwritten\n// with resolved modules upon the first request.\nconst { routes: MODULE_BY_ROUTE, files: FILE_BY_PATH } = NODE_ENV_IS_DEVELOPMENT\n ? { routes: {}, files: {} }\n : ((await import(`file://${join(CWD, \"dist\", \"[--metadata--].js\")}`)).default as {\n routes: Record<string, string | { default: Function }>;\n files: Record<string, string>;\n });\n\ndeclare module \"fastify\" {\n interface FastifyRequest {\n path: string; // Path without query parameters\n route: string; // Path to resolved route handler\n }\n}\n\nconst FASTIFY_SEND_OPTIONS = {\n ...(CONFIG.FASTIFY_SEND_OPTIONS?.() as SendOptions),\n};\n\n// Enhance Fastify server from userland\nconst FASTIFY_SERVER = (CONFIG.FASTIFY_SERVER ?? ((fastify) => fastify)) as (\n fastify: FastifyInstance,\n) => FastifyInstance;\n\n// Create and export a Fastify instance\nexport default FASTIFY_SERVER(\n fastify({\n ...(CONFIG.FASTIFY_SERVER_OPTIONS?.() as FastifyServerOptions),\n }),\n)\n // Create encapsulation context\n .register((fastify) => {\n fastify\n .register(fastifyCookie, {\n ...(CONFIG.FASTIFY_COOKIE_OPTIONS?.() as FastifyCookieOptions),\n })\n .register(fastifyFormbody, {\n ...(CONFIG.FASTIFY_FORMBODY_OPTIONS?.() as FastifyFormbodyOptions),\n })\n .register(fastifyMultipart, {\n ...(CONFIG.FASTIFY_MULTIPART_OPTIONS?.() as FastifyMultipartOptions),\n })\n .decorateRequest(\"route\", \"\")\n .decorateRequest(\"path\", \"\")\n .addHook(\"onRequest\", async (request) => {\n // Extract path from url\n const index = request.url.indexOf(\"?\");\n request.path = index === -1 ? request.url : request.url.slice(0, index);\n })\n .all(\"*\", async (request: FastifyRequest, reply: FastifyReply) => {\n try {\n const payload = await handler(request, reply);\n if (\n reply.getHeader(\"content-type\") === undefined &&\n (typeof payload === \"string\" || Buffer.isBuffer(payload))\n ) {\n reply.type(\"text/html; charset=utf-8\");\n }\n return payload;\n } catch (error) {\n request.log.error(error);\n throw error;\n }\n });\n });\n\n/**\n * Resolves route module based on the request path and execute it.\n */\nasync function handler(request: FastifyRequest, reply: FastifyReply) {\n let response: unknown;\n\n // Global context object for route handlers\n const context = {};\n\n // Default props for route handlers\n const props = { request, reply };\n\n try {\n // Execute route handlers for current request\n for (const route of generateRoutes(request.path)) {\n // Try to serve static file when route matches path.\n if (route === request.path) {\n const sendResult = await tryFile(request);\n if (sendResult) {\n reply.status(sendResult.statusCode);\n reply.headers(sendResult.headers);\n response = sendResult.stream;\n break;\n }\n continue;\n }\n\n // Resolve module or path to module\n let module = MODULE_BY_ROUTE[route];\n\n // Skip processing if the route path was not initialized.\n if (module === undefined && !NODE_ENV_IS_DEVELOPMENT) {\n continue;\n }\n\n // Module was not loaded yet?\n try {\n if (typeof module === \"string\") {\n // Production: Load and cache module only via pre-calculated path.\n // This avoids potential path traversal vulnerabilities caused\n // by unexpected `route` values.\n module = MODULE_BY_ROUTE[route] = await import(`file://${join(CWD, module)}`);\n } else if (module === undefined && NODE_ENV_IS_DEVELOPMENT) {\n // Only map module paths depending on `route` during development.\n const modulePath = join(CWD, \"dist\", `${route}.js`);\n if (typeof require === \"function\" && require.cache[modulePath]) {\n // Bun: Remove module from cache before importing\n // as query parameter for import is ignored.\n delete require.cache[modulePath];\n }\n // Use timestamp as query parameter to update modules.\n const mtime = (await stat(modulePath)).mtime.getTime();\n // Dynamic imports are restricted to development environments;\n // therefore, production-level path validation is not required here.\n module = await import(`file://${modulePath}?${mtime}`);\n }\n } catch (e) {\n switch (e.code) {\n case \"ENOENT\":\n case \"ENOTDIR\":\n case \"ERR_MODULE_NOT_FOUND\":\n continue;\n default:\n // Module exists, but fails to load.\n throw e;\n }\n }\n\n // Store current route in request.\n request.route = route;\n\n // Ensure module is a valid object before processing.\n if (module && typeof module === \"object\") {\n response =\n typeof module.default === \"function\"\n ? // Call functions with context as `this` and props as parameters,\n await module.default.call(context, props)\n : // otherwise return default export.\n module.default;\n }\n\n if (reply.sent) {\n return;\n } else if (route.endsWith(\"/[404]\")) {\n // Preserve existing status if a 404 page is requested directly.\n // If no status is defined, set status to 404 automatically.\n if (reply.statusCode === 200 && !request.path.endsWith(\"/404\")) {\n reply.status(404);\n }\n break;\n } else if (\n typeof response === \"string\" ||\n response instanceof Readable ||\n Buffer.isBuffer(response) ||\n isJSX(response)\n ) {\n break;\n } else if (route.endsWith(\"/[...guard]\") && typeof response === \"object\") {\n // Add object entries from guard to props\n Object.assign(props, response);\n continue;\n } else if (response === undefined || reply.statusCode === 404) {\n continue;\n } else {\n break;\n }\n }\n\n return await renderResponse(context, response);\n } catch (error) {\n const errorHandler = context[\"errorHandler\"];\n if (typeof errorHandler === \"function\") {\n reply.status(500);\n response = await errorHandler.call(context, error);\n return await renderResponse(context, response);\n } else {\n throw error;\n }\n }\n}\n\n/**\n * Generates all possible routes based on the given input path.\n *\n * Example routes for \"/a/b/c\":\n *\n * [\n * \"/[...guard]\",\"/a/[...guard]\",\"/a/b/[...guard]\",\"/a/b/c/[...guard]\",\n * \"/a/b/c\",\n * \"/a/b/[c]\",\"/a/b/c/[index]\",\n * \"/a/b/c/[...path]\",\"/a/b/[...path]\",\"/a/[...path]\",\"/[...path]\",\n * \"/a/b/c/[404]\",\"/a/b/[404]\",\"/a/[404]\",\"/[404]\"\n * ]\n */\nfunction generateRoutes(path: string): string[] {\n const routes = [];\n\n // Transform given path into array of all its segments.\n // \"/a/b/c\" => [\"\", \"/a\", \"/a/b/\", \"/a/b/c\"]\n const segments = [\"\"];\n let edgeSegment = \"\";\n for (const segment of path.split(\"/\")) {\n // Ignore redundant slashes.\n if (segment !== \"\") {\n edgeSegment += `/${segment}`;\n segments.push(edgeSegment);\n }\n }\n\n // [...guard]s are pushed from root to edge.\n for (let i = 0; i < segments.length; i++) {\n routes.push(`${segments[i]}/[...guard]`);\n }\n\n // Append the verbatim path for static file serving.\n routes.push(path);\n\n // \"/a/b/c\" => [\"/a/b/[c]\", \"/a/b/c/[index]\"]\n const lastSlash = edgeSegment.lastIndexOf(\"/\") + 1;\n if (lastSlash > 0) {\n routes.push(`${edgeSegment.substring(0, lastSlash)}[${edgeSegment.substring(lastSlash)}]`);\n }\n routes.push(`${edgeSegment}/[index]`);\n\n // [...path]s are pushed from edge to root.\n for (let i = segments.length - 1; i >= 0; i--) {\n routes.push(`${segments[i]}/[...path]`);\n }\n\n // [404]s are pushed from edge to root.\n for (let i = segments.length - 1; i >= 0; i--) {\n routes.push(`${segments[i]}/[404]`);\n }\n\n return routes;\n}\n\n/**\n * Determines if a given object is a JSX element.\n */\nfunction isJSX(obj: unknown): boolean {\n return !!obj && typeof obj === \"object\" && \"type\" in obj && \"props\" in obj;\n}\n\n/**\n * Renders JSX to string and applies optional response handler.\n */\nasync function renderResponse(context: object, response: unknown) {\n const payload = isJSX(response) ? await jsxToString.call(context, response) : response;\n\n // Post-process the payload with an optional response handler\n const responseHandler = context[\"responseHandler\"];\n return typeof responseHandler === \"function\"\n ? await responseHandler.call(context, payload)\n : payload;\n}\n\n/**\n * Returns stream and metadata for requested file.\n */\nasync function tryFile(request: FastifyRequest): Promise<SendResult> | undefined {\n // Production: Retrieve files only from pre-initialized mapping.\n // This avoids potential path traversal vulnerabilities caused\n // by unexpected `request.path` values.\n const file = FILE_BY_PATH[request.path];\n if (file) {\n return await fastifySend(request.raw, file, FASTIFY_SEND_OPTIONS);\n }\n\n if (NODE_ENV_IS_DEVELOPMENT) {\n for (const directory of [\"dist\", \"public\"]) {\n try {\n if ((await stat(join(CWD, directory, request.path))).isFile()) {\n // Dynamic path loading is restricted to development environments;\n // therefore, production-level path validation is not required here.\n return await fastifySend(\n request.raw,\n `${directory}${request.path}`,\n FASTIFY_SEND_OPTIONS,\n );\n }\n } catch {\n continue;\n }\n }\n }\n\n return undefined;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["fastify"]
|
|
7
7
|
}
|
package/serverless.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fastifyCookie, { FastifyCookieOptions } from "@fastify/cookie";
|
|
2
2
|
import fastifyFormbody, { FastifyFormbodyOptions } from "@fastify/formbody";
|
|
3
3
|
import fastifyMultipart, { FastifyMultipartOptions } from "@fastify/multipart";
|
|
4
|
-
import
|
|
4
|
+
import fastifySend, { SendOptions, SendResult } from "@fastify/send";
|
|
5
5
|
import fastify, {
|
|
6
6
|
FastifyInstance,
|
|
7
7
|
FastifyReply,
|
|
@@ -11,6 +11,7 @@ import fastify, {
|
|
|
11
11
|
import { jsxToString } from "jsx-async-runtime";
|
|
12
12
|
import { stat } from "node:fs/promises";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
+
import { Readable } from "node:stream";
|
|
14
15
|
import env from "./env.js";
|
|
15
16
|
|
|
16
17
|
env();
|
|
@@ -19,20 +20,15 @@ const CWD = process.cwd();
|
|
|
19
20
|
const CONFIG = (await import(`file://${join(CWD, "jeasx.config.js")}`)).default;
|
|
20
21
|
const NODE_ENV_IS_DEVELOPMENT = process.env.NODE_ENV === "development";
|
|
21
22
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Map route identifiers to their absolute file system paths in the build directory.
|
|
32
|
-
for (const route of routes) {
|
|
33
|
-
MODULE_BY_ROUTE[route] = join(CWD, "dist", `${route}.js`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
23
|
+
// Map routes and files for non-development environments from metadata export.
|
|
24
|
+
// Module paths are initialized at startup but overwritten
|
|
25
|
+
// with resolved modules upon the first request.
|
|
26
|
+
const { routes: MODULE_BY_ROUTE, files: FILE_BY_PATH } = NODE_ENV_IS_DEVELOPMENT
|
|
27
|
+
? { routes: {}, files: {} }
|
|
28
|
+
: ((await import(`file://${join(CWD, "dist", "[--metadata--].js")}`)).default as {
|
|
29
|
+
routes: Record<string, string | { default: Function }>;
|
|
30
|
+
files: Record<string, string>;
|
|
31
|
+
});
|
|
36
32
|
|
|
37
33
|
declare module "fastify" {
|
|
38
34
|
interface FastifyRequest {
|
|
@@ -41,6 +37,10 @@ declare module "fastify" {
|
|
|
41
37
|
}
|
|
42
38
|
}
|
|
43
39
|
|
|
40
|
+
const FASTIFY_SEND_OPTIONS = {
|
|
41
|
+
...(CONFIG.FASTIFY_SEND_OPTIONS?.() as SendOptions),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
44
|
// Enhance Fastify server from userland
|
|
45
45
|
const FASTIFY_SERVER = (CONFIG.FASTIFY_SERVER ?? ((fastify) => fastify)) as (
|
|
46
46
|
fastify: FastifyInstance,
|
|
@@ -64,12 +64,6 @@ export default FASTIFY_SERVER(
|
|
|
64
64
|
.register(fastifyMultipart, {
|
|
65
65
|
...(CONFIG.FASTIFY_MULTIPART_OPTIONS?.() as FastifyMultipartOptions),
|
|
66
66
|
})
|
|
67
|
-
.register(fastifyStatic, {
|
|
68
|
-
root: ["public", "dist"].map((dir) => join(CWD, dir)),
|
|
69
|
-
wildcard: false,
|
|
70
|
-
globIgnore: ["/**/\\[*\\].js?(.map)"],
|
|
71
|
-
...(CONFIG.FASTIFY_STATIC_OPTIONS?.() as FastifyStaticOptions),
|
|
72
|
-
})
|
|
73
67
|
.decorateRequest("route", "")
|
|
74
68
|
.decorateRequest("path", "")
|
|
75
69
|
.addHook("onRequest", async (request) => {
|
|
@@ -109,6 +103,18 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
109
103
|
try {
|
|
110
104
|
// Execute route handlers for current request
|
|
111
105
|
for (const route of generateRoutes(request.path)) {
|
|
106
|
+
// Try to serve static file when route matches path.
|
|
107
|
+
if (route === request.path) {
|
|
108
|
+
const sendResult = await tryFile(request);
|
|
109
|
+
if (sendResult) {
|
|
110
|
+
reply.status(sendResult.statusCode);
|
|
111
|
+
reply.headers(sendResult.headers);
|
|
112
|
+
response = sendResult.stream;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
112
118
|
// Resolve module or path to module
|
|
113
119
|
let module = MODULE_BY_ROUTE[route];
|
|
114
120
|
|
|
@@ -123,7 +129,7 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
123
129
|
// Production: Load and cache module only via pre-calculated path.
|
|
124
130
|
// This avoids potential path traversal vulnerabilities caused
|
|
125
131
|
// by unexpected `route` values.
|
|
126
|
-
module = MODULE_BY_ROUTE[route] = await import(`file://${module}`);
|
|
132
|
+
module = MODULE_BY_ROUTE[route] = await import(`file://${join(CWD, module)}`);
|
|
127
133
|
} else if (module === undefined && NODE_ENV_IS_DEVELOPMENT) {
|
|
128
134
|
// Only map module paths depending on `route` during development.
|
|
129
135
|
const modulePath = join(CWD, "dist", `${route}.js`);
|
|
@@ -150,20 +156,18 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
150
156
|
}
|
|
151
157
|
}
|
|
152
158
|
|
|
153
|
-
// Ensure module is a valid object before processing.
|
|
154
|
-
if (!module || typeof module !== "object") {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
159
|
// Store current route in request.
|
|
159
160
|
request.route = route;
|
|
160
161
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
// Ensure module is a valid object before processing.
|
|
163
|
+
if (module && typeof module === "object") {
|
|
164
|
+
response =
|
|
165
|
+
typeof module.default === "function"
|
|
166
|
+
? // Call functions with context as `this` and props as parameters,
|
|
167
|
+
await module.default.call(context, props)
|
|
168
|
+
: // otherwise return default export.
|
|
169
|
+
module.default;
|
|
170
|
+
}
|
|
167
171
|
|
|
168
172
|
if (reply.sent) {
|
|
169
173
|
return;
|
|
@@ -174,28 +178,31 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
174
178
|
reply.status(404);
|
|
175
179
|
}
|
|
176
180
|
break;
|
|
177
|
-
} else if (typeof response === "string" || Buffer.isBuffer(response) || isJSX(response)) {
|
|
178
|
-
break;
|
|
179
181
|
} else if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
+
typeof response === "string" ||
|
|
183
|
+
response instanceof Readable ||
|
|
184
|
+
Buffer.isBuffer(response) ||
|
|
185
|
+
isJSX(response)
|
|
182
186
|
) {
|
|
187
|
+
break;
|
|
188
|
+
} else if (route.endsWith("/[...guard]") && typeof response === "object") {
|
|
183
189
|
// Add object entries from guard to props
|
|
184
190
|
Object.assign(props, response);
|
|
185
191
|
continue;
|
|
186
|
-
} else if (reply.statusCode === 404) {
|
|
192
|
+
} else if (response === undefined || reply.statusCode === 404) {
|
|
187
193
|
continue;
|
|
188
194
|
} else {
|
|
189
195
|
break;
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
|
-
|
|
198
|
+
|
|
199
|
+
return await renderResponse(context, response);
|
|
193
200
|
} catch (error) {
|
|
194
201
|
const errorHandler = context["errorHandler"];
|
|
195
202
|
if (typeof errorHandler === "function") {
|
|
196
203
|
reply.status(500);
|
|
197
204
|
response = await errorHandler.call(context, error);
|
|
198
|
-
return await
|
|
205
|
+
return await renderResponse(context, response);
|
|
199
206
|
} else {
|
|
200
207
|
throw error;
|
|
201
208
|
}
|
|
@@ -209,6 +216,7 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
209
216
|
*
|
|
210
217
|
* [
|
|
211
218
|
* "/[...guard]","/a/[...guard]","/a/b/[...guard]","/a/b/c/[...guard]",
|
|
219
|
+
* "/a/b/c",
|
|
212
220
|
* "/a/b/[c]","/a/b/c/[index]",
|
|
213
221
|
* "/a/b/c/[...path]","/a/b/[...path]","/a/[...path]","/[...path]",
|
|
214
222
|
* "/a/b/c/[404]","/a/b/[404]","/a/[404]","/[404]"
|
|
@@ -218,36 +226,39 @@ function generateRoutes(path: string): string[] {
|
|
|
218
226
|
const routes = [];
|
|
219
227
|
|
|
220
228
|
// Transform given path into array of all its segments.
|
|
221
|
-
// "/a/b/c" => ["/a
|
|
229
|
+
// "/a/b/c" => ["", "/a", "/a/b/", "/a/b/c"]
|
|
222
230
|
const segments = [""];
|
|
223
|
-
let
|
|
231
|
+
let edgeSegment = "";
|
|
224
232
|
for (const segment of path.split("/")) {
|
|
225
233
|
// Ignore redundant slashes.
|
|
226
234
|
if (segment !== "") {
|
|
227
|
-
|
|
228
|
-
segments.push(
|
|
235
|
+
edgeSegment += `/${segment}`;
|
|
236
|
+
segments.push(edgeSegment);
|
|
229
237
|
}
|
|
230
238
|
}
|
|
231
|
-
segments.reverse();
|
|
232
239
|
|
|
233
|
-
// [...guard]s are
|
|
234
|
-
for (let i =
|
|
240
|
+
// [...guard]s are pushed from root to edge.
|
|
241
|
+
for (let i = 0; i < segments.length; i++) {
|
|
235
242
|
routes.push(`${segments[i]}/[...guard]`);
|
|
236
243
|
}
|
|
237
244
|
|
|
245
|
+
// Append the verbatim path for static file serving.
|
|
246
|
+
routes.push(path);
|
|
247
|
+
|
|
238
248
|
// "/a/b/c" => ["/a/b/[c]", "/a/b/c/[index]"]
|
|
239
|
-
const edgeSegment = segments[0];
|
|
240
249
|
const lastSlash = edgeSegment.lastIndexOf("/") + 1;
|
|
241
250
|
if (lastSlash > 0) {
|
|
242
251
|
routes.push(`${edgeSegment.substring(0, lastSlash)}[${edgeSegment.substring(lastSlash)}]`);
|
|
243
252
|
}
|
|
244
253
|
routes.push(`${edgeSegment}/[index]`);
|
|
245
254
|
|
|
246
|
-
|
|
255
|
+
// [...path]s are pushed from edge to root.
|
|
256
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
247
257
|
routes.push(`${segments[i]}/[...path]`);
|
|
248
258
|
}
|
|
249
259
|
|
|
250
|
-
|
|
260
|
+
// [404]s are pushed from edge to root.
|
|
261
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
251
262
|
routes.push(`${segments[i]}/[404]`);
|
|
252
263
|
}
|
|
253
264
|
|
|
@@ -264,7 +275,7 @@ function isJSX(obj: unknown): boolean {
|
|
|
264
275
|
/**
|
|
265
276
|
* Renders JSX to string and applies optional response handler.
|
|
266
277
|
*/
|
|
267
|
-
async function
|
|
278
|
+
async function renderResponse(context: object, response: unknown) {
|
|
268
279
|
const payload = isJSX(response) ? await jsxToString.call(context, response) : response;
|
|
269
280
|
|
|
270
281
|
// Post-process the payload with an optional response handler
|
|
@@ -273,3 +284,36 @@ async function renderJSX(context: object, response: unknown) {
|
|
|
273
284
|
? await responseHandler.call(context, payload)
|
|
274
285
|
: payload;
|
|
275
286
|
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Returns stream and metadata for requested file.
|
|
290
|
+
*/
|
|
291
|
+
async function tryFile(request: FastifyRequest): Promise<SendResult> | undefined {
|
|
292
|
+
// Production: Retrieve files only from pre-initialized mapping.
|
|
293
|
+
// This avoids potential path traversal vulnerabilities caused
|
|
294
|
+
// by unexpected `request.path` values.
|
|
295
|
+
const file = FILE_BY_PATH[request.path];
|
|
296
|
+
if (file) {
|
|
297
|
+
return await fastifySend(request.raw, file, FASTIFY_SEND_OPTIONS);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (NODE_ENV_IS_DEVELOPMENT) {
|
|
301
|
+
for (const directory of ["dist", "public"]) {
|
|
302
|
+
try {
|
|
303
|
+
if ((await stat(join(CWD, directory, request.path))).isFile()) {
|
|
304
|
+
// Dynamic path loading is restricted to development environments;
|
|
305
|
+
// therefore, production-level path validation is not required here.
|
|
306
|
+
return await fastifySend(
|
|
307
|
+
request.raw,
|
|
308
|
+
`${directory}${request.path}`,
|
|
309
|
+
FASTIFY_SEND_OPTIONS,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|