[remix] Split Edge and Node server builds using serverBundles config (#9504)

Utilize the [`serverBundles`](https://github.com/remix-run/remix/pull/5479) config option to generate two server bundle builds. One contains only the routes that should run in Node.js, and the other contains only the routes that should run in the Edge runtime. In the future we could update this configuration to generate more than two bundles to be more granular and allow for infinite scalability.

Because the `serverBundles` PR is not yet merged, this PR introduces usage of a forked version of `@remix-run/dev` which incorporates our changes. Hopefully usage of a fork is temporary, but it gets us unblocked for now.
This commit is contained in:
Nathan Rajlich
2023-02-28 19:10:42 -08:00
committed by GitHub
parent 1ca3704297
commit 61de63d285
12 changed files with 1885 additions and 2078 deletions

View File

@@ -41,6 +41,10 @@ import {
const _require: typeof require = eval('require');
const REMIX_RUN_DEV_PATH = dirname(
_require.resolve('@remix-run/dev/package.json')
);
export const build: BuildV2 = async ({
entrypoint,
files,
@@ -101,19 +105,84 @@ export const build: BuildV2 = async ({
await fs.readFile(remixDevPackageJsonPath, 'utf8')
).version;
// Make our version of `remix` CLI available to the project's build
// command by creating a symlink to the copy in our node modules,
// so that `serverBundles` works: https://github.com/remix-run/remix/pull/5479
const nodeModulesDir = join(entrypointFsDirname, 'node_modules');
const remixRunDevPath = join(nodeModulesDir, '@remix-run/dev');
let backupRemixRunDevPath:
| string
| false = `${remixRunDevPath}.__vercel_backup`;
try {
await fs.rename(remixRunDevPath, backupRemixRunDevPath);
} catch (err: any) {
if (err.code !== 'ENOENT') {
throw err;
}
backupRemixRunDevPath = false;
}
await fs.symlink(REMIX_RUN_DEV_PATH, remixRunDevPath);
// Make `remix build` output production mode
spawnOpts.env.NODE_ENV = 'production';
const remixConfig = await chdirAndReadConfig(entrypointFsDirname);
const remixRoutes = Object.values(remixConfig.routes);
// Read the `export const config` (if any) for each route
const project = new Project();
const staticConfigsMap = new Map<ConfigRoute, BaseFunctionConfig>();
for (const route of remixRoutes) {
const routePath = join(remixConfig.appDirectory, route.file);
const staticConfig = getConfig(project, routePath);
if (staticConfig) {
staticConfigsMap.set(route, staticConfig);
}
}
// Figure out which pages should be edge functions
const edgeRoutes = new Set<ConfigRoute>();
const nodeRoutes = new Set<ConfigRoute>();
for (const route of remixRoutes) {
if (isLayoutRoute(route.id, remixRoutes)) continue;
// Support runtime inheritance when defined in a parent route,
// by iterating through the route's parent hierarchy until a
// config containing "runtime" is found.
let isEdge = false;
for (const currentRoute of getRouteIterator(route, remixConfig.routes)) {
const staticConfig = staticConfigsMap.get(currentRoute);
if (staticConfig?.runtime) {
isEdge = isEdgeRuntime(staticConfig.runtime);
break;
}
}
if (isEdge) {
edgeRoutes.add(route);
} else {
nodeRoutes.add(route);
}
}
const serverBundles = [
{
serverBuildPath: 'build/build-edge.js',
routes: Array.from(edgeRoutes).map(r => r.id),
},
{
serverBuildPath: 'build/build-node.js',
routes: Array.from(nodeRoutes).map(r => r.id),
},
];
// We need to patch the `remix.config.js` file to force some values necessary
// for a build that works on either Node.js or the Edge runtime
const remixConfigPath = findConfig(entrypointFsDirname, 'remix.config');
const renamedRemixConfigPath = remixConfigPath
? `${remixConfigPath}.original${extname(remixConfigPath)}`
: undefined;
const serverBuildPath = 'build/index.js';
if (remixConfigPath && renamedRemixConfigPath) {
await fs.rename(remixConfigPath, renamedRemixConfigPath);
@@ -133,7 +202,8 @@ export const build: BuildV2 = async ({
config.serverBuildTarget = undefined;
config.serverModuleFormat = 'cjs';
config.serverPlatform = 'node';
config.serverBuildPath = ${JSON.stringify(serverBuildPath)}
config.serverBuildPath = undefined;
config.serverBundles = ${JSON.stringify(serverBundles)};
export default config;`;
} else {
patchedConfig = `const config = require('./${basename(
@@ -142,7 +212,8 @@ export default config;`;
config.serverBuildTarget = undefined;
config.serverModuleFormat = 'cjs';
config.serverPlatform = 'node';
config.serverBuildPath = ${JSON.stringify(serverBuildPath)}
config.serverBuildPath = undefined;
config.serverBundles = ${JSON.stringify(serverBundles)};
module.exports = config;`;
}
await fs.writeFile(remixConfigPath, patchedConfig);
@@ -182,20 +253,12 @@ module.exports = config;`;
if (remixConfigPath && renamedRemixConfigPath) {
await fs.rename(renamedRemixConfigPath, remixConfigPath);
}
}
// Figure out which pages should be edge functions
let hasEdgeRoute = false;
const staticConfigsMap = new Map<ConfigRoute, BaseFunctionConfig>();
const project = new Project();
for (const route of remixRoutes) {
const routePath = join(remixConfig.appDirectory, route.file);
const staticConfig = getConfig(project, routePath);
if (staticConfig) {
staticConfigsMap.set(route, staticConfig);
}
if (staticConfig?.runtime && isEdgeRuntime(staticConfig.runtime)) {
hasEdgeRoute = true;
// Remove `@remix-run/dev` symlink
await fs.unlink(remixRunDevPath);
if (backupRemixRunDevPath) {
// Restore previous version if it was existed
await fs.rename(backupRemixRunDevPath, remixRunDevPath);
}
}
@@ -212,18 +275,18 @@ module.exports = config;`;
const [staticFiles, nodeFunction, edgeFunction] = await Promise.all([
glob('**', join(entrypointFsDirname, 'public')),
createRenderNodeFunction(
nodeVersion,
entrypointFsDirname,
repoRootPath,
join(entrypointFsDirname, serverBuildPath),
join(entrypointFsDirname, 'build/build-node.js'),
remixConfig.serverEntryPoint,
nodeVersion,
remixVersion
),
hasEdgeRoute
edgeRoutes.size > 0
? createRenderEdgeFunction(
entrypointFsDirname,
repoRootPath,
join(entrypointFsDirname, serverBuildPath),
join(entrypointFsDirname, 'build/build-edge.js'),
remixConfig.serverEntryPoint,
remixVersion
)
@@ -254,17 +317,8 @@ module.exports = config;`;
continue;
}
let isEdge = false;
for (const currentRoute of getRouteIterator(route, remixConfig.routes)) {
const staticConfig = staticConfigsMap.get(currentRoute);
if (staticConfig?.runtime) {
isEdge = isEdgeRuntime(staticConfig.runtime);
break;
}
}
const fn =
isEdge && edgeFunction
edgeRoutes.has(route) && edgeFunction
? // `EdgeFunction` currently requires the "name" property to be set.
// Ideally this property will be removed, at which point we can
// return the same `edgeFunction` instance instead of creating a
@@ -307,11 +361,11 @@ function hasScript(scriptName: string, pkg: PackageJson | null) {
}
async function createRenderNodeFunction(
nodeVersion: NodeVersion,
entrypointDir: string,
rootDir: string,
serverBuildPath: string,
serverEntryPoint: string | undefined,
nodeVersion: NodeVersion,
remixVersion: string
): Promise<NodejsLambda> {
const files: Files = {};

View File

@@ -41,7 +41,10 @@ export function getPathFromRoute(
route: ConfigRoute,
routes: RouteManifest
): string {
if (route.id === 'root' || (route.parentId === 'root' && route.index)) {
if (
route.id === 'root' ||
(route.parentId === 'root' && !route.path && route.index)
) {
return 'index';
}