jeasx 2.7.1 → 2.7.3

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/esbuild.config.js CHANGED
@@ -64,7 +64,7 @@ const BROWSER_OPTIONS = {
64
64
  if (result.metafile?.outputs) {
65
65
  const routes = Object.keys(result.metafile.outputs)
66
66
  .filter((path) => /\[.+\]\.js$/.test(path))
67
- .map((path) => path.slice("dist".length));
67
+ .map((path) => path.slice("dist".length, -".js".length));
68
68
  await writeFile(
69
69
  join(CWD, "dist", `[--metadata--].js`),
70
70
  `export default ${JSON.stringify({ routes })};`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeasx",
3
- "version": "2.7.1",
3
+ "version": "2.7.3",
4
4
  "description": "Jeasx - the ease of JSX with the power of SSR",
5
5
  "keywords": [
6
6
  "async",
@@ -33,12 +33,12 @@
33
33
  "@fastify/formbody": "8.0.2",
34
34
  "@fastify/multipart": "10.0.0",
35
35
  "@fastify/static": "9.1.3",
36
- "@types/node": "25.9.2",
37
- "esbuild": "0.28.0",
36
+ "@types/node": "25.9.3",
37
+ "esbuild": "0.28.1",
38
38
  "fastify": "5.8.5",
39
- "jsx-async-runtime": "2.1.2"
39
+ "jsx-async-runtime": "2.1.3"
40
40
  },
41
41
  "allowScripts": {
42
- "esbuild@0.28.0": true
42
+ "esbuild": true
43
43
  }
44
44
  }
package/serverless.js CHANGED
@@ -11,12 +11,10 @@ env();
11
11
  const CWD = process.cwd();
12
12
  const CONFIG = (await import(`file://${join(CWD, "jeasx.config.js")}`)).default;
13
13
  const NODE_ENV_IS_DEVELOPMENT = process.env.NODE_ENV === "development";
14
- const MODULE_BY_ROUTE = /* @__PURE__ */ new Map();
14
+ const MODULE_BY_ROUTE = {};
15
15
  if (!NODE_ENV_IS_DEVELOPMENT) {
16
16
  const { routes } = (await import(`file://${join(CWD, "dist", "[--metadata--].js")}`)).default;
17
- for (const route of routes) {
18
- MODULE_BY_ROUTE.set(route, null);
19
- }
17
+ routes.forEach((route) => MODULE_BY_ROUTE[route] = null);
20
18
  }
21
19
  const FASTIFY_SERVER = CONFIG.FASTIFY_SERVER ?? ((fastify2) => fastify2);
22
20
  var serverless_default = FASTIFY_SERVER(
@@ -34,7 +32,6 @@ var serverless_default = FASTIFY_SERVER(
34
32
  root: ["public", "dist"].map((dir) => join(CWD, dir)),
35
33
  wildcard: false,
36
34
  globIgnore: ["/**/\\[*\\].js?(.map)"],
37
- // ignore server routes
38
35
  ...CONFIG.FASTIFY_STATIC_OPTIONS?.()
39
36
  }).decorateRequest("route", "").decorateRequest("path", "").addHook("onRequest", async (request) => {
40
37
  const index = request.url.indexOf("?");
@@ -58,8 +55,8 @@ async function handler(request, reply) {
58
55
  const props = { request, reply };
59
56
  try {
60
57
  for (const route of generateRoutes(request.path)) {
61
- let module = MODULE_BY_ROUTE.get(`${route}.js`);
62
- if (!NODE_ENV_IS_DEVELOPMENT && module === void 0) {
58
+ let module = MODULE_BY_ROUTE[route];
59
+ if (module === void 0 && !NODE_ENV_IS_DEVELOPMENT) {
63
60
  continue;
64
61
  }
65
62
  if (module === null || NODE_ENV_IS_DEVELOPMENT && module === void 0) {
@@ -77,7 +74,7 @@ async function handler(request, reply) {
77
74
  }
78
75
  } else {
79
76
  module = await import(`file://${modulePath}`);
80
- MODULE_BY_ROUTE.set(`${route}.js`, module);
77
+ MODULE_BY_ROUTE[route] = module;
81
78
  }
82
79
  } catch (e) {
83
80
  switch (e.code) {
@@ -91,8 +88,7 @@ async function handler(request, reply) {
91
88
  }
92
89
  }
93
90
  request.route = route;
94
- response = // Call functions with 'this' context and props as parameters
95
- typeof module.default === "function" ? await module.default.call(context, props) : module.default;
91
+ response = typeof module.default === "function" ? await module.default.call(context, props) : module.default;
96
92
  if (reply.sent) {
97
93
  return;
98
94
  } else if (route.endsWith("/[404]")) {
@@ -124,29 +120,30 @@ async function handler(request, reply) {
124
120
  }
125
121
  }
126
122
  function generateRoutes(path) {
127
- const segments = generateSegments(path);
128
- const edges = generateEdges(segments[0]);
129
- return [
130
- ...segments.toReversed().map((segment) => `${segment}/[...guard]`),
131
- ...edges.map((edge) => `${edge}`),
132
- ...segments.map((segment) => `${segment}/[...path]`),
133
- ...segments.map((segment) => `${segment}/[404]`)
134
- ];
135
- }
136
- function generateSegments(path) {
137
- return path.split("/").filter((segment) => segment !== "").reduce((acc, segment) => {
138
- acc.push((acc.length > 0 ? acc[acc.length - 1] : "") + "/" + segment);
139
- return acc;
140
- }, []).reverse().concat("");
141
- }
142
- function generateEdges(path) {
143
- const edges = [];
144
- if (path) {
145
- const lastSegment = path.lastIndexOf("/") + 1;
146
- edges.push(`${path.substring(0, lastSegment)}[${path.substring(lastSegment)}]`);
123
+ const routes = [];
124
+ const segments = [""];
125
+ let current = "";
126
+ for (const segment of path.split("/").filter(Boolean)) {
127
+ current += `/${segment}`;
128
+ segments.push(current);
129
+ }
130
+ segments.reverse();
131
+ for (let i = segments.length - 1; i >= 0; i--) {
132
+ routes.push(`${segments[i]}/[...guard]`);
133
+ }
134
+ const edgeSegment = segments[0];
135
+ const lastSlash = edgeSegment.lastIndexOf("/") + 1;
136
+ if (lastSlash > 0) {
137
+ routes.push(`${edgeSegment.substring(0, lastSlash)}[${edgeSegment.substring(lastSlash)}]`);
138
+ }
139
+ routes.push(`${edgeSegment}/[index]`);
140
+ for (let i = 0; i < segments.length; i++) {
141
+ routes.push(`${segments[i]}/[...path]`);
142
+ }
143
+ for (let i = 0; i < segments.length; i++) {
144
+ routes.push(`${segments[i]}/[404]`);
147
145
  }
148
- edges.push(`${path}/[index]`);
149
- return edges;
146
+ return routes;
150
147
  }
151
148
  function isJSX(obj) {
152
149
  return !!obj && typeof obj === "object" && "type" in obj && "props" in obj;
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// Cache for route modules used in non-development environments.\nconst MODULE_BY_ROUTE = new Map<string, { default: Function } | null>();\n\n// Initialize the cache with `null` for all known modules.\n// Modules are lazily loaded on their first request for a specific route.\n// Only routes explicitly initialized with `null` will be loaded.\nif (!NODE_ENV_IS_DEVELOPMENT) {\n const { routes } = (await import(`file://${join(CWD, \"dist\", \"[--metadata--].js\")}`)).default;\n for (const route of routes) {\n MODULE_BY_ROUTE.set(route, null);\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)\"], // ignore server routes\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 via cache\n let module = MODULE_BY_ROUTE.get(`${route}.js`);\n\n // Skip loading the module if the route path was not initialized.\n // This avoids potential path traversal vulnerabilities caused\n // by unexpected `route` values.\n if (!NODE_ENV_IS_DEVELOPMENT && module === undefined) {\n continue;\n }\n\n // Module was not loaded yet?\n if (module === null || (NODE_ENV_IS_DEVELOPMENT && module === undefined)) {\n try {\n const modulePath = join(CWD, \"dist\", `${route}.js`);\n if (NODE_ENV_IS_DEVELOPMENT) {\n if (typeof require === \"function\") {\n // Bun: Remove module from cache before importing\n // as query parameter for import is ignored (see Node).\n if (require.cache[modulePath]) {\n delete require.cache[modulePath];\n }\n module = await import(`file://${modulePath}`);\n } else {\n // Node: Use timestamp as query parameter to update modules.\n const mtime = (await stat(modulePath)).mtime.getTime();\n module = await import(`file://${modulePath}?${mtime}`);\n }\n } else {\n // Load and cache module for non-development\n module = await import(`file://${modulePath}`);\n MODULE_BY_ROUTE.set(`${route}.js`, module);\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\n // Store current route in request\n request.route = route;\n\n response =\n // Call functions with 'this' context and props as parameters\n typeof module.default === \"function\"\n ? await module.default.call(context, props)\n : module.default; // otherwise return default export\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 */\nfunction generateRoutes(path: string): string[] {\n // \"/a/b/c\" => [\"/a/b/c\", \"/a/b\", \"/a\", \"\"]\n const segments = generateSegments(path);\n\n // \"/a/b/c\" => [\"/a/b/[c]\", \"/a/b/c/[index]\"]\n const edges = generateEdges(segments[0]);\n\n return [\n ...segments\n .toReversed() // [...guard]s are evaluated from top to bottom\n .map((segment) => `${segment}/[...guard]`),\n ...edges.map((edge) => `${edge}`),\n ...segments.map((segment) => `${segment}/[...path]`),\n ...segments.map((segment) => `${segment}/[404]`),\n ];\n}\n\n/**\n * Transforms a given path into an array of all its segments.\n *\n * @example\n * generateSegments(\"/a/b/c\") => [\"/a/b/c\", \"/a/b\", \"/a\", \"\"]\n */\nfunction generateSegments(path: string): string[] {\n return path\n .split(\"/\")\n .filter((segment) => segment !== \"\")\n .reduce((acc, segment) => {\n acc.push((acc.length > 0 ? acc[acc.length - 1] : \"\") + \"/\" + segment);\n return acc;\n }, [])\n .reverse()\n .concat(\"\");\n}\n\n/**\n * Generates edge routes for the given input path.\n *\n * An edge is either a route with a named segment (e.g. \"/a/b/[c]\")\n * or a route with an \"index\" segment (e.g. \"/a/b/c/[index]\").\n */\nfunction generateEdges(path: string): string[] {\n const edges = [];\n if (path) {\n const lastSegment = path.lastIndexOf(\"/\") + 1;\n edges.push(`${path.substring(0, lastSegment)}[${path.substring(lastSegment)}]`);\n }\n edges.push(`${path}/[index]`);\n return edges;\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 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// Cache for route modules used in non-development environments.\nconst MODULE_BY_ROUTE: Record<string, { default: Function }> = {};\n\n// Initialize the cache with `null` for all known modules.\n// Modules are lazily loaded on their first request for a specific route.\n// Only routes explicitly initialized with `null` will be loaded.\nif (!NODE_ENV_IS_DEVELOPMENT) {\n const { routes } = (await import(`file://${join(CWD, \"dist\", \"[--metadata--].js\")}`)).default as {\n routes: string[];\n };\n routes.forEach((route) => (MODULE_BY_ROUTE[route] = null));\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 via cache\n let module = MODULE_BY_ROUTE[route];\n\n // Skip loading the module if the route path was not initialized.\n // This avoids potential path traversal vulnerabilities caused\n // by unexpected `route` values.\n if (module === undefined && !NODE_ENV_IS_DEVELOPMENT) {\n continue;\n }\n\n // Module was not loaded yet?\n if (module === null || (NODE_ENV_IS_DEVELOPMENT && module === undefined)) {\n try {\n const modulePath = join(CWD, \"dist\", `${route}.js`);\n if (NODE_ENV_IS_DEVELOPMENT) {\n if (typeof require === \"function\") {\n // Bun: Remove module from cache before importing\n // as query parameter for import is ignored (see Node).\n if (require.cache[modulePath]) {\n delete require.cache[modulePath];\n }\n module = await import(`file://${modulePath}`);\n } else {\n // Node: Use timestamp as query parameter to update modules.\n const mtime = (await stat(modulePath)).mtime.getTime();\n module = await import(`file://${modulePath}?${mtime}`);\n }\n } else {\n // Load and cache module for non-development\n module = await import(`file://${modulePath}`);\n MODULE_BY_ROUTE[route] = module;\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\n // Store current route in request\n request.route = route;\n\n // Call functions with 'this' context and props as parameters\n // otherwise return default export\n response =\n typeof module.default === \"function\"\n ? await module.default.call(context, props)\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(\"/\").filter(Boolean)) {\n current += `/${segment}`;\n segments.push(current);\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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",
6
6
  "names": ["fastify"]
7
7
  }
package/serverless.ts CHANGED
@@ -20,16 +20,16 @@ const CONFIG = (await import(`file://${join(CWD, "jeasx.config.js")}`)).default;
20
20
  const NODE_ENV_IS_DEVELOPMENT = process.env.NODE_ENV === "development";
21
21
 
22
22
  // Cache for route modules used in non-development environments.
23
- const MODULE_BY_ROUTE = new Map<string, { default: Function } | null>();
23
+ const MODULE_BY_ROUTE: Record<string, { default: Function }> = {};
24
24
 
25
25
  // Initialize the cache with `null` for all known modules.
26
26
  // Modules are lazily loaded on their first request for a specific route.
27
27
  // Only routes explicitly initialized with `null` will be loaded.
28
28
  if (!NODE_ENV_IS_DEVELOPMENT) {
29
- const { routes } = (await import(`file://${join(CWD, "dist", "[--metadata--].js")}`)).default;
30
- for (const route of routes) {
31
- MODULE_BY_ROUTE.set(route, null);
32
- }
29
+ const { routes } = (await import(`file://${join(CWD, "dist", "[--metadata--].js")}`)).default as {
30
+ routes: string[];
31
+ };
32
+ routes.forEach((route) => (MODULE_BY_ROUTE[route] = null));
33
33
  }
34
34
 
35
35
  declare module "fastify" {
@@ -65,7 +65,7 @@ export default FASTIFY_SERVER(
65
65
  .register(fastifyStatic, {
66
66
  root: ["public", "dist"].map((dir) => join(CWD, dir)),
67
67
  wildcard: false,
68
- globIgnore: ["/**/\\[*\\].js?(.map)"], // ignore server routes
68
+ globIgnore: ["/**/\\[*\\].js?(.map)"],
69
69
  ...(CONFIG.FASTIFY_STATIC_OPTIONS?.() as FastifyStaticOptions),
70
70
  })
71
71
  .decorateRequest("route", "")
@@ -108,12 +108,12 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
108
108
  // Execute route handlers for current request
109
109
  for (const route of generateRoutes(request.path)) {
110
110
  // Resolve module via cache
111
- let module = MODULE_BY_ROUTE.get(`${route}.js`);
111
+ let module = MODULE_BY_ROUTE[route];
112
112
 
113
113
  // Skip loading the module if the route path was not initialized.
114
114
  // This avoids potential path traversal vulnerabilities caused
115
115
  // by unexpected `route` values.
116
- if (!NODE_ENV_IS_DEVELOPMENT && module === undefined) {
116
+ if (module === undefined && !NODE_ENV_IS_DEVELOPMENT) {
117
117
  continue;
118
118
  }
119
119
 
@@ -137,7 +137,7 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
137
137
  } else {
138
138
  // Load and cache module for non-development
139
139
  module = await import(`file://${modulePath}`);
140
- MODULE_BY_ROUTE.set(`${route}.js`, module);
140
+ MODULE_BY_ROUTE[route] = module;
141
141
  }
142
142
  } catch (e) {
143
143
  switch (e.code) {
@@ -155,11 +155,12 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
155
155
  // Store current route in request
156
156
  request.route = route;
157
157
 
158
+ // Call functions with 'this' context and props as parameters
159
+ // otherwise return default export
158
160
  response =
159
- // Call functions with 'this' context and props as parameters
160
161
  typeof module.default === "function"
161
162
  ? await module.default.call(context, props)
162
- : module.default; // otherwise return default export
163
+ : module.default;
163
164
 
164
165
  if (reply.sent) {
165
166
  return;
@@ -200,56 +201,51 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
200
201
 
201
202
  /**
202
203
  * Generates all possible routes based on the given input path.
204
+ *
205
+ * Example routes for "/a/b/c":
206
+ *
207
+ * [
208
+ * "/[...guard]","/a/[...guard]","/a/b/[...guard]","/a/b/c/[...guard]",
209
+ * "/a/b/[c]","/a/b/c/[index]",
210
+ * "/a/b/c/[...path]","/a/b/[...path]","/a/[...path]","/[...path]",
211
+ * "/a/b/c/[404]","/a/b/[404]","/a/[404]","/[404]"
212
+ * ]
203
213
  */
204
214
  function generateRoutes(path: string): string[] {
215
+ const routes = [];
216
+
217
+ // Transform given path into array of all its segments.
205
218
  // "/a/b/c" => ["/a/b/c", "/a/b", "/a", ""]
206
- const segments = generateSegments(path);
219
+ const segments = [""];
220
+ let current = "";
221
+ for (const segment of path.split("/").filter(Boolean)) {
222
+ current += `/${segment}`;
223
+ segments.push(current);
224
+ }
225
+ segments.reverse();
207
226
 
208
- // "/a/b/c" => ["/a/b/[c]", "/a/b/c/[index]"]
209
- const edges = generateEdges(segments[0]);
227
+ // [...guard]s are evaluated from top to bottom
228
+ for (let i = segments.length - 1; i >= 0; i--) {
229
+ routes.push(`${segments[i]}/[...guard]`);
230
+ }
210
231
 
211
- return [
212
- ...segments
213
- .toReversed() // [...guard]s are evaluated from top to bottom
214
- .map((segment) => `${segment}/[...guard]`),
215
- ...edges.map((edge) => `${edge}`),
216
- ...segments.map((segment) => `${segment}/[...path]`),
217
- ...segments.map((segment) => `${segment}/[404]`),
218
- ];
219
- }
232
+ // "/a/b/c" => ["/a/b/[c]", "/a/b/c/[index]"]
233
+ const edgeSegment = segments[0];
234
+ const lastSlash = edgeSegment.lastIndexOf("/") + 1;
235
+ if (lastSlash > 0) {
236
+ routes.push(`${edgeSegment.substring(0, lastSlash)}[${edgeSegment.substring(lastSlash)}]`);
237
+ }
238
+ routes.push(`${edgeSegment}/[index]`);
220
239
 
221
- /**
222
- * Transforms a given path into an array of all its segments.
223
- *
224
- * @example
225
- * generateSegments("/a/b/c") => ["/a/b/c", "/a/b", "/a", ""]
226
- */
227
- function generateSegments(path: string): string[] {
228
- return path
229
- .split("/")
230
- .filter((segment) => segment !== "")
231
- .reduce((acc, segment) => {
232
- acc.push((acc.length > 0 ? acc[acc.length - 1] : "") + "/" + segment);
233
- return acc;
234
- }, [])
235
- .reverse()
236
- .concat("");
237
- }
240
+ for (let i = 0; i < segments.length; i++) {
241
+ routes.push(`${segments[i]}/[...path]`);
242
+ }
238
243
 
239
- /**
240
- * Generates edge routes for the given input path.
241
- *
242
- * An edge is either a route with a named segment (e.g. "/a/b/[c]")
243
- * or a route with an "index" segment (e.g. "/a/b/c/[index]").
244
- */
245
- function generateEdges(path: string): string[] {
246
- const edges = [];
247
- if (path) {
248
- const lastSegment = path.lastIndexOf("/") + 1;
249
- edges.push(`${path.substring(0, lastSegment)}[${path.substring(lastSegment)}]`);
244
+ for (let i = 0; i < segments.length; i++) {
245
+ routes.push(`${segments[i]}/[404]`);
250
246
  }
251
- edges.push(`${path}/[index]`);
252
- return edges;
247
+
248
+ return routes;
253
249
  }
254
250
 
255
251
  /**