jeasx 2.8.0 → 2.9.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/package.json +4 -7
- package/serverless.js +14 -26
- package/serverless.js.map +2 -2
- package/serverless.ts +57 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jeasx",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Jeasx - the ease of JSX with the power of SSR",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"async",
|
|
@@ -29,14 +29,11 @@
|
|
|
29
29
|
"build": "esbuild --platform=node --format=esm --sourcemap=linked --outdir=. serverless.ts"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@fastify/cookie": "11.0.2",
|
|
33
|
-
"@fastify/formbody": "8.0.2",
|
|
34
|
-
"@fastify/multipart": "10.0.0",
|
|
35
32
|
"@fastify/send": "4.1.0",
|
|
36
|
-
"@types/node": "26.1.
|
|
33
|
+
"@types/node": "26.1.1",
|
|
37
34
|
"esbuild": "0.28.1",
|
|
38
|
-
"fastify": "5.
|
|
39
|
-
"jsx-async-runtime": "2.
|
|
35
|
+
"fastify": "5.10.0",
|
|
36
|
+
"jsx-async-runtime": "2.2.0"
|
|
40
37
|
},
|
|
41
38
|
"allowScripts": {
|
|
42
39
|
"esbuild": true
|
package/serverless.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import fastifyCookie from "@fastify/cookie";
|
|
2
|
-
import fastifyFormbody from "@fastify/formbody";
|
|
3
|
-
import fastifyMultipart from "@fastify/multipart";
|
|
4
1
|
import fastifySend from "@fastify/send";
|
|
5
2
|
import fastify from "fastify";
|
|
6
3
|
import { jsxToString } from "jsx-async-runtime";
|
|
@@ -12,23 +9,14 @@ env();
|
|
|
12
9
|
const CWD = process.cwd();
|
|
13
10
|
const CONFIG = (await import(`file://${join(CWD, "jeasx.config.js")}`)).default;
|
|
14
11
|
const NODE_ENV_IS_DEVELOPMENT = process.env.NODE_ENV === "development";
|
|
15
|
-
const {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
12
|
+
const {
|
|
13
|
+
routes: MODULE_BY_ROUTE,
|
|
14
|
+
files: FILE_BY_PATH
|
|
15
|
+
} = NODE_ENV_IS_DEVELOPMENT ? { routes: {}, files: {} } : (await import(`file://${join(CWD, "dist", "[--metadata--].js")}`)).default;
|
|
16
|
+
const FASTIFY_SEND_OPTIONS = CONFIG.FASTIFY_SEND_OPTIONS?.();
|
|
19
17
|
const FASTIFY_SERVER = CONFIG.FASTIFY_SERVER ?? ((fastify2) => fastify2);
|
|
20
18
|
var serverless_default = FASTIFY_SERVER(
|
|
21
|
-
fastify({
|
|
22
|
-
...CONFIG.FASTIFY_SERVER_OPTIONS?.()
|
|
23
|
-
})
|
|
24
|
-
).register((fastify2) => {
|
|
25
|
-
fastify2.register(fastifyCookie, {
|
|
26
|
-
...CONFIG.FASTIFY_COOKIE_OPTIONS?.()
|
|
27
|
-
}).register(fastifyFormbody, {
|
|
28
|
-
...CONFIG.FASTIFY_FORMBODY_OPTIONS?.()
|
|
29
|
-
}).register(fastifyMultipart, {
|
|
30
|
-
...CONFIG.FASTIFY_MULTIPART_OPTIONS?.()
|
|
31
|
-
}).decorateRequest("route", "").decorateRequest("path", "").addHook("onRequest", async (request) => {
|
|
19
|
+
fastify(CONFIG.FASTIFY_SERVER_OPTIONS?.()).decorateRequest("route", "").decorateRequest("path", "").decorateReply("file", void 0).addHook("onRequest", async (request) => {
|
|
32
20
|
const index = request.url.indexOf("?");
|
|
33
21
|
request.path = index === -1 ? request.url : request.url.slice(0, index);
|
|
34
22
|
}).all("*", async (request, reply) => {
|
|
@@ -42,20 +30,20 @@ var serverless_default = FASTIFY_SERVER(
|
|
|
42
30
|
request.log.error(error);
|
|
43
31
|
throw error;
|
|
44
32
|
}
|
|
45
|
-
})
|
|
46
|
-
|
|
33
|
+
})
|
|
34
|
+
);
|
|
47
35
|
async function handler(request, reply) {
|
|
48
36
|
let response;
|
|
49
37
|
const context = {};
|
|
50
38
|
const props = { request, reply };
|
|
51
39
|
try {
|
|
40
|
+
reply.file = await tryFile(request);
|
|
52
41
|
for (const route of generateRoutes(request.path)) {
|
|
53
42
|
if (route === request.path) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
reply.
|
|
57
|
-
reply.
|
|
58
|
-
response = sendResult.stream;
|
|
43
|
+
if (reply.file) {
|
|
44
|
+
reply.status(reply.file.statusCode);
|
|
45
|
+
reply.headers(reply.file.headers);
|
|
46
|
+
response = reply.file.stream;
|
|
59
47
|
break;
|
|
60
48
|
}
|
|
61
49
|
continue;
|
|
@@ -76,7 +64,7 @@ async function handler(request, reply) {
|
|
|
76
64
|
module = await import(`file://${modulePath}?${mtime}`);
|
|
77
65
|
}
|
|
78
66
|
} catch (e) {
|
|
79
|
-
switch (e
|
|
67
|
+
switch (e?.code) {
|
|
80
68
|
case "ENOENT":
|
|
81
69
|
case "ENOTDIR":
|
|
82
70
|
case "ERR_MODULE_NOT_FOUND":
|
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 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": "AAAA,OAAO,
|
|
4
|
+
"sourcesContent": ["import 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 {\n routes: MODULE_BY_ROUTE,\n files: FILE_BY_PATH,\n}: {\n routes: Record<string, string | { default: Function }>;\n files: Record<string, string>;\n} = NODE_ENV_IS_DEVELOPMENT\n ? { routes: {}, files: {} }\n : (await import(`file://${join(CWD, \"dist\", \"[--metadata--].js\")}`)).default;\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 = CONFIG.FASTIFY_SEND_OPTIONS?.() as SendOptions;\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(CONFIG.FASTIFY_SERVER_OPTIONS?.() as FastifyServerOptions)\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,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fastifyFormbody, { FastifyFormbodyOptions } from "@fastify/formbody";
|
|
3
|
-
import fastifyMultipart, { FastifyMultipartOptions } from "@fastify/multipart";
|
|
4
|
-
import fastifySend, { SendOptions, SendResult } from "@fastify/send";
|
|
1
|
+
import fastifySend, { BaseSendResult, SendOptions } from "@fastify/send";
|
|
5
2
|
import fastify, {
|
|
6
3
|
FastifyInstance,
|
|
7
4
|
FastifyReply,
|
|
@@ -23,23 +20,31 @@ const NODE_ENV_IS_DEVELOPMENT = process.env.NODE_ENV === "development";
|
|
|
23
20
|
// Map routes and files for non-development environments from metadata export.
|
|
24
21
|
// Module paths are initialized at startup but overwritten
|
|
25
22
|
// with resolved modules upon the first request.
|
|
26
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
routes: MODULE_BY_ROUTE,
|
|
25
|
+
files: FILE_BY_PATH,
|
|
26
|
+
}: {
|
|
27
|
+
routes: Record<string, string | { default: Function }>;
|
|
28
|
+
files: Record<string, string>;
|
|
29
|
+
} = NODE_ENV_IS_DEVELOPMENT
|
|
27
30
|
? { routes: {}, files: {} }
|
|
28
|
-
: (
|
|
29
|
-
routes: Record<string, string | { default: Function }>;
|
|
30
|
-
files: Record<string, string>;
|
|
31
|
-
});
|
|
31
|
+
: (await import(`file://${join(CWD, "dist", "[--metadata--].js")}`)).default;
|
|
32
32
|
|
|
33
33
|
declare module "fastify" {
|
|
34
34
|
interface FastifyRequest {
|
|
35
|
-
|
|
36
|
-
|
|
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;
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
const FASTIFY_SEND_OPTIONS =
|
|
41
|
-
...(CONFIG.FASTIFY_SEND_OPTIONS?.() as SendOptions),
|
|
42
|
-
};
|
|
47
|
+
const FASTIFY_SEND_OPTIONS = CONFIG.FASTIFY_SEND_OPTIONS?.() as SendOptions;
|
|
43
48
|
|
|
44
49
|
// Enhance Fastify server from userland
|
|
45
50
|
const FASTIFY_SERVER = (CONFIG.FASTIFY_SERVER ?? ((fastify) => fastify)) as (
|
|
@@ -48,45 +53,31 @@ const FASTIFY_SERVER = (CONFIG.FASTIFY_SERVER ?? ((fastify) => fastify)) as (
|
|
|
48
53
|
|
|
49
54
|
// Create and export a Fastify instance
|
|
50
55
|
export default FASTIFY_SERVER(
|
|
51
|
-
fastify(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.decorateRequest("path", "")
|
|
69
|
-
.addHook("onRequest", async (request) => {
|
|
70
|
-
// Extract path from url
|
|
71
|
-
const index = request.url.indexOf("?");
|
|
72
|
-
request.path = index === -1 ? request.url : request.url.slice(0, index);
|
|
73
|
-
})
|
|
74
|
-
.all("*", async (request: FastifyRequest, reply: FastifyReply) => {
|
|
75
|
-
try {
|
|
76
|
-
const payload = await handler(request, reply);
|
|
77
|
-
if (
|
|
78
|
-
reply.getHeader("content-type") === undefined &&
|
|
79
|
-
(typeof payload === "string" || Buffer.isBuffer(payload))
|
|
80
|
-
) {
|
|
81
|
-
reply.type("text/html; charset=utf-8");
|
|
82
|
-
}
|
|
83
|
-
return payload;
|
|
84
|
-
} catch (error) {
|
|
85
|
-
request.log.error(error);
|
|
86
|
-
throw error;
|
|
56
|
+
fastify(CONFIG.FASTIFY_SERVER_OPTIONS?.() as FastifyServerOptions)
|
|
57
|
+
.decorateRequest("route", "")
|
|
58
|
+
.decorateRequest("path", "")
|
|
59
|
+
.decorateReply("file", undefined)
|
|
60
|
+
.addHook("onRequest", async (request) => {
|
|
61
|
+
// Extract path from url
|
|
62
|
+
const index = request.url.indexOf("?");
|
|
63
|
+
request.path = index === -1 ? request.url : request.url.slice(0, index);
|
|
64
|
+
})
|
|
65
|
+
.all("*", async (request: FastifyRequest, reply: FastifyReply) => {
|
|
66
|
+
try {
|
|
67
|
+
const payload = await handler(request, reply);
|
|
68
|
+
if (
|
|
69
|
+
reply.getHeader("content-type") === undefined &&
|
|
70
|
+
(typeof payload === "string" || Buffer.isBuffer(payload))
|
|
71
|
+
) {
|
|
72
|
+
reply.type("text/html; charset=utf-8");
|
|
87
73
|
}
|
|
88
|
-
|
|
89
|
-
|
|
74
|
+
return payload;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
request.log.error(error);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
90
81
|
|
|
91
82
|
/**
|
|
92
83
|
* Resolves route module based on the request path and execute it.
|
|
@@ -95,21 +86,23 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
95
86
|
let response: unknown;
|
|
96
87
|
|
|
97
88
|
// Global context object for route handlers
|
|
98
|
-
const context = {};
|
|
89
|
+
const context: any = {};
|
|
99
90
|
|
|
100
91
|
// Default props for route handlers
|
|
101
92
|
const props = { request, reply };
|
|
102
93
|
|
|
103
94
|
try {
|
|
95
|
+
// Check for static file and store result for later processing.
|
|
96
|
+
reply.file = await tryFile(request);
|
|
97
|
+
|
|
104
98
|
// Execute route handlers for current request
|
|
105
99
|
for (const route of generateRoutes(request.path)) {
|
|
106
100
|
// Try to serve static file when route matches path.
|
|
107
101
|
if (route === request.path) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
reply.
|
|
111
|
-
reply.
|
|
112
|
-
response = sendResult.stream;
|
|
102
|
+
if (reply.file) {
|
|
103
|
+
reply.status(reply.file.statusCode);
|
|
104
|
+
reply.headers(reply.file.headers);
|
|
105
|
+
response = reply.file.stream;
|
|
113
106
|
break;
|
|
114
107
|
}
|
|
115
108
|
continue;
|
|
@@ -145,7 +138,7 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
|
|
|
145
138
|
module = await import(`file://${modulePath}?${mtime}`);
|
|
146
139
|
}
|
|
147
140
|
} catch (e) {
|
|
148
|
-
switch (e
|
|
141
|
+
switch ((e as any)?.code) {
|
|
149
142
|
case "ENOENT":
|
|
150
143
|
case "ENOTDIR":
|
|
151
144
|
case "ERR_MODULE_NOT_FOUND":
|
|
@@ -275,8 +268,10 @@ function isJSX(obj: unknown): boolean {
|
|
|
275
268
|
/**
|
|
276
269
|
* Renders JSX to string and applies optional response handler.
|
|
277
270
|
*/
|
|
278
|
-
async function renderResponse(context:
|
|
279
|
-
const payload = isJSX(response)
|
|
271
|
+
async function renderResponse(context: any, response: unknown) {
|
|
272
|
+
const payload = isJSX(response)
|
|
273
|
+
? await jsxToString.call(context, response as JSX.Element)
|
|
274
|
+
: response;
|
|
280
275
|
|
|
281
276
|
// Post-process the payload with an optional response handler
|
|
282
277
|
const responseHandler = context["responseHandler"];
|
|
@@ -288,7 +283,7 @@ async function renderResponse(context: object, response: unknown) {
|
|
|
288
283
|
/**
|
|
289
284
|
* Returns stream and metadata for requested file.
|
|
290
285
|
*/
|
|
291
|
-
async function tryFile(request: FastifyRequest): Promise<
|
|
286
|
+
async function tryFile(request: FastifyRequest): Promise<BaseSendResult | undefined> {
|
|
292
287
|
// Production: Retrieve files only from pre-initialized mapping.
|
|
293
288
|
// This avoids potential path traversal vulnerabilities caused
|
|
294
289
|
// by unexpected `request.path` values.
|