jeasx 2.10.5 → 2.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeasx",
3
- "version": "2.10.5",
3
+ "version": "2.10.7",
4
4
  "description": "Jeasx - the ease of JSX with the power of SSR",
5
5
  "keywords": [
6
6
  "async",
@@ -29,11 +29,11 @@
29
29
  "build": "esbuild --platform=node --format=esm --sourcemap=linked --outdir=. serverless.ts"
30
30
  },
31
31
  "dependencies": {
32
- "@fastify/send": "4.1.1",
33
- "@types/node": "26.6.3",
32
+ "@fastify/send": "4.2.0",
33
+ "@types/node": "26.6.5",
34
34
  "esbuild": "0.28.2",
35
- "fastify": "5.12.5",
36
- "jsx-async-runtime": "2.2.1"
35
+ "fastify": "5.12.7",
36
+ "jsx-async-runtime": "2.3.0"
37
37
  },
38
38
  "allowScripts": {
39
39
  "esbuild": true
package/serverless.js CHANGED
@@ -20,8 +20,9 @@ var serverless_default = FASTIFY_SERVER(
20
20
  const index = request.url.indexOf("?");
21
21
  request.path = index === -1 ? request.url : request.url.slice(0, index);
22
22
  }).all("*", async (request, reply) => {
23
+ let payload;
23
24
  try {
24
- const payload = await handler(request, reply);
25
+ payload = await handler(request, reply);
25
26
  if (reply.getHeader("content-type") === void 0 && (typeof payload === "string" || Buffer.isBuffer(payload))) {
26
27
  reply.type("text/html; charset=utf-8");
27
28
  }
@@ -29,9 +30,15 @@ var serverless_default = FASTIFY_SERVER(
29
30
  } catch (error) {
30
31
  request.log.error(error);
31
32
  throw error;
33
+ } finally {
34
+ const stream = reply.file?.stream;
35
+ if (stream && stream !== payload && stream.readableFlowing === null) {
36
+ stream.destroy();
37
+ }
32
38
  }
33
39
  })
34
40
  );
