UNPKG

@mcansh/remix-fastify

Version:

Fastify server request handler for Remix and React Router

1 lines 14.4 kB
{"version":3,"file":"plugins-B2SMtsK6.cjs","names":["requestHeaders: FastifyRequest[\"headers\"]","request: FastifyRequest<RouteGenericInterface, Server>","url","reply: FastifyReply<RouteGenericInterface, Server>","createReadableStreamFromReadable:\n | typeof RemixCreateReadableStreamFromReadable\n | typeof RRCreateReadableStreamFromReadable","controller: AbortController | null","init: RequestInit","nodeResponse: Response","response: Response","Readable","fastify: FastifyInstance","virtualModule:\n | \"virtual:remix/server-build\"\n | \"virtual:react-router/server-build\"","createRequestHandler:\n | RemixCreateRequestHandlerFunction\n | RRCreateRequestHandlerFunction","vite: ViteDevServer | undefined","fastifyStatic"],"sources":["../src/shared.ts","../src/plugins/index.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type * as http2 from \"node:http2\";\nimport type * as https from \"node:https\";\nimport { Readable } from \"node:stream\";\n\nimport type { createReadableStreamFromReadable as RRCreateReadableStreamFromReadable } from \"@react-router/node\";\nimport type { createReadableStreamFromReadable as RemixCreateReadableStreamFromReadable } from \"@remix-run/node\";\nimport type {\n FastifyReply,\n FastifyRequest,\n RawReplyDefaultExpression,\n RawRequestDefaultExpression,\n RouteGenericInterface,\n} from \"fastify\";\n\nexport type HttpServer =\n | http.Server\n | https.Server\n | http2.Http2Server\n | http2.Http2SecureServer;\n\nexport type HttpRequest = RawRequestDefaultExpression<HttpServer>;\nexport type HttpResponse = RawReplyDefaultExpression<HttpServer>;\n\nexport type RequestHandler<Server extends HttpServer> = (\n request: FastifyRequest<RouteGenericInterface, Server>,\n reply: FastifyReply<RouteGenericInterface, Server>,\n) => Promise<void>;\n\n/**\n * A function that returns the value to use as `context` in route `loader` and\n * `action` functions.\n *\n * You can think of this as an escape hatch that allows you to pass\n * environment/platform-specific values through to your loader/action.\n */\nexport type GetLoadContextFunction<\n Server extends HttpServer,\n AppLoadContext,\n> = (\n request: FastifyRequest<RouteGenericInterface, Server>,\n reply: FastifyReply<RouteGenericInterface, Server>,\n) => Promise<AppLoadContext> | AppLoadContext;\n\nexport function createHeaders(\n requestHeaders: FastifyRequest[\"headers\"],\n): Headers {\n let headers = new Headers();\n\n for (let [key, values] of Object.entries(requestHeaders)) {\n if (values) {\n if (Array.isArray(values)) {\n for (let value of values) {\n headers.append(key, value);\n }\n } else {\n headers.set(key, values);\n }\n }\n }\n\n return headers;\n}\n\nexport function getUrl<Server extends HttpServer>(\n request: FastifyRequest<RouteGenericInterface, Server>,\n): string {\n let origin = `${request.protocol}://${request.host}`;\n // Use `request.originalUrl` so Remix and React Router are aware of the full path\n let url = `${origin}${request.originalUrl}`;\n return url;\n}\n\nexport function createRequest<Server extends HttpServer>(\n request: FastifyRequest<RouteGenericInterface, Server>,\n reply: FastifyReply<RouteGenericInterface, Server>,\n createReadableStreamFromReadable:\n | typeof RemixCreateReadableStreamFromReadable\n | typeof RRCreateReadableStreamFromReadable,\n): Request {\n let url = getUrl(request);\n\n let controller: AbortController | null = new AbortController();\n\n let init: RequestInit = {\n method: request.method,\n headers: createHeaders(request.headers),\n signal: controller.signal,\n };\n\n // Abort action/loaders once we can no longer write a response if we have\n // not yet sent a response (i.e., `close` without `finish`)\n // `finish` -> done rendering the response\n // `close` -> response can no longer be written to\n reply.raw.on(\"finish\", () => (controller = null));\n reply.raw.on(\"close\", () => controller?.abort());\n\n if (request.method !== \"GET\" && request.method !== \"HEAD\") {\n init.body = createReadableStreamFromReadable(request.raw);\n init.duplex = \"half\";\n }\n\n return new Request(url, init);\n}\n\nexport async function sendResponse<Server extends HttpServer>(\n reply: FastifyReply<RouteGenericInterface, Server>,\n nodeResponse: Response,\n): Promise<void> {\n reply.status(nodeResponse.status);\n\n for (let [key, values] of nodeResponse.headers.entries()) {\n reply.headers({ [key]: values });\n }\n\n if (nodeResponse.body) {\n let stream = responseToReadable(nodeResponse);\n return reply.send(stream);\n }\n\n return reply.send(await nodeResponse.text());\n}\n\nfunction responseToReadable(response: Response): Readable | null {\n if (!response.body) return null;\n\n let reader = response.body.getReader();\n let readable = new Readable();\n readable._read = async () => {\n try {\n let result = await reader.read();\n if (!result.done) {\n readable.push(Buffer.from(result.value));\n } else {\n readable.push(null);\n }\n } catch (error) {\n readable.destroy(error as Error);\n throw error;\n }\n };\n\n return readable;\n}\n","import path from \"node:path\";\nimport url from \"node:url\";\n\nimport type { FastifyStaticOptions } from \"@fastify/static\";\nimport fastifyStatic from \"@fastify/static\";\nimport type { FastifyInstance, RouteShorthandOptions } from \"fastify\";\nimport { cacheHeader } from \"pretty-cache-header\";\nimport type { InlineConfig, ViteDevServer } from \"vite\";\n\nimport type { CreateRequestHandlerFunction as RRCreateRequestHandlerFunction } from \"../servers/react-router\";\nimport type { CreateRequestHandlerFunction as RemixCreateRequestHandlerFunction } from \"../servers/remix\";\nimport type { GetLoadContextFunction, HttpServer } from \"../shared\";\n\nexport type PluginOptions<\n Server extends HttpServer = HttpServer,\n AppLoadContext = unknown,\n ServerBuild = unknown,\n> = {\n /**\n * The base path for the Remix app.\n * match the `basename` in your Vite config.\n * @default \"/\"\n */\n basename?: string;\n /**\n * The directory where the Remix app is built.\n * This should match the `buildDirectory` directory in your Remix config.\n * @default \"build\"\n */\n buildDirectory?: string;\n /**\n * The Remix server output filename\n * This should match the `serverBuildFile` filename in your Remix config.\n * @default \"index.js\"\n */\n serverBuildFile?: string;\n /**\n * A function that returns the value to use as `context` in route `loader` and\n * `action` functions.\n *\n * You can think of this as an escape hatch that allows you to pass\n * environment/platform-specific values through to your loader/action.\n */\n getLoadContext?: GetLoadContextFunction<Server, AppLoadContext>;\n mode?: string;\n /**\n * Options to pass to the Vite server in development.\n */\n viteOptions?: InlineConfig;\n /**\n * Options to pass to the `@fastify/static` plugin for serving compiled assets in production.\n */\n fastifyStaticOptions?: FastifyStaticOptions;\n /**\n * The cache control options to use for build assets in production.\n * uses `pretty-cache-header` under the hood.\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control\n * @default { public: true, maxAge: '1 year', immutable: true }\n */\n assetCacheControl?: Parameters<typeof cacheHeader>[0];\n /**\n * The cache control options to use for other assets in production.\n * uses `pretty-cache-header` under the hood.\n * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control\n * @default { public: true, maxAge: '1 hour' }\n */\n defaultCacheControl?: Parameters<typeof cacheHeader>[0];\n /**\n * The Remix server build to use in production. Use this only if the default approach doesn't work for you.\n *\n * If not provided, it will be loaded using `import()` with the server build path provided in the options.\n */\n productionServerBuild?:\n | ServerBuild\n | (() => ServerBuild | Promise<ServerBuild>);\n\n childServerOptions?: RouteShorthandOptions<Server>;\n};\n\nexport function createPlugin(\n fastify: FastifyInstance,\n {\n basename = \"/\",\n buildDirectory = \"build\",\n serverBuildFile = \"index.js\",\n getLoadContext,\n mode = process.env.NODE_ENV,\n viteOptions,\n fastifyStaticOptions,\n assetCacheControl = { public: true, maxAge: \"1 year\", immutable: true },\n defaultCacheControl = { public: true, maxAge: \"1 hour\" },\n productionServerBuild,\n childServerOptions,\n }: PluginOptions,\n virtualModule:\n | \"virtual:remix/server-build\"\n | \"virtual:react-router/server-build\",\n // TODO: look if importing the function as a type requires the peer dependency\n createRequestHandler:\n | RemixCreateRequestHandlerFunction\n | RRCreateRequestHandlerFunction,\n) {\n return async () => {\n let cwd = process.env.REMIX_ROOT ?? process.cwd();\n\n let vite: ViteDevServer | undefined;\n\n if (mode !== \"production\") {\n vite = await import(\"vite\").then((mod) => {\n return mod.createServer({\n ...viteOptions,\n server: {\n ...viteOptions?.server,\n middlewareMode: true,\n },\n });\n });\n }\n\n let resolvedBuildDirectory = path.resolve(cwd, buildDirectory);\n let SERVER_BUILD = path.join(\n resolvedBuildDirectory,\n \"server\",\n serverBuildFile,\n );\n let SERVER_BUILD_URL = url.pathToFileURL(SERVER_BUILD).href;\n\n let handler = createRequestHandler<HttpServer>({\n mode,\n // @ts-expect-error - fix this\n getLoadContext,\n build: vite\n ? () => vite.ssrLoadModule(virtualModule)\n : (productionServerBuild ?? (await import(SERVER_BUILD_URL))),\n });\n\n // handle asset requests\n if (vite) {\n let middie = await import(\"@fastify/middie\").then((mod) => mod.default);\n await fastify.register(middie);\n fastify.use(vite.middlewares);\n } else {\n let BUILD_DIR = path.join(resolvedBuildDirectory, \"client\");\n let ASSET_DIR = path.join(BUILD_DIR, \"assets\");\n await fastify.register(fastifyStatic, {\n root: BUILD_DIR,\n prefix: basename,\n wildcard: false,\n cacheControl: false, // required because we are setting custom cache-control headers in setHeaders\n dotfiles: \"allow\",\n etag: true,\n serveDotFiles: true,\n lastModified: true,\n setHeaders(res, filepath) {\n let isAsset = filepath.startsWith(ASSET_DIR);\n res.setHeader(\n \"cache-control\",\n isAsset\n ? cacheHeader(assetCacheControl)\n : cacheHeader(defaultCacheControl),\n );\n },\n ...fastifyStaticOptions,\n });\n }\n\n fastify.register(\n async function createRemixRequestHandler(childServer) {\n // remove the default content type parsers\n childServer.removeAllContentTypeParsers();\n // allow all content types\n childServer.addContentTypeParser(\"*\", (_request, payload, done) => {\n done(null, payload);\n });\n\n if (childServerOptions) {\n childServer.all(\"*\", childServerOptions, handler);\n } else {\n childServer.all(\"*\", handler);\n }\n },\n { prefix: basename },\n );\n };\n}\n"],"mappings":"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"}