mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 04:22:13 +00:00
[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:
@@ -22,7 +22,7 @@
|
||||
"vercel-edge-entrypoint.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@remix-run/dev": "1.13.0",
|
||||
"@remix-run/dev": "npm:@vercel/remix-run-dev@1.13.0-patch.1",
|
||||
"@vercel/nft": "0.22.5",
|
||||
"@vercel/static-config": "2.0.13",
|
||||
"path-to-regexp": "6.2.1",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { createRequestHandler } from '@remix-run/server-runtime';
|
||||
import build from './index.js';
|
||||
import build from './build-edge.js';
|
||||
export default createRequestHandler(build);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
installGlobals();
|
||||
|
||||
import build from './index.js';
|
||||
import build from './build-node.js';
|
||||
|
||||
const handleRequest = createRemixRequestHandler(build, process.env.NODE_ENV);
|
||||
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
17
packages/remix/test/fixtures/01-remix-basics/app/b.server.ts
vendored
Normal file
17
packages/remix/test/fixtures/01-remix-basics/app/b.server.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// Edge functions can not use child processes, but this is route
|
||||
// uses Node.js. So this is here to verify that bundle splitting
|
||||
// is working correctly (because this route should not exist in
|
||||
// the Edge bundle).
|
||||
import { exec } from 'child_process';
|
||||
|
||||
import { json } from '@remix-run/node';
|
||||
|
||||
export async function loader() {
|
||||
const hi = await new Promise<string>((resolve, reject) => {
|
||||
exec('echo hi from the B page', (err, stdout) => {
|
||||
if (err) return reject(err);
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
return json({ hi });
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
import { loader } from '~/b.server';
|
||||
import { useLoaderData } from '@remix-run/react';
|
||||
|
||||
export { loader };
|
||||
|
||||
export default function B() {
|
||||
const { hi } = useLoaderData<typeof loader>();
|
||||
return (
|
||||
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
|
||||
<h1>B page</h1>
|
||||
<p>{hi}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"probes": [
|
||||
{ "path": "/", "mustContain": "Welcome to Remix" },
|
||||
{ "path": "/edge", "mustContain": "Welcome to Remix@Edge" },
|
||||
{ "path": "/b", "mustContain": "B page" },
|
||||
{ "path": "/b", "mustContain": "hi from the B page" },
|
||||
{ "path": "/nested", "mustContain": "Nested index page" },
|
||||
{ "path": "/nested/another", "mustContain": "Nested another page" },
|
||||
{ "path": "/nested/index", "status": 404, "mustContain": "Not Found" },
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
"start": "remix-serve build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remix-run/react": "^1.7.4",
|
||||
"@remix-run/serve": "^1.7.4",
|
||||
"@remix-run/react": "1.5.0",
|
||||
"@remix-run/serve": "1.5.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.7.4",
|
||||
"@remix-run/dev": "1.5.0",
|
||||
"@types/react": "^17.0.45",
|
||||
"@types/react-dom": "^17.0.17",
|
||||
"typescript": "^4.6.4"
|
||||
|
||||
3281
packages/remix/test/fixtures/03-with-pnpm/pnpm-lock.yaml
generated
vendored
3281
packages/remix/test/fixtures/03-with-pnpm/pnpm-lock.yaml
generated
vendored
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,14 @@ describe('getPathFromRoute()', () => {
|
||||
parentId: 'root',
|
||||
file: 'routes/$.tsx',
|
||||
},
|
||||
'routes/nested/index': {
|
||||
path: 'nested',
|
||||
index: true,
|
||||
caseSensitive: undefined,
|
||||
id: 'routes/nested/index',
|
||||
parentId: 'root',
|
||||
file: 'routes/nested/index.tsx',
|
||||
},
|
||||
};
|
||||
|
||||
it.each([
|
||||
@@ -79,6 +87,7 @@ describe('getPathFromRoute()', () => {
|
||||
{ id: 'routes/__pathless', expected: '' },
|
||||
{ id: 'routes/index', expected: 'index' },
|
||||
{ id: 'routes/api.hello', expected: 'api/hello' },
|
||||
{ id: 'routes/nested/index', expected: 'nested' },
|
||||
{ id: 'routes/projects', expected: 'projects' },
|
||||
{ id: 'routes/projects/__pathless', expected: 'projects' },
|
||||
{ id: 'routes/projects/index', expected: 'projects' },
|
||||
|
||||
514
pnpm-lock.yaml
generated
514
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user