jeasx 2.7.4 → 2.8.0

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