jeasx 2.7.5 → 2.8.1
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 +4 -4
- package/serverless.js +63 -40
- package/serverless.js.map +2 -2
- package/serverless.ts +112 -56
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.1",
|
|
4
4
|
"description": "Jeasx - the ease of JSX with the power of SSR",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"async",
|
|
@@ -32,10 +32,10 @@
|
|
|
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
|
-
"fastify": "5.
|
|
38
|
+
"fastify": "5.10.0",
|
|
39
39
|
"jsx-async-runtime": "2.1.3"
|
|
40
40
|
},
|
|
41
41
|
"allowScripts": {
|
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,12 +28,7 @@ var serverless_default = FASTIFY_SERVER(
|
|
|
30
28
|
...CONFIG.FASTIFY_FORMBODY_OPTIONS?.()
|
|
31
29
|
}).register(fastifyMultipart, {
|
|
32
30
|
...CONFIG.FASTIFY_MULTIPART_OPTIONS?.()
|
|
33
|
-
}).
|
|
34
|
-
root: ["public", "dist"].map((dir) => join(CWD, dir)),
|
|
35
|
-
wildcard: false,
|
|
36
|
-
globIgnore: ["/**/\\[*\\].js?(.map)"],
|
|
37
|
-
...CONFIG.FASTIFY_STATIC_OPTIONS?.()
|
|
38
|
-
}).decorateRequest("route", "").decorateRequest("path", "").addHook("onRequest", async (request) => {
|
|
31
|
+
}).decorateRequest("route", "").decorateRequest("path", "").decorateReply("file", void 0).addHook("onRequest", async (request) => {
|
|
39
32
|
const index = request.url.indexOf("?");
|
|
40
33
|
request.path = index === -1 ? request.url : request.url.slice(0, index);
|
|
41
34
|
}).all("*", async (request, reply) => {
|
|
@@ -56,14 +49,24 @@ async function handler(request, reply) {
|
|
|
56
49
|
const context = {};
|
|
57
50
|
const props = { request, reply };
|
|
58
51
|
try {
|
|
52
|
+
reply.file = await tryFile(request);
|
|
59
53
|
for (const route of generateRoutes(request.path)) {
|
|
54
|
+
if (route === request.path) {
|
|
55
|
+
if (reply.file) {
|
|
56
|
+
reply.status(reply.file.statusCode);
|
|
57
|
+
reply.headers(reply.file.headers);
|
|
58
|
+
response = reply.file.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]) {
|
|
@@ -73,7 +76,7 @@ async function handler(request, reply) {
|
|
|
73
76
|
module = await import(`file://${modulePath}?${mtime}`);
|
|
74
77
|
}
|
|
75
78
|
} catch (e) {
|
|
76
|
-
switch (e
|
|
79
|
+
switch (e?.code) {
|
|
77
80
|
case "ENOENT":
|
|
78
81
|
case "ENOTDIR":
|
|
79
82
|
case "ERR_MODULE_NOT_FOUND":
|
|
@@ -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, { BaseSendResult, SendOptions } 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 without query parameters */\n path: string;\n /** Path to resolved route handler */\n route: string;\n }\n\n interface FastifyReply {\n /** Populated when serving a static file; otherwise undefined. */\n file?: BaseSendResult;\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 .decorateReply(\"file\", undefined)\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: any = {};\n\n // Default props for route handlers\n const props = { request, reply };\n\n try {\n // Check for static file and store result for later processing.\n reply.file = await tryFile(request);\n\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 if (reply.file) {\n reply.status(reply.file.statusCode);\n reply.headers(reply.file.headers);\n response = reply.file.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 as any)?.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: any, response: unknown) {\n const payload = isJSX(response)\n ? await jsxToString.call(context, response as JSX.Element)\n : 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<BaseSendResult | 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, { BaseSendResult, SendOptions } 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,28 +20,34 @@ 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 {
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
/** Path without query parameters */
|
|
36
|
+
path: string;
|
|
37
|
+
/** Path to resolved route handler */
|
|
38
|
+
route: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FastifyReply {
|
|
42
|
+
/** Populated when serving a static file; otherwise undefined. */
|
|
43
|
+
file?: BaseSendResult;
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
const FASTIFY_SEND_OPTIONS = {
|
|
48
|
+
...(CONFIG.FASTIFY_SEND_OPTIONS?.() as SendOptions),
|
|
49
|
+
};
|
|
50
|
+
|
|
44
51
|
// Enhance Fastify server from userland
|
|
45
52
|
const FASTIFY_SERVER = (CONFIG.FASTIFY_SERVER ?? ((fastify) => fastify)) as (
|
|
46
53
|
fastify: FastifyInstance,
|
|
@@ -64,14 +71,9 @@ export default FASTIFY_SERVER(
|
|
|
64
71
|
.register(fastifyMultipart, {
|
|
65
72
|
...(CONFIG.FASTIFY_MULTIPART_OPTIONS?.() as FastifyMultipartOptions),
|
|
66
73
|
})
|
|
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
74
|
.decorateRequest("route", "")
|
|
74
75
|
.decorateRequest("path", "")
|
|
76
|
+
.decorateReply("file", undefined)
|
|
75
77
|
.addHook("onRequest", async (request) => {
|
|
76
78
|
// Extract path from url
|
|
77
79
|
const index = request.url.indexOf("?");
|
|
@@ -101,14 +103,28 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
101
103
|
let response: unknown;
|
|
102
104
|
|
|
103
105
|
// Global context object for route handlers
|
|
104
|
-
const context = {};
|
|
106
|
+
const context: any = {};
|
|
105
107
|
|
|
106
108
|
// Default props for route handlers
|
|
107
109
|
const props = { request, reply };
|
|
108
110
|
|
|
109
111
|
try {
|
|
112
|
+
// Check for static file and store result for later processing.
|
|
113
|
+
reply.file = await tryFile(request);
|
|
114
|
+
|
|
110
115
|
// Execute route handlers for current request
|
|
111
116
|
for (const route of generateRoutes(request.path)) {
|
|
117
|
+
// Try to serve static file when route matches path.
|
|
118
|
+
if (route === request.path) {
|
|
119
|
+
if (reply.file) {
|
|
120
|
+
reply.status(reply.file.statusCode);
|
|
121
|
+
reply.headers(reply.file.headers);
|
|
122
|
+
response = reply.file.stream;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
// Resolve module or path to module
|
|
113
129
|
let module = MODULE_BY_ROUTE[route];
|
|
114
130
|
|
|
@@ -123,7 +139,7 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
123
139
|
// Production: Load and cache module only via pre-calculated path.
|
|
124
140
|
// This avoids potential path traversal vulnerabilities caused
|
|
125
141
|
// by unexpected `route` values.
|
|
126
|
-
module = MODULE_BY_ROUTE[route] = await import(`file://${module}`);
|
|
142
|
+
module = MODULE_BY_ROUTE[route] = await import(`file://${join(CWD, module)}`);
|
|
127
143
|
} else if (module === undefined && NODE_ENV_IS_DEVELOPMENT) {
|
|
128
144
|
// Only map module paths depending on `route` during development.
|
|
129
145
|
const modulePath = join(CWD, "dist", `${route}.js`);
|
|
@@ -139,7 +155,7 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
139
155
|
module = await import(`file://${modulePath}?${mtime}`);
|
|
140
156
|
}
|
|
141
157
|
} catch (e) {
|
|
142
|
-
switch (e
|
|
158
|
+
switch ((e as any)?.code) {
|
|
143
159
|
case "ENOENT":
|
|
144
160
|
case "ENOTDIR":
|
|
145
161
|
case "ERR_MODULE_NOT_FOUND":
|
|
@@ -150,20 +166,18 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
150
166
|
}
|
|
151
167
|
}
|
|
152
168
|
|
|
153
|
-
// Ensure module is a valid object before processing.
|
|
154
|
-
if (!module || typeof module !== "object") {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
169
|
// Store current route in request.
|
|
159
170
|
request.route = route;
|
|
160
171
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
// Ensure module is a valid object before processing.
|
|
173
|
+
if (module && typeof module === "object") {
|
|
174
|
+
response =
|
|
175
|
+
typeof module.default === "function"
|
|
176
|
+
? // Call functions with context as `this` and props as parameters,
|
|
177
|
+
await module.default.call(context, props)
|
|
178
|
+
: // otherwise return default export.
|
|
179
|
+
module.default;
|
|
180
|
+
}
|
|
167
181
|
|
|
168
182
|
if (reply.sent) {
|
|
169
183
|
return;
|
|
@@ -174,28 +188,31 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
174
188
|
reply.status(404);
|
|
175
189
|
}
|
|
176
190
|
break;
|
|
177
|
-
} else if (typeof response === "string" || Buffer.isBuffer(response) || isJSX(response)) {
|
|
178
|
-
break;
|
|
179
191
|
} else if (
|
|
180
|
-
|
|
181
|
-
|
|
192
|
+
typeof response === "string" ||
|
|
193
|
+
response instanceof Readable ||
|
|
194
|
+
Buffer.isBuffer(response) ||
|
|
195
|
+
isJSX(response)
|
|
182
196
|
) {
|
|
197
|
+
break;
|
|
198
|
+
} else if (route.endsWith("/[...guard]") && typeof response === "object") {
|
|
183
199
|
// Add object entries from guard to props
|
|
184
200
|
Object.assign(props, response);
|
|
185
201
|
continue;
|
|
186
|
-
} else if (reply.statusCode === 404) {
|
|
202
|
+
} else if (response === undefined || reply.statusCode === 404) {
|
|
187
203
|
continue;
|
|
188
204
|
} else {
|
|
189
205
|
break;
|
|
190
206
|
}
|
|
191
207
|
}
|
|
192
|
-
|
|
208
|
+
|
|
209
|
+
return await renderResponse(context, response);
|
|
193
210
|
} catch (error) {
|
|
194
211
|
const errorHandler = context["errorHandler"];
|
|
195
212
|
if (typeof errorHandler === "function") {
|
|
196
213
|
reply.status(500);
|
|
197
214
|
response = await errorHandler.call(context, error);
|
|
198
|
-
return await
|
|
215
|
+
return await renderResponse(context, response);
|
|
199
216
|
} else {
|
|
200
217
|
throw error;
|
|
201
218
|
}
|
|
@@ -209,6 +226,7 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
209
226
|
*
|
|
210
227
|
* [
|
|
211
228
|
* "/[...guard]","/a/[...guard]","/a/b/[...guard]","/a/b/c/[...guard]",
|
|
229
|
+
* "/a/b/c",
|
|
212
230
|
* "/a/b/[c]","/a/b/c/[index]",
|
|
213
231
|
* "/a/b/c/[...path]","/a/b/[...path]","/a/[...path]","/[...path]",
|
|
214
232
|
* "/a/b/c/[404]","/a/b/[404]","/a/[404]","/[404]"
|
|
@@ -218,36 +236,39 @@ function generateRoutes(path: string): string[] {
|
|
|
218
236
|
const routes = [];
|
|
219
237
|
|
|
220
238
|
// Transform given path into array of all its segments.
|
|
221
|
-
// "/a/b/c" => ["/a
|
|
239
|
+
// "/a/b/c" => ["", "/a", "/a/b/", "/a/b/c"]
|
|
222
240
|
const segments = [""];
|
|
223
|
-
let
|
|
241
|
+
let edgeSegment = "";
|
|
224
242
|
for (const segment of path.split("/")) {
|
|
225
243
|
// Ignore redundant slashes.
|
|
226
244
|
if (segment !== "") {
|
|
227
|
-
|
|
228
|
-
segments.push(
|
|
245
|
+
edgeSegment += `/${segment}`;
|
|
246
|
+
segments.push(edgeSegment);
|
|
229
247
|
}
|
|
230
248
|
}
|
|
231
|
-
segments.reverse();
|
|
232
249
|
|
|
233
|
-
// [...guard]s are
|
|
234
|
-
for (let i =
|
|
250
|
+
// [...guard]s are pushed from root to edge.
|
|
251
|
+
for (let i = 0; i < segments.length; i++) {
|
|
235
252
|
routes.push(`${segments[i]}/[...guard]`);
|
|
236
253
|
}
|
|
237
254
|
|
|
255
|
+
// Append the verbatim path for static file serving.
|
|
256
|
+
routes.push(path);
|
|
257
|
+
|
|
238
258
|
// "/a/b/c" => ["/a/b/[c]", "/a/b/c/[index]"]
|
|
239
|
-
const edgeSegment = segments[0];
|
|
240
259
|
const lastSlash = edgeSegment.lastIndexOf("/") + 1;
|
|
241
260
|
if (lastSlash > 0) {
|
|
242
261
|
routes.push(`${edgeSegment.substring(0, lastSlash)}[${edgeSegment.substring(lastSlash)}]`);
|
|
243
262
|
}
|
|
244
263
|
routes.push(`${edgeSegment}/[index]`);
|
|
245
264
|
|
|
246
|
-
|
|
265
|
+
// [...path]s are pushed from edge to root.
|
|
266
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
247
267
|
routes.push(`${segments[i]}/[...path]`);
|
|
248
268
|
}
|
|
249
269
|
|
|
250
|
-
|
|
270
|
+
// [404]s are pushed from edge to root.
|
|
271
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
251
272
|
routes.push(`${segments[i]}/[404]`);
|
|
252
273
|
}
|
|
253
274
|
|
|
@@ -264,8 +285,10 @@ function isJSX(obj: unknown): boolean {
|
|
|
264
285
|
/**
|
|
265
286
|
* Renders JSX to string and applies optional response handler.
|
|
266
287
|
*/
|
|
267
|
-
async function
|
|
268
|
-
const payload = isJSX(response)
|
|
288
|
+
async function renderResponse(context: any, response: unknown) {
|
|
289
|
+
const payload = isJSX(response)
|
|
290
|
+
? await jsxToString.call(context, response as JSX.Element)
|
|
291
|
+
: response;
|
|
269
292
|
|
|
270
293
|
// Post-process the payload with an optional response handler
|
|
271
294
|
const responseHandler = context["responseHandler"];
|
|
@@ -273,3 +296,36 @@ async function renderJSX(context: object, response: unknown) {
|
|
|
273
296
|
? await responseHandler.call(context, payload)
|
|
274
297
|
: payload;
|
|
275
298
|
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Returns stream and metadata for requested file.
|
|
302
|
+
*/
|
|
303
|
+
async function tryFile(request: FastifyRequest): Promise<BaseSendResult | undefined> {
|
|
304
|
+
// Production: Retrieve files only from pre-initialized mapping.
|
|
305
|
+
// This avoids potential path traversal vulnerabilities caused
|
|
306
|
+
// by unexpected `request.path` values.
|
|
307
|
+
const file = FILE_BY_PATH[request.path];
|
|
308
|
+
if (file) {
|
|
309
|
+
return await fastifySend(request.raw, file, FASTIFY_SEND_OPTIONS);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (NODE_ENV_IS_DEVELOPMENT) {
|
|
313
|
+
for (const directory of ["dist", "public"]) {
|
|
314
|
+
try {
|
|
315
|
+
if ((await stat(join(CWD, directory, request.path))).isFile()) {
|
|
316
|
+
// Dynamic path loading is restricted to development environments;
|
|
317
|
+
// therefore, production-level path validation is not required here.
|
|
318
|
+
return await fastifySend(
|
|
319
|
+
request.raw,
|
|
320
|
+
`${directory}${request.path}`,
|
|
321
|
+
FASTIFY_SEND_OPTIONS,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|