|
| 1 | +import type { Plugin, ResolvedConfig } from 'vite' |
| 2 | +import { mkdir, writeFile } from 'node:fs/promises' |
| 3 | +import { join, relative, sep } from 'node:path' |
| 4 | +import { sep as posixSep } from 'node:path/posix' |
| 5 | +import { version, name } from '../package.json' |
| 6 | + |
| 7 | +const NETLIFY_FUNCTIONS_DIR = '.netlify/functions-internal' |
| 8 | + |
| 9 | +const FUNCTION_FILENAME = 'react-router-server.mjs' |
| 10 | +/** |
| 11 | + * The chunk filename without an extension, i.e. in the Rollup config `input` format |
| 12 | + */ |
| 13 | +const FUNCTION_HANDLER_CHUNK = 'server' |
| 14 | + |
| 15 | +const FUNCTION_HANDLER_MODULE_ID = 'virtual:netlify-server' |
| 16 | +const RESOLVED_FUNCTION_HANDLER_MODULE_ID = `\0${FUNCTION_HANDLER_MODULE_ID}` |
| 17 | + |
| 18 | +const toPosixPath = (path: string) => path.split(sep).join(posixSep) |
| 19 | + |
| 20 | +// The virtual module that is the compiled Vite SSR entrypoint (a Netlify Function handler) |
| 21 | +const FUNCTION_HANDLER = /* js */ ` |
| 22 | +import { createRequestHandler } from "@netlify/vite-plugin-react-router"; |
| 23 | +import * as build from "virtual:react-router/server-build"; |
| 24 | +export default createRequestHandler({ |
| 25 | + build, |
| 26 | + getLoadContext: async (_req, ctx) => ctx, |
| 27 | +}); |
| 28 | +` |
| 29 | + |
| 30 | +// This is written to the functions directory. It just re-exports |
| 31 | +// the compiled entrypoint, along with Netlify function config. |
| 32 | +function generateNetlifyFunction(handlerPath: string) { |
| 33 | + return /* js */ ` |
| 34 | + export { default } from "${handlerPath}"; |
| 35 | +
|
| 36 | + export const config = { |
| 37 | + name: "React Router server handler", |
| 38 | + generator: "${name}@${version}", |
| 39 | + path: "/*", |
| 40 | + preferStatic: true, |
| 41 | + }; |
| 42 | + ` |
| 43 | +} |
| 44 | + |
| 45 | +export function netlifyPlugin(): Plugin { |
| 46 | + let resolvedConfig: ResolvedConfig |
| 47 | + let currentCommand: string |
| 48 | + let isSsr: boolean | undefined |
| 49 | + return { |
| 50 | + name: 'vite-plugin-react-router-netlify-functions', |
| 51 | + config(config, { command, isSsrBuild }) { |
| 52 | + currentCommand = command |
| 53 | + isSsr = isSsrBuild |
| 54 | + if (command === 'build') { |
| 55 | + if (isSsrBuild) { |
| 56 | + // We need to add an extra SSR entrypoint, as we need to compile |
| 57 | + // the server entrypoint too. This is because it uses virtual |
| 58 | + // modules. |
| 59 | + // NOTE: the below is making various assumptions about the React Router Vite plugin's |
| 60 | + // implementation details: |
| 61 | + // https://github.com/remix-run/remix/blob/cc65962b1a96d1e134336aa9620ef1dad7c5efb1/packages/remix-dev/vite/plugin.ts#L1149-L1168 |
| 62 | + // TODO(serhalp) Stop making these assumptions or assert them explictly. |
| 63 | + // TODO(serhalp) Unless I'm misunderstanding something, we should only need to *replace* |
| 64 | + // the default React Router Vite SSR entrypoint, not add an additional one. |
| 65 | + if (typeof config.build?.rollupOptions?.input === 'string') { |
| 66 | + config.build.rollupOptions.input = { |
| 67 | + [FUNCTION_HANDLER_CHUNK]: FUNCTION_HANDLER_MODULE_ID, |
| 68 | + index: config.build.rollupOptions.input, |
| 69 | + } |
| 70 | + if (config.build.rollupOptions.output && !Array.isArray(config.build.rollupOptions.output)) { |
| 71 | + config.build.rollupOptions.output.entryFileNames = '[name].js' |
| 72 | + } |
| 73 | + } |
| 74 | + } |
| 75 | + } |
| 76 | + }, |
| 77 | + async resolveId(source) { |
| 78 | + if (source === FUNCTION_HANDLER_MODULE_ID) { |
| 79 | + return RESOLVED_FUNCTION_HANDLER_MODULE_ID |
| 80 | + } |
| 81 | + }, |
| 82 | + // See https://vitejs.dev/guide/api-plugin#virtual-modules-convention. |
| 83 | + load(id) { |
| 84 | + if (id === RESOLVED_FUNCTION_HANDLER_MODULE_ID) { |
| 85 | + return FUNCTION_HANDLER |
| 86 | + } |
| 87 | + }, |
| 88 | + async configResolved(config) { |
| 89 | + resolvedConfig = config |
| 90 | + }, |
| 91 | + // See https://rollupjs.org/plugin-development/#writebundle. |
| 92 | + async writeBundle() { |
| 93 | + // Write the server entrypoint to the Netlify functions directory |
| 94 | + if (currentCommand === 'build' && isSsr) { |
| 95 | + const functionsDirectory = join(resolvedConfig.root, NETLIFY_FUNCTIONS_DIR) |
| 96 | + |
| 97 | + await mkdir(functionsDirectory, { recursive: true }) |
| 98 | + |
| 99 | + const handlerPath = join(resolvedConfig.build.outDir, `${FUNCTION_HANDLER_CHUNK}.js`) |
| 100 | + const relativeHandlerPath = toPosixPath(relative(functionsDirectory, handlerPath)) |
| 101 | + |
| 102 | + await writeFile(join(functionsDirectory, FUNCTION_FILENAME), generateNetlifyFunction(relativeHandlerPath)) |
| 103 | + } |
| 104 | + }, |
| 105 | + } |
| 106 | +} |
0 commit comments