41
+ const STATIC_FILE_ROUTE = /* @__PURE__ */ Symbol("static-file-route");
35
42
  async function handler(request, reply) {
36
43
  let response;
37
44
  const context = {};
@@ -39,7 +46,7 @@ async function handler(request, reply) {
39
46
  try {
40
47
  reply.file = await tryFile(request);
41
48
  for (const route of generateRoutes(request.path)) {
42
- if (route === request.path) {
49
+ if (route === STATIC_FILE_ROUTE) {
43
50
  if (reply.file) {
44
51
  reply.status(reply.file.statusCode);
45
52
  reply.headers(reply.file.headers);
@@ -90,9 +97,9 @@ async function handler(request, reply) {
90
97
  reply.status(404);
91
98
  }
92
99
  break;
93
- } else if (typeof response === "string" || response instanceof Readable || Buffer.isBuffer(response) || isJSX(response)) {
100
+ } else if (typeof response === "string" || response instanceof Response || response instanceof Readable || response instanceof ReadableStream || Buffer.isBuffer(response) || isJSX(response)) {
94
101
  break;
95
- } else if (route.endsWith("/[...guard]") && typeof response === "object") {
102
+ } else if (typeof response === "object" && route.endsWith("/[...guard]")) {
96
103
  Object.assign(props, response);
97
104
  continue;
98
105
  } else if (response === void 0 || reply.statusCode === 404) {
@@ -125,7 +132,7 @@ function* generateRoutes(path) {
125
132
  for (let i = 0; i < segments.length; i++) {
126
133
  yield `${segments[i]}/[...guard]`;
127
134
  }
128
- yield path;
135
+ yield STATIC_FILE_ROUTE;
129
136
  const lastSlash = edgeSegment.lastIndexOf("/") + 1;
130
137
  if (lastSlash > 0) {
131
138
  yield `${edgeSegment.substring(0, lastSlash)}[${edgeSegment.substring(lastSlash)}]`;
package/serverless.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["serverless.ts"],
4
- "sourcesContent": ["import fastifySend, { BaseSendResult, SendOptions } from \"@fastify/send\";\nimport fastify, {\n FastifyInstance,\n FastifyReply,\n FastifyRequest,\n FastifyServerOptions,\n} from \"fastify\";\nimport { jsxToString } from \"jsx-async-runtime\";\nimport { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport env from \"./env.js\";\n\nenv();\n\nconst CWD = process.cwd();\nconst CONFIG = (await import(`file://${join(CWD, \"jeasx.config.js\")}`)).default;\nconst NODE_ENV_IS_DEVELOPMENT = process.env.NODE_ENV === \"development\";\n\n// Map routes and files for non-development environments from metadata export.\n// Module paths are initialized at startup but overwritten\n// with resolved modules upon the first request.\nconst {\n routes: MODULE_BY_ROUTE,\n files: FILE_BY_PATH,\n}: {\n routes: Record<string, string | { default: Function }>;\n files: Record<string, string>;\n} = NODE_ENV_IS_DEVELOPMENT\n ? { routes: {}, files: {} }\n : (await import(`file://${join(CWD, \"dist\", \"[--metadata--].js\")}`)).default;\n\ndeclare module \"fastify\" {\n interface FastifyRequest {\n /** Path without query parameters */\n path: string;\n /** Path to resolved route handler */\n route: string;\n }\n\n interface FastifyReply {\n /** Populated when serving a static file; otherwise undefined. */\n file?: BaseSendResult;\n }\n}\n\nconst FASTIFY_SEND_OPTIONS = CONFIG.FASTIFY_SEND_OPTIONS?.() as SendOptions;\n\n// Enhance Fastify server from userland\nconst FASTIFY_SERVER = (CONFIG.FASTIFY_SERVER ?? ((fastify) => fastify)) as (\n fastify: FastifyInstance,\n) => FastifyInstance;\n\n// Create and export a Fastify instance\nexport default FASTIFY_SERVER(\n fastify(CONFIG.FASTIFY_SERVER_OPTIONS?.() as FastifyServerOptions)\n .decorateRequest(\"route\", \"\")\n .decorateRequest(\"path\", \"\")\n .decorateReply(\"file\", undefined)\n .addHook(\"onRequest\", async (request) => {\n // Extract path from url\n const index = request.url.indexOf(\"?\");\n request.path = index === -1 ? request.url : request.url.slice(0, index);\n })\n .all(\"*\", async (request: FastifyRequest, reply: FastifyReply) => {\n try {\n const payload = await handler(request, reply);\n if (\n reply.getHeader(\"content-type\") === undefined &&\n (typeof payload === \"string\" || Buffer.isBuffer(payload))\n ) {\n reply.type(\"text/html; charset=utf-8\");\n }\n return payload;\n } catch (error) {\n request.log.error(error);\n throw error;\n }\n }),\n);\n\n/**\n * Resolves route module based on the request path and execute it.\n */\nasync function handler(request: FastifyRequest, reply: FastifyReply) {\n let response: unknown;\n\n // Global context object for route handlers\n const context: any = {};\n\n // Default props for route handlers\n const props = { request, reply };\n\n try {\n // Check for static file and store result for later processing.\n reply.file = await tryFile(request);\n\n // Execute route handlers for current request\n for (const route of generateRoutes(request.path)) {\n // Try to serve static file when route matches path.\n if (route === request.path) {\n if (reply.file) {\n reply.status(reply.file.statusCode);\n reply.headers(reply.file.headers);\n response = reply.file.stream;\n break;\n }\n continue;\n }\n\n // Resolve module or path to module\n let module = MODULE_BY_ROUTE[route];\n\n // Skip processing if the route path was not initialized.\n if (module === undefined && !NODE_ENV_IS_DEVELOPMENT) {\n continue;\n }\n\n // Module was not loaded yet?\n try {\n if (typeof module === \"string\") {\n // Production: Load and cache module only via pre-calculated path.\n // This avoids potential path traversal vulnerabilities caused\n // by unexpected `route` values.\n module = MODULE_BY_ROUTE[route] = await import(`file://${join(CWD, module)}`);\n } else if (module === undefined && NODE_ENV_IS_DEVELOPMENT) {\n // Only map module paths depending on `route` during development.\n const modulePath = join(CWD, \"dist\", `${route}.js`);\n if (typeof require === \"function\" && require.cache[modulePath]) {\n // Bun: Remove module from cache before importing\n // as query parameter for import is ignored.\n delete require.cache[modulePath];\n }\n // Use timestamp as query parameter to update modules.\n const mtime = (await stat(modulePath)).mtime.getTime();\n // Dynamic imports are restricted to development environments;\n // therefore, production-level path validation is not required here.\n module = await import(`file://${modulePath}?${mtime}`);\n }\n } catch (e) {\n switch ((e as any)?.code) {\n case \"ENOENT\":\n case \"ENOTDIR\":\n case \"ERR_MODULE_NOT_FOUND\":\n continue;\n default:\n // Module exists, but fails to load.\n throw e;\n }\n }\n\n // Store current route in request.\n request.route = route;\n\n // Ensure module is a valid object before processing.\n if (module && typeof module === \"object\") {\n response =\n typeof module.default === \"function\"\n ? // Call functions with context as `this` and props as parameters,\n await module.default.call(context, props)\n : // otherwise return default export.\n module.default;\n }\n\n if (reply.sent) {\n return;\n } else if (route.endsWith(\"/[404]\")) {\n // Preserve existing status if a 404 page is requested directly.\n // If no status is defined, set status to 404 automatically.\n if (reply.statusCode === 200 && !request.path.endsWith(\"/404\")) {\n reply.status(404);\n }\n break;\n } else if (\n typeof response === \"string\" ||\n response instanceof Readable ||\n Buffer.isBuffer(response) ||\n isJSX(response)\n ) {\n break;\n } else if (route.endsWith(\"/[...guard]\") && typeof response === \"object\") {\n // Add object entries from guard to props\n Object.assign(props, response);\n continue;\n } else if (response === undefined || reply.statusCode === 404) {\n continue;\n } else {\n break;\n }\n }\n\n return await renderResponse(context, response);\n } catch (error) {\n const errorHandler = context[\"errorHandler\"];\n if (typeof errorHandler === \"function\") {\n reply.status(500);\n response = await errorHandler.call(context, error);\n return await renderResponse(context, response);\n } else {\n throw error;\n }\n }\n}\n\n/**\n * Generates all possible routes based on the given input path.\n *\n * Example routes for \"/a/b/c\":\n *\n * [\n * \"/[...guard]\",\"/a/[...guard]\",\"/a/b/[...guard]\",\"/a/b/c/[...guard]\",\n * \"/a/b/c\",\n * \"/a/b/[c]\",\"/a/b/c/[index]\",\n * \"/a/b/c/[...path]\",\"/a/b/[...path]\",\"/a/[...path]\",\"/[...path]\",\n * \"/a/b/c/[404]\",\"/a/b/[404]\",\"/a/[404]\",\"/[404]\"\n * ]\n */\nfunction* generateRoutes(path: string): Generator<string> {\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 yield `${segments[i]}/[...guard]`;\n }\n\n // Append the verbatim path for static file serving.\n yield path;\n\n // \"/a/b/c\" => [\"/a/b/[c]\", \"/a/b/c/[index]\"]\n const lastSlash = edgeSegment.lastIndexOf(\"/\") + 1;\n if (lastSlash > 0) {\n yield `${edgeSegment.substring(0, lastSlash)}[${edgeSegment.substring(lastSlash)}]`;\n }\n yield `${edgeSegment}/[index]`;\n\n // [...path]s are pushed from edge to root.\n for (let i = segments.length - 1; i >= 0; i--) {\n yield `${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 yield `${segments[i]}/[404]`;\n }\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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",
4
+ "sourcesContent": ["import fastifySend, { BaseSendResult, SendOptions } from \"@fastify/send\";\nimport fastify, {\n FastifyInstance,\n FastifyReply,\n FastifyRequest,\n FastifyServerOptions,\n} from \"fastify\";\nimport { jsxToString } from \"jsx-async-runtime\";\nimport { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport env from \"./env.js\";\n\nenv();\n\nconst CWD = process.cwd();\nconst CONFIG = (await import(`file://${join(CWD, \"jeasx.config.js\")}`)).default;\nconst NODE_ENV_IS_DEVELOPMENT = process.env.NODE_ENV === \"development\";\n\n// Map routes and files for non-development environments from metadata export.\n// Module paths are initialized at startup but overwritten\n// with resolved modules upon the first request.\nconst {\n routes: MODULE_BY_ROUTE,\n files: FILE_BY_PATH,\n}: {\n routes: Record<string, string | { default: Function }>;\n files: Record<string, string>;\n} = NODE_ENV_IS_DEVELOPMENT\n ? { routes: {}, files: {} }\n : (await import(`file://${join(CWD, \"dist\", \"[--metadata--].js\")}`)).default;\n\ndeclare module \"fastify\" {\n interface FastifyRequest {\n /** Path without query parameters */\n path: string;\n /** Path to resolved route handler */\n route: string;\n }\n\n interface FastifyReply {\n /** Populated when serving a static file; otherwise undefined. */\n file?: BaseSendResult;\n }\n}\n\nconst FASTIFY_SEND_OPTIONS = CONFIG.FASTIFY_SEND_OPTIONS?.() as SendOptions;\n\n// Enhance Fastify server from userland\nconst FASTIFY_SERVER = (CONFIG.FASTIFY_SERVER ?? ((fastify) => fastify)) as (\n fastify: FastifyInstance,\n) => FastifyInstance;\n\n// Create and export a Fastify instance\nexport default FASTIFY_SERVER(\n fastify(CONFIG.FASTIFY_SERVER_OPTIONS?.() as FastifyServerOptions)\n .decorateRequest(\"route\", \"\")\n .decorateRequest(\"path\", \"\")\n .decorateReply(\"file\", undefined)\n .addHook(\"onRequest\", async (request) => {\n // Extract path from url\n const index = request.url.indexOf(\"?\");\n request.path = index === -1 ? request.url : request.url.slice(0, index);\n })\n .all(\"*\", async (request: FastifyRequest, reply: FastifyReply) => {\n let payload: unknown;\n try {\n 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 } finally {\n // Close file stream if it isn't sent and nobody started reading it,\n // otherwise its file handle leaks.\n const stream = reply.file?.stream;\n if (stream && stream !== payload && stream.readableFlowing === null) {\n stream.destroy();\n }\n }\n }),\n);\n\n// Used as marker for static file routes.\nconst STATIC_FILE_ROUTE = Symbol(\"static-file-route\");\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\n if (route === STATIC_FILE_ROUTE) {\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 Response ||\n response instanceof Readable ||\n response instanceof ReadableStream ||\n Buffer.isBuffer(response) ||\n isJSX(response)\n ) {\n break;\n } else if (typeof response === \"object\" && route.endsWith(\"/[...guard]\")) {\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 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 * STATIC_FILE,\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): Generator<string | typeof STATIC_FILE_ROUTE> {\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 yield `${segments[i]}/[...guard]`;\n }\n\n // Append the marker for static files\n yield STATIC_FILE_ROUTE;\n\n // \"/a/b/c\" => [\"/a/b/[c]\", \"/a/b/c/[index]\"]\n const lastSlash = edgeSegment.lastIndexOf(\"/\") + 1;\n if (lastSlash > 0) {\n yield `${edgeSegment.substring(0, lastSlash)}[${edgeSegment.substring(lastSlash)}]`;\n }\n yield `${edgeSegment}/[index]`;\n\n // [...path]s are pushed from edge to root.\n for (let i = segments.length - 1; i >= 0; i--) {\n yield `${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 yield `${segments[i]}/[404]`;\n }\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
@@ -63,8 +63,9 @@ export default FASTIFY_SERVER(
63
63
  request.path = index === -1 ? request.url : request.url.slice(0, index);
64
64
  })
65
65
  .all("*", async (request: FastifyRequest, reply: FastifyReply) => {
66
+ let payload: unknown;
66
67
  try {
67
- const payload = await handler(request, reply);
68
+ payload = await handler(request, reply);
68
69
  if (
69
70
  reply.getHeader("content-type") === undefined &&
70
71
  (typeof payload === "string" || Buffer.isBuffer(payload))
@@ -75,10 +76,20 @@ export default FASTIFY_SERVER(
75
76
  } catch (error) {
76
77
  request.log.error(error);
77
78
  throw error;
79
+ } finally {
80
+ // Close file stream if it isn't sent and nobody started reading it,
81
+ // otherwise its file handle leaks.
82
+ const stream = reply.file?.stream;
83
+ if (stream && stream !== payload && stream.readableFlowing === null) {
84
+ stream.destroy();
85
+ }
78
86
  }
79
87
  }),
80
88
  );
81
89
 
90
+ // Used as marker for static file routes.
91
+ const STATIC_FILE_ROUTE = Symbol("static-file-route");
92
+
82
93
  /**
83
94
  * Resolves route module based on the request path and execute it.
84
95
  */
@@ -97,8 +108,8 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
97
108
 
98
109
  // Execute route handlers for current request
99
110
  for (const route of generateRoutes(request.path)) {
100
- // Try to serve static file when route matches path.
101
- if (route === request.path) {
111
+ // Try to serve static file
112
+ if (route === STATIC_FILE_ROUTE) {
102
113
  if (reply.file) {
103
114
  reply.status(reply.file.statusCode);
104
115
  reply.headers(reply.file.headers);
@@ -173,12 +184,14 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
173
184
  break;
174
185
  } else if (
175
186
  typeof response === "string" ||
187
+ response instanceof Response ||
176
188
  response instanceof Readable ||
189
+ response instanceof ReadableStream ||
177
190
  Buffer.isBuffer(response) ||
178
191
  isJSX(response)
179
192
  ) {
180
193
  break;
181
- } else if (route.endsWith("/[...guard]") && typeof response === "object") {
194
+ } else if (typeof response === "object" && route.endsWith("/[...guard]")) {
182
195
  // Add object entries from guard to props
183
196
  Object.assign(props, response);
184
197
  continue;
@@ -188,7 +201,6 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
188
201
  break;
189
202
  }
190
203
  }
191
-
192
204
  return await renderResponse(context, response);
193
205
  } catch (error) {
194
206
  const errorHandler = context["errorHandler"];
@@ -209,13 +221,13 @@ async function handler(request: FastifyRequest, reply: FastifyReply) {
209
221
  *
210
222
  * [
211
223
  * "/[...guard]","/a/[...guard]","/a/b/[...guard]","/a/b/c/[...guard]",
212
- * "/a/b/c",
224
+ * STATIC_FILE,
213
225
  * "/a/b/[c]","/a/b/c/[index]",
214
226
  * "/a/b/c/[...path]","/a/b/[...path]","/a/[...path]","/[...path]",
215
227
  * "/a/b/c/[404]","/a/b/[404]","/a/[404]","/[404]"
216
228
  * ]
217
229
  */
218
- function* generateRoutes(path: string): Generator<string> {
230
+ function* generateRoutes(path: string): Generator<string | typeof STATIC_FILE_ROUTE> {
219
231
  // Transform given path into array of all its segments.
220
232
  // "/a/b/c" => ["", "/a", "/a/b/", "/a/b/c"]
221
233
  const segments = [""];
@@ -233,8 +245,8 @@ function* generateRoutes(path: string): Generator<string> {
233
245
  yield `${segments[i]}/[...guard]`;
234
246
  }
235
247
 
236
- // Append the verbatim path for static file serving.
237
- yield path;
248
+ // Append the marker for static files
249
+ yield STATIC_FILE_ROUTE;
238
250
 
239
251
  // "/a/b/c" => ["/a/b/[c]", "/a/b/c/[index]"]
240
252
  const lastSlash = edgeSegment.lastIndexOf("/") + 1;