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 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].forEach(async (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
- const result = await esbuild.build(options);
62
- if (options === SERVER_OPTIONS) {
63
- // Create metadata file with all routes
64
- if (result.metafile?.outputs) {
65
- const routes = Object.keys(result.metafile.outputs)
66
- .filter((path) => /\[.+\]\.js$/.test(path))
67
- .map((path) => path.slice("dist".length, -".js".length));
68
- await writeFile(
69
- join(CWD, "dist", "[--metadata--].js"),
70
- `export default ${JSON.stringify({ routes })};`,
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.7.5",
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/static": "9.1.3",
36
- "@types/node": "26.0.1",
35
+ "@fastify/send": "4.1.0",
36
+ "@types/node": "26.1.0",
37
37
  "esbuild": "0.28.1",
38
- "fastify": "5.9.0",
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 fastifyStatic from "@fastify/static";
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
- if (!NODE_ENV_IS_DEVELOPMENT) {
16
- const { routes } = (await import(`file://${join(CWD, "dist", "[--metadata--].js")}`)).default;
17
- for (const route of routes) {
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
- }).register(fastifyStatic, {
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.code) {
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
- response = typeof module.default === "function" ? (
90
- // Call functions with context as `this` and props as parameters,
91
- await module.default.call(context, props)
92
- ) : (
93
- // otherwise return default export.
94
- module.default
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]") && (response === void 0 || typeof response === "object")) {
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 renderJSX(context, response);
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 renderJSX(context, response);
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 current = "";
131
+ let edgeSegment = "";
130
132
  for (const segment of path.split("/")) {
131
133
  if (segment !== "") {
132
- current += `/${segment}`;
133
- segments.push(current);
134
+ edgeSegment += `/${segment}`;
135
+ segments.push(edgeSegment);
134
136
  }
135
137
  }
136
- segments.reverse();
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
- const edgeSegment = segments[0];
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 = 0; i < segments.length; i++) {
147
+ for (let i = segments.length - 1; i >= 0; i--) {
147
148
  routes.push(`${segments[i]}/[...path]`);
148
149
  }
149
- for (let i = 0; i < segments.length; 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 renderJSX(context, response) {
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": "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",
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 fastifyStatic, { FastifyStaticOptions } from "@fastify/static";
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
- // Mapping for route modules used in non-development environments.
23
- const MODULE_BY_ROUTE: Record<string, { default: Function } | string> = {};
24
-
25
- // Initialize the mapping with absolute paths for all known modules.
26
- // Modules are lazily loaded on their first request for a specific route.
27
- if (!NODE_ENV_IS_DEVELOPMENT) {
28
- const { routes } = (await import(`file://${join(CWD, "dist", "[--metadata--].js")}`)).default as {
29
- routes: string[];
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
- path: string; // Path without query parameters
40
- route: string; // Path to resolved route handler
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.code) {
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
- response =
162
- typeof module.default === "function"
163
- ? // Call functions with context as `this` and props as parameters,
164
- await module.default.call(context, props)
165
- : // otherwise return default export.
166
- module.default;
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
- route.endsWith("/[...guard]") &&
181
- (response === undefined || typeof response === "object")
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
- return await renderJSX(context, response);
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 renderJSX(context, response);
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/b/c", "/a/b", "/a", ""]
239
+ // "/a/b/c" => ["", "/a", "/a/b/", "/a/b/c"]
222
240
  const segments = [""];
223
- let current = "";
241
+ let edgeSegment = "";
224
242
  for (const segment of path.split("/")) {
225
243
  // Ignore redundant slashes.
226
244
  if (segment !== "") {
227
- current += `/${segment}`;
228
- segments.push(current);
245
+ edgeSegment += `/${segment}`;
246
+ segments.push(edgeSegment);
229
247
  }
230
248
  }
231
- segments.reverse();
232
249
 
233
- // [...guard]s are evaluated from top to bottom
234
- for (let i = segments.length - 1; i >= 0; 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
- for (let i = 0; i < segments.length; i++) {
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
- for (let i = 0; i < segments.length; i++) {
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 renderJSX(context: object, response: unknown) {
268
- const payload = isJSX(response) ? await jsxToString.call(context, response) : 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
+ }