mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 21:07:46 +00:00
[remix] Support "regions", "memory" and "maxDuration" in static config (#9442)
Apply the `regions` configuration (for both Edge and Node) and `memory`/`maxDuration` (only for Node) in a page's static config export, i.e.
```js
export const config = { runtime: 'edge', regions: ['iad1'] }
// or for Node
export const config = { runtime: 'nodejs', regions: ['iad1'], maxDuration: 5, memory: 3008 }
```
Similar to `runtime`, these config values can be inherited from a parent layout route to apply to all sub-routes. Routes with common config settings get placed into a common server bundle, meaning that there may now be more than 2 functions created (previously was one Edge, one Node), allowing for more granularity between the server build bundles.
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
scanParentDirs,
|
||||
walkParentDirs,
|
||||
} from '@vercel/build-utils';
|
||||
import { getConfig, BaseFunctionConfig } from '@vercel/static-config';
|
||||
import { getConfig } from '@vercel/static-config';
|
||||
import { nodeFileTrace } from '@vercel/nft';
|
||||
import { readConfig } from '@remix-run/dev/dist/config';
|
||||
import type {
|
||||
@@ -29,14 +29,19 @@ import type {
|
||||
PackageJson,
|
||||
BuildResultV2Typical,
|
||||
} from '@vercel/build-utils';
|
||||
import type { BaseFunctionConfig } from '@vercel/static-config';
|
||||
import type { RemixConfig } from '@remix-run/dev/dist/config';
|
||||
import type { ConfigRoute } from '@remix-run/dev/dist/config/routes';
|
||||
import {
|
||||
calculateRouteConfigHash,
|
||||
findConfig,
|
||||
getPathFromRoute,
|
||||
getRegExpFromPath,
|
||||
getRouteIterator,
|
||||
getResolvedRouteConfig,
|
||||
isLayoutRoute,
|
||||
ResolvedRouteConfig,
|
||||
ResolvedNodeRouteConfig,
|
||||
ResolvedEdgeRouteConfig,
|
||||
} from './utils';
|
||||
|
||||
const _require: typeof require = eval('require');
|
||||
@@ -45,6 +50,15 @@ const REMIX_RUN_DEV_PATH = dirname(
|
||||
_require.resolve('@remix-run/dev/package.json')
|
||||
);
|
||||
|
||||
const edgeServerSrcPromise = fs.readFile(
|
||||
join(__dirname, '../server-edge.mjs'),
|
||||
'utf-8'
|
||||
);
|
||||
const nodeServerSrcPromise = fs.readFile(
|
||||
join(__dirname, '../server-node.mjs'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
export const build: BuildV2 = async ({
|
||||
entrypoint,
|
||||
files,
|
||||
@@ -133,49 +147,53 @@ export const build: BuildV2 = async ({
|
||||
|
||||
// Read the `export const config` (if any) for each route
|
||||
const project = new Project();
|
||||
const staticConfigsMap = new Map<ConfigRoute, BaseFunctionConfig>();
|
||||
const staticConfigsMap = new Map<ConfigRoute, BaseFunctionConfig | null>();
|
||||
for (const route of remixRoutes) {
|
||||
const routePath = join(remixConfig.appDirectory, route.file);
|
||||
const staticConfig = getConfig(project, routePath);
|
||||
if (staticConfig) {
|
||||
staticConfigsMap.set(route, staticConfig);
|
||||
}
|
||||
staticConfigsMap.set(route, staticConfig);
|
||||
}
|
||||
|
||||
// Figure out which pages should be edge functions
|
||||
const edgeRoutes = new Set<ConfigRoute>();
|
||||
const nodeRoutes = new Set<ConfigRoute>();
|
||||
const resolvedConfigsMap = new Map<ConfigRoute, ResolvedRouteConfig>();
|
||||
for (const route of remixRoutes) {
|
||||
const config = getResolvedRouteConfig(
|
||||
route,
|
||||
remixConfig.routes,
|
||||
staticConfigsMap
|
||||
);
|
||||
resolvedConfigsMap.set(route, config);
|
||||
}
|
||||
|
||||
// Figure out which routes belong to which server bundles
|
||||
// based on having common static config properties
|
||||
const serverBundlesMap = new Map<string, 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;
|
||||
}
|
||||
const config = resolvedConfigsMap.get(route);
|
||||
if (!config) {
|
||||
throw new Error(`Expected resolved config for "${route.id}"`);
|
||||
}
|
||||
const hash = calculateRouteConfigHash(config);
|
||||
|
||||
let routesForHash = serverBundlesMap.get(hash);
|
||||
if (!Array.isArray(routesForHash)) {
|
||||
routesForHash = [];
|
||||
serverBundlesMap.set(hash, routesForHash);
|
||||
}
|
||||
|
||||
if (isEdge) {
|
||||
edgeRoutes.add(route);
|
||||
} else {
|
||||
nodeRoutes.add(route);
|
||||
}
|
||||
routesForHash.push(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),
|
||||
},
|
||||
];
|
||||
|
||||
const serverBundles = Array.from(serverBundlesMap.entries()).map(
|
||||
([hash, routes]) => {
|
||||
const runtime = resolvedConfigsMap.get(routes[0])?.runtime ?? 'nodejs';
|
||||
return {
|
||||
serverBuildPath: `build/build-${runtime}-${hash}.js`,
|
||||
routes: routes.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
|
||||
@@ -272,25 +290,35 @@ module.exports = config;`;
|
||||
ensureResolvable(entrypointFsDirname, repoRootPath, '@remix-run/node'),
|
||||
]);
|
||||
|
||||
const [staticFiles, nodeFunction, edgeFunction] = await Promise.all([
|
||||
const [staticFiles, ...functions] = await Promise.all([
|
||||
glob('**', join(entrypointFsDirname, 'public')),
|
||||
createRenderNodeFunction(
|
||||
nodeVersion,
|
||||
entrypointFsDirname,
|
||||
repoRootPath,
|
||||
join(entrypointFsDirname, 'build/build-node.js'),
|
||||
remixConfig.serverEntryPoint,
|
||||
remixVersion
|
||||
),
|
||||
edgeRoutes.size > 0
|
||||
? createRenderEdgeFunction(
|
||||
...serverBundles.map(bundle => {
|
||||
const firstRoute = remixConfig.routes[bundle.routes[0]];
|
||||
const config = resolvedConfigsMap.get(firstRoute) ?? {
|
||||
runtime: 'nodejs',
|
||||
};
|
||||
|
||||
if (config.runtime === 'edge') {
|
||||
return createRenderEdgeFunction(
|
||||
entrypointFsDirname,
|
||||
repoRootPath,
|
||||
join(entrypointFsDirname, 'build/build-edge.js'),
|
||||
join(entrypointFsDirname, bundle.serverBuildPath),
|
||||
remixConfig.serverEntryPoint,
|
||||
remixVersion
|
||||
)
|
||||
: undefined,
|
||||
remixVersion,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
return createRenderNodeFunction(
|
||||
nodeVersion,
|
||||
entrypointFsDirname,
|
||||
repoRootPath,
|
||||
join(entrypointFsDirname, bundle.serverBuildPath),
|
||||
remixConfig.serverEntryPoint,
|
||||
remixVersion,
|
||||
config
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
const output: BuildResultV2Typical['output'] = staticFiles;
|
||||
@@ -317,18 +345,26 @@ module.exports = config;`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const fn =
|
||||
edgeRoutes.has(route) && edgeFunction
|
||||
const funcIndex = serverBundles.findIndex(bundle => {
|
||||
return bundle.routes.includes(route.id);
|
||||
});
|
||||
const func = functions[funcIndex];
|
||||
|
||||
if (!func) {
|
||||
throw new Error(`Could not determine server bundle for "${route.id}"`);
|
||||
}
|
||||
|
||||
output[path] =
|
||||
func instanceof 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
|
||||
// new one for each page.
|
||||
new EdgeFunction({
|
||||
...edgeFunction,
|
||||
...func,
|
||||
name: path,
|
||||
})
|
||||
: nodeFunction;
|
||||
output[path] = fn;
|
||||
: func;
|
||||
|
||||
// If this is a dynamic route then add a Vercel route
|
||||
const re = getRegExpFromPath(path);
|
||||
@@ -341,11 +377,20 @@ module.exports = config;`;
|
||||
}
|
||||
|
||||
// Add a 404 path for not found pages to be server-side rendered by Remix.
|
||||
// Use the edge function if one was generated, otherwise use Node.js.
|
||||
// Use an edge function bundle if one was generated, otherwise use Node.js.
|
||||
if (!output['404']) {
|
||||
output['404'] = edgeFunction
|
||||
? new EdgeFunction({ ...edgeFunction, name: '404' })
|
||||
: nodeFunction;
|
||||
const edgeFunctionIndex = Array.from(serverBundlesMap.values()).findIndex(
|
||||
routes => {
|
||||
const runtime = resolvedConfigsMap.get(routes[0])?.runtime;
|
||||
return runtime === 'edge';
|
||||
}
|
||||
);
|
||||
const func =
|
||||
edgeFunctionIndex !== -1 ? functions[edgeFunctionIndex] : functions[0];
|
||||
output['404'] =
|
||||
func instanceof EdgeFunction
|
||||
? new EdgeFunction({ ...func, name: '404' })
|
||||
: func;
|
||||
}
|
||||
routes.push({
|
||||
src: '/(.*)',
|
||||
@@ -366,19 +411,27 @@ async function createRenderNodeFunction(
|
||||
rootDir: string,
|
||||
serverBuildPath: string,
|
||||
serverEntryPoint: string | undefined,
|
||||
remixVersion: string
|
||||
remixVersion: string,
|
||||
config: ResolvedNodeRouteConfig
|
||||
): Promise<NodejsLambda> {
|
||||
const files: Files = {};
|
||||
|
||||
let handler = relative(rootDir, serverBuildPath);
|
||||
let handlerPath = join(rootDir, handler);
|
||||
if (!serverEntryPoint) {
|
||||
handler = join(dirname(handler), 'server-node.mjs');
|
||||
const baseServerBuildPath = basename(serverBuildPath, '.js');
|
||||
handler = join(dirname(handler), `server-${baseServerBuildPath}.mjs`);
|
||||
handlerPath = join(rootDir, handler);
|
||||
|
||||
// Copy the `server-node.mjs` file into the "build" directory
|
||||
const sourceHandlerPath = join(__dirname, '../server-node.mjs');
|
||||
await fs.copyFile(sourceHandlerPath, handlerPath);
|
||||
const nodeServerSrc = await nodeServerSrcPromise;
|
||||
await fs.writeFile(
|
||||
handlerPath,
|
||||
nodeServerSrc.replace(
|
||||
'@remix-run/dev/server-build',
|
||||
`./${baseServerBuildPath}.js`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Trace the handler with `@vercel/nft`
|
||||
@@ -403,6 +456,9 @@ async function createRenderNodeFunction(
|
||||
shouldAddSourcemapSupport: false,
|
||||
operationType: 'SSR',
|
||||
experimentalResponseStreaming: true,
|
||||
regions: config.regions,
|
||||
memory: config.memory,
|
||||
maxDuration: config.maxDuration,
|
||||
framework: {
|
||||
slug: 'remix',
|
||||
version: remixVersion,
|
||||
@@ -417,19 +473,27 @@ async function createRenderEdgeFunction(
|
||||
rootDir: string,
|
||||
serverBuildPath: string,
|
||||
serverEntryPoint: string | undefined,
|
||||
remixVersion: string
|
||||
remixVersion: string,
|
||||
config: ResolvedEdgeRouteConfig
|
||||
): Promise<EdgeFunction> {
|
||||
const files: Files = {};
|
||||
|
||||
let handler = relative(rootDir, serverBuildPath);
|
||||
let handlerPath = join(rootDir, handler);
|
||||
if (!serverEntryPoint) {
|
||||
handler = join(dirname(handler), 'server-edge.mjs');
|
||||
const baseServerBuildPath = basename(serverBuildPath, '.js');
|
||||
handler = join(dirname(handler), `server-${baseServerBuildPath}.mjs`);
|
||||
handlerPath = join(rootDir, handler);
|
||||
|
||||
// Copy the `server-edge.mjs` file into the "build" directory
|
||||
const sourceHandlerPath = join(__dirname, '../server-edge.mjs');
|
||||
await fs.copyFile(sourceHandlerPath, handlerPath);
|
||||
const edgeServerSrc = await edgeServerSrcPromise;
|
||||
await fs.writeFile(
|
||||
handlerPath,
|
||||
edgeServerSrc.replace(
|
||||
'@remix-run/dev/server-build',
|
||||
`./${baseServerBuildPath}.js`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let remixRunVercelPkgJson: string | undefined;
|
||||
@@ -517,6 +581,7 @@ async function createRenderEdgeFunction(
|
||||
deploymentTarget: 'v8-worker',
|
||||
name: 'render',
|
||||
entrypoint: handler,
|
||||
regions: config.regions,
|
||||
framework: {
|
||||
slug: 'remix',
|
||||
version: remixVersion,
|
||||
@@ -614,10 +679,6 @@ async function ensureSymlink(
|
||||
);
|
||||
}
|
||||
|
||||
function isEdgeRuntime(runtime: string): boolean {
|
||||
return runtime === 'edge' || runtime === 'experimental-edge';
|
||||
}
|
||||
|
||||
async function chdirAndReadConfig(dir: string) {
|
||||
const originalCwd = process.cwd();
|
||||
let remixConfig: RemixConfig;
|
||||
|
||||
@@ -5,6 +5,22 @@ import type {
|
||||
ConfigRoute,
|
||||
RouteManifest,
|
||||
} from '@remix-run/dev/dist/config/routes';
|
||||
import type { BaseFunctionConfig } from '@vercel/static-config';
|
||||
|
||||
export interface ResolvedNodeRouteConfig {
|
||||
runtime: 'nodejs';
|
||||
regions?: string[];
|
||||
maxDuration?: number;
|
||||
memory?: number;
|
||||
}
|
||||
export interface ResolvedEdgeRouteConfig {
|
||||
runtime: 'edge';
|
||||
regions?: BaseFunctionConfig['regions'];
|
||||
}
|
||||
|
||||
export type ResolvedRouteConfig =
|
||||
| ResolvedNodeRouteConfig
|
||||
| ResolvedEdgeRouteConfig;
|
||||
|
||||
const configExts = ['.js', '.cjs', '.mjs'];
|
||||
|
||||
@@ -18,6 +34,60 @@ export function findConfig(dir: string, basename: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isEdgeRuntime(runtime: string): boolean {
|
||||
return runtime === 'edge' || runtime === 'experimental-edge';
|
||||
}
|
||||
|
||||
export function getResolvedRouteConfig(
|
||||
route: ConfigRoute,
|
||||
routes: RouteManifest,
|
||||
configs: Map<ConfigRoute, BaseFunctionConfig | null>
|
||||
): ResolvedRouteConfig {
|
||||
let runtime: ResolvedRouteConfig['runtime'] | undefined;
|
||||
let regions: ResolvedRouteConfig['regions'];
|
||||
let maxDuration: ResolvedNodeRouteConfig['maxDuration'];
|
||||
let memory: ResolvedNodeRouteConfig['memory'];
|
||||
|
||||
for (const currentRoute of getRouteIterator(route, routes)) {
|
||||
const staticConfig = configs.get(currentRoute);
|
||||
if (staticConfig) {
|
||||
if (typeof runtime === 'undefined' && staticConfig.runtime) {
|
||||
runtime = isEdgeRuntime(staticConfig.runtime) ? 'edge' : 'nodejs';
|
||||
}
|
||||
if (typeof regions === 'undefined') {
|
||||
regions = staticConfig.regions;
|
||||
}
|
||||
if (typeof maxDuration === 'undefined') {
|
||||
maxDuration = staticConfig.maxDuration;
|
||||
}
|
||||
if (typeof memory === 'undefined') {
|
||||
memory = staticConfig.memory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(regions)) {
|
||||
regions = Array.from(new Set(regions)).sort();
|
||||
}
|
||||
|
||||
if (runtime === 'edge') {
|
||||
return { runtime, regions };
|
||||
}
|
||||
|
||||
if (regions && !Array.isArray(regions)) {
|
||||
throw new Error(
|
||||
`"regions" for route "${route.id}" must be an array of strings`
|
||||
);
|
||||
}
|
||||
|
||||
return { runtime: 'nodejs', regions, maxDuration, memory };
|
||||
}
|
||||
|
||||
export function calculateRouteConfigHash(config: ResolvedRouteConfig): string {
|
||||
const str = JSON.stringify(config);
|
||||
return Buffer.from(str).toString('base64url');
|
||||
}
|
||||
|
||||
export function isLayoutRoute(
|
||||
routeId: string,
|
||||
routes: Pick<ConfigRoute, 'id' | 'parentId'>[]
|
||||
|
||||
Reference in New Issue
Block a user