mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 04:22:12 +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:
@@ -1,3 +1,3 @@
|
|||||||
import { createRequestHandler } from '@remix-run/server-runtime';
|
import { createRequestHandler } from '@remix-run/server-runtime';
|
||||||
import build from './build-edge.js';
|
import build from '@remix-run/dev/server-build';
|
||||||
export default createRequestHandler(build);
|
export default createRequestHandler(build);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
installGlobals();
|
installGlobals();
|
||||||
|
|
||||||
import build from './build-node.js';
|
import build from '@remix-run/dev/server-build';
|
||||||
|
|
||||||
const handleRequest = createRemixRequestHandler(build, process.env.NODE_ENV);
|
const handleRequest = createRemixRequestHandler(build, process.env.NODE_ENV);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
scanParentDirs,
|
scanParentDirs,
|
||||||
walkParentDirs,
|
walkParentDirs,
|
||||||
} from '@vercel/build-utils';
|
} from '@vercel/build-utils';
|
||||||
import { getConfig, BaseFunctionConfig } from '@vercel/static-config';
|
import { getConfig } from '@vercel/static-config';
|
||||||
import { nodeFileTrace } from '@vercel/nft';
|
import { nodeFileTrace } from '@vercel/nft';
|
||||||
import { readConfig } from '@remix-run/dev/dist/config';
|
import { readConfig } from '@remix-run/dev/dist/config';
|
||||||
import type {
|
import type {
|
||||||
@@ -29,14 +29,19 @@ import type {
|
|||||||
PackageJson,
|
PackageJson,
|
||||||
BuildResultV2Typical,
|
BuildResultV2Typical,
|
||||||
} from '@vercel/build-utils';
|
} from '@vercel/build-utils';
|
||||||
|
import type { BaseFunctionConfig } from '@vercel/static-config';
|
||||||
import type { RemixConfig } from '@remix-run/dev/dist/config';
|
import type { RemixConfig } from '@remix-run/dev/dist/config';
|
||||||
import type { ConfigRoute } from '@remix-run/dev/dist/config/routes';
|
import type { ConfigRoute } from '@remix-run/dev/dist/config/routes';
|
||||||
import {
|
import {
|
||||||
|
calculateRouteConfigHash,
|
||||||
findConfig,
|
findConfig,
|
||||||
getPathFromRoute,
|
getPathFromRoute,
|
||||||
getRegExpFromPath,
|
getRegExpFromPath,
|
||||||
getRouteIterator,
|
getResolvedRouteConfig,
|
||||||
isLayoutRoute,
|
isLayoutRoute,
|
||||||
|
ResolvedRouteConfig,
|
||||||
|
ResolvedNodeRouteConfig,
|
||||||
|
ResolvedEdgeRouteConfig,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
const _require: typeof require = eval('require');
|
const _require: typeof require = eval('require');
|
||||||
@@ -45,6 +50,15 @@ const REMIX_RUN_DEV_PATH = dirname(
|
|||||||
_require.resolve('@remix-run/dev/package.json')
|
_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 ({
|
export const build: BuildV2 = async ({
|
||||||
entrypoint,
|
entrypoint,
|
||||||
files,
|
files,
|
||||||
@@ -133,49 +147,53 @@ export const build: BuildV2 = async ({
|
|||||||
|
|
||||||
// Read the `export const config` (if any) for each route
|
// Read the `export const config` (if any) for each route
|
||||||
const project = new Project();
|
const project = new Project();
|
||||||
const staticConfigsMap = new Map<ConfigRoute, BaseFunctionConfig>();
|
const staticConfigsMap = new Map<ConfigRoute, BaseFunctionConfig | null>();
|
||||||
for (const route of remixRoutes) {
|
for (const route of remixRoutes) {
|
||||||
const routePath = join(remixConfig.appDirectory, route.file);
|
const routePath = join(remixConfig.appDirectory, route.file);
|
||||||
const staticConfig = getConfig(project, routePath);
|
const staticConfig = getConfig(project, routePath);
|
||||||
if (staticConfig) {
|
staticConfigsMap.set(route, staticConfig);
|
||||||
staticConfigsMap.set(route, staticConfig);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Figure out which pages should be edge functions
|
const resolvedConfigsMap = new Map<ConfigRoute, ResolvedRouteConfig>();
|
||||||
const edgeRoutes = new Set<ConfigRoute>();
|
for (const route of remixRoutes) {
|
||||||
const nodeRoutes = new Set<ConfigRoute>();
|
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) {
|
for (const route of remixRoutes) {
|
||||||
if (isLayoutRoute(route.id, remixRoutes)) continue;
|
if (isLayoutRoute(route.id, remixRoutes)) continue;
|
||||||
|
|
||||||
// Support runtime inheritance when defined in a parent route,
|
const config = resolvedConfigsMap.get(route);
|
||||||
// by iterating through the route's parent hierarchy until a
|
if (!config) {
|
||||||
// config containing "runtime" is found.
|
throw new Error(`Expected resolved config for "${route.id}"`);
|
||||||
let isEdge = false;
|
}
|
||||||
for (const currentRoute of getRouteIterator(route, remixConfig.routes)) {
|
const hash = calculateRouteConfigHash(config);
|
||||||
const staticConfig = staticConfigsMap.get(currentRoute);
|
|
||||||
if (staticConfig?.runtime) {
|
let routesForHash = serverBundlesMap.get(hash);
|
||||||
isEdge = isEdgeRuntime(staticConfig.runtime);
|
if (!Array.isArray(routesForHash)) {
|
||||||
break;
|
routesForHash = [];
|
||||||
}
|
serverBundlesMap.set(hash, routesForHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEdge) {
|
routesForHash.push(route);
|
||||||
edgeRoutes.add(route);
|
|
||||||
} else {
|
|
||||||
nodeRoutes.add(route);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const serverBundles = [
|
|
||||||
{
|
const serverBundles = Array.from(serverBundlesMap.entries()).map(
|
||||||
serverBuildPath: 'build/build-edge.js',
|
([hash, routes]) => {
|
||||||
routes: Array.from(edgeRoutes).map(r => r.id),
|
const runtime = resolvedConfigsMap.get(routes[0])?.runtime ?? 'nodejs';
|
||||||
},
|
return {
|
||||||
{
|
serverBuildPath: `build/build-${runtime}-${hash}.js`,
|
||||||
serverBuildPath: 'build/build-node.js',
|
routes: routes.map(r => r.id),
|
||||||
routes: Array.from(nodeRoutes).map(r => r.id),
|
};
|
||||||
},
|
}
|
||||||
];
|
);
|
||||||
|
|
||||||
// We need to patch the `remix.config.js` file to force some values necessary
|
// 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
|
// 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'),
|
ensureResolvable(entrypointFsDirname, repoRootPath, '@remix-run/node'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [staticFiles, nodeFunction, edgeFunction] = await Promise.all([
|
const [staticFiles, ...functions] = await Promise.all([
|
||||||
glob('**', join(entrypointFsDirname, 'public')),
|
glob('**', join(entrypointFsDirname, 'public')),
|
||||||
createRenderNodeFunction(
|
...serverBundles.map(bundle => {
|
||||||
nodeVersion,
|
const firstRoute = remixConfig.routes[bundle.routes[0]];
|
||||||
entrypointFsDirname,
|
const config = resolvedConfigsMap.get(firstRoute) ?? {
|
||||||
repoRootPath,
|
runtime: 'nodejs',
|
||||||
join(entrypointFsDirname, 'build/build-node.js'),
|
};
|
||||||
remixConfig.serverEntryPoint,
|
|
||||||
remixVersion
|
if (config.runtime === 'edge') {
|
||||||
),
|
return createRenderEdgeFunction(
|
||||||
edgeRoutes.size > 0
|
|
||||||
? createRenderEdgeFunction(
|
|
||||||
entrypointFsDirname,
|
entrypointFsDirname,
|
||||||
repoRootPath,
|
repoRootPath,
|
||||||
join(entrypointFsDirname, 'build/build-edge.js'),
|
join(entrypointFsDirname, bundle.serverBuildPath),
|
||||||
remixConfig.serverEntryPoint,
|
remixConfig.serverEntryPoint,
|
||||||
remixVersion
|
remixVersion,
|
||||||
)
|
config
|
||||||
: undefined,
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createRenderNodeFunction(
|
||||||
|
nodeVersion,
|
||||||
|
entrypointFsDirname,
|
||||||
|
repoRootPath,
|
||||||
|
join(entrypointFsDirname, bundle.serverBuildPath),
|
||||||
|
remixConfig.serverEntryPoint,
|
||||||
|
remixVersion,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const output: BuildResultV2Typical['output'] = staticFiles;
|
const output: BuildResultV2Typical['output'] = staticFiles;
|
||||||
@@ -317,18 +345,26 @@ module.exports = config;`;
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn =
|
const funcIndex = serverBundles.findIndex(bundle => {
|
||||||
edgeRoutes.has(route) && edgeFunction
|
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.
|
? // `EdgeFunction` currently requires the "name" property to be set.
|
||||||
// Ideally this property will be removed, at which point we can
|
// Ideally this property will be removed, at which point we can
|
||||||
// return the same `edgeFunction` instance instead of creating a
|
// return the same `edgeFunction` instance instead of creating a
|
||||||
// new one for each page.
|
// new one for each page.
|
||||||
new EdgeFunction({
|
new EdgeFunction({
|
||||||
...edgeFunction,
|
...func,
|
||||||
name: path,
|
name: path,
|
||||||
})
|
})
|
||||||
: nodeFunction;
|
: func;
|
||||||
output[path] = fn;
|
|
||||||
|
|
||||||
// If this is a dynamic route then add a Vercel route
|
// If this is a dynamic route then add a Vercel route
|
||||||
const re = getRegExpFromPath(path);
|
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.
|
// 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']) {
|
if (!output['404']) {
|
||||||
output['404'] = edgeFunction
|
const edgeFunctionIndex = Array.from(serverBundlesMap.values()).findIndex(
|
||||||
? new EdgeFunction({ ...edgeFunction, name: '404' })
|
routes => {
|
||||||
: nodeFunction;
|
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({
|
routes.push({
|
||||||
src: '/(.*)',
|
src: '/(.*)',
|
||||||
@@ -366,19 +411,27 @@ async function createRenderNodeFunction(
|
|||||||
rootDir: string,
|
rootDir: string,
|
||||||
serverBuildPath: string,
|
serverBuildPath: string,
|
||||||
serverEntryPoint: string | undefined,
|
serverEntryPoint: string | undefined,
|
||||||
remixVersion: string
|
remixVersion: string,
|
||||||
|
config: ResolvedNodeRouteConfig
|
||||||
): Promise<NodejsLambda> {
|
): Promise<NodejsLambda> {
|
||||||
const files: Files = {};
|
const files: Files = {};
|
||||||
|
|
||||||
let handler = relative(rootDir, serverBuildPath);
|
let handler = relative(rootDir, serverBuildPath);
|
||||||
let handlerPath = join(rootDir, handler);
|
let handlerPath = join(rootDir, handler);
|
||||||
if (!serverEntryPoint) {
|
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);
|
handlerPath = join(rootDir, handler);
|
||||||
|
|
||||||
// Copy the `server-node.mjs` file into the "build" directory
|
// Copy the `server-node.mjs` file into the "build" directory
|
||||||
const sourceHandlerPath = join(__dirname, '../server-node.mjs');
|
const nodeServerSrc = await nodeServerSrcPromise;
|
||||||
await fs.copyFile(sourceHandlerPath, handlerPath);
|
await fs.writeFile(
|
||||||
|
handlerPath,
|
||||||
|
nodeServerSrc.replace(
|
||||||
|
'@remix-run/dev/server-build',
|
||||||
|
`./${baseServerBuildPath}.js`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace the handler with `@vercel/nft`
|
// Trace the handler with `@vercel/nft`
|
||||||
@@ -403,6 +456,9 @@ async function createRenderNodeFunction(
|
|||||||
shouldAddSourcemapSupport: false,
|
shouldAddSourcemapSupport: false,
|
||||||
operationType: 'SSR',
|
operationType: 'SSR',
|
||||||
experimentalResponseStreaming: true,
|
experimentalResponseStreaming: true,
|
||||||
|
regions: config.regions,
|
||||||
|
memory: config.memory,
|
||||||
|
maxDuration: config.maxDuration,
|
||||||
framework: {
|
framework: {
|
||||||
slug: 'remix',
|
slug: 'remix',
|
||||||
version: remixVersion,
|
version: remixVersion,
|
||||||
@@ -417,19 +473,27 @@ async function createRenderEdgeFunction(
|
|||||||
rootDir: string,
|
rootDir: string,
|
||||||
serverBuildPath: string,
|
serverBuildPath: string,
|
||||||
serverEntryPoint: string | undefined,
|
serverEntryPoint: string | undefined,
|
||||||
remixVersion: string
|
remixVersion: string,
|
||||||
|
config: ResolvedEdgeRouteConfig
|
||||||
): Promise<EdgeFunction> {
|
): Promise<EdgeFunction> {
|
||||||
const files: Files = {};
|
const files: Files = {};
|
||||||
|
|
||||||
let handler = relative(rootDir, serverBuildPath);
|
let handler = relative(rootDir, serverBuildPath);
|
||||||
let handlerPath = join(rootDir, handler);
|
let handlerPath = join(rootDir, handler);
|
||||||
if (!serverEntryPoint) {
|
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);
|
handlerPath = join(rootDir, handler);
|
||||||
|
|
||||||
// Copy the `server-edge.mjs` file into the "build" directory
|
// Copy the `server-edge.mjs` file into the "build" directory
|
||||||
const sourceHandlerPath = join(__dirname, '../server-edge.mjs');
|
const edgeServerSrc = await edgeServerSrcPromise;
|
||||||
await fs.copyFile(sourceHandlerPath, handlerPath);
|
await fs.writeFile(
|
||||||
|
handlerPath,
|
||||||
|
edgeServerSrc.replace(
|
||||||
|
'@remix-run/dev/server-build',
|
||||||
|
`./${baseServerBuildPath}.js`
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let remixRunVercelPkgJson: string | undefined;
|
let remixRunVercelPkgJson: string | undefined;
|
||||||
@@ -517,6 +581,7 @@ async function createRenderEdgeFunction(
|
|||||||
deploymentTarget: 'v8-worker',
|
deploymentTarget: 'v8-worker',
|
||||||
name: 'render',
|
name: 'render',
|
||||||
entrypoint: handler,
|
entrypoint: handler,
|
||||||
|
regions: config.regions,
|
||||||
framework: {
|
framework: {
|
||||||
slug: 'remix',
|
slug: 'remix',
|
||||||
version: remixVersion,
|
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) {
|
async function chdirAndReadConfig(dir: string) {
|
||||||
const originalCwd = process.cwd();
|
const originalCwd = process.cwd();
|
||||||
let remixConfig: RemixConfig;
|
let remixConfig: RemixConfig;
|
||||||
|
|||||||
@@ -5,6 +5,22 @@ import type {
|
|||||||
ConfigRoute,
|
ConfigRoute,
|
||||||
RouteManifest,
|
RouteManifest,
|
||||||
} from '@remix-run/dev/dist/config/routes';
|
} 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'];
|
const configExts = ['.js', '.cjs', '.mjs'];
|
||||||
|
|
||||||
@@ -18,6 +34,60 @@ export function findConfig(dir: string, basename: string): string | undefined {
|
|||||||
return 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(
|
export function isLayoutRoute(
|
||||||
routeId: string,
|
routeId: string,
|
||||||
routes: Pick<ConfigRoute, 'id' | 'parentId'>[]
|
routes: Pick<ConfigRoute, 'id' | 'parentId'>[]
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import { json } from '@remix-run/node';
|
|||||||
|
|
||||||
export async function loader() {
|
export async function loader() {
|
||||||
const hi = await new Promise<string>((resolve, reject) => {
|
const hi = await new Promise<string>((resolve, reject) => {
|
||||||
exec('echo hi from the B page', (err, stdout) => {
|
exec(
|
||||||
if (err) return reject(err);
|
`echo hi from the B page running in ${process.env.VERCEL_REGION}`,
|
||||||
resolve(stdout);
|
(err, stdout) => {
|
||||||
});
|
if (err) return reject(err);
|
||||||
|
resolve(stdout);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
return json({ hi });
|
return json({ hi });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { loader } from '~/b.server';
|
import { loader } from '~/b.server';
|
||||||
import { useLoaderData } from '@remix-run/react';
|
import { useLoaderData } from '@remix-run/react';
|
||||||
|
|
||||||
|
export const config = { regions: ['sfo1'] };
|
||||||
|
|
||||||
export { loader };
|
export { loader };
|
||||||
|
|
||||||
export default function B() {
|
export default function B() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"probes": [
|
"probes": [
|
||||||
{ "path": "/", "mustContain": "Welcome to Remix" },
|
{ "path": "/", "mustContain": "Welcome to Remix" },
|
||||||
{ "path": "/edge", "mustContain": "Welcome to Remix@Edge" },
|
{ "path": "/edge", "mustContain": "Welcome to Remix@Edge" },
|
||||||
{ "path": "/b", "mustContain": "hi from the B page" },
|
{ "path": "/b", "mustContain": "hi from the B page running in sfo1" },
|
||||||
{ "path": "/nested", "mustContain": "Nested index page" },
|
{ "path": "/nested", "mustContain": "Nested index page" },
|
||||||
{ "path": "/nested/another", "mustContain": "Nested another page" },
|
{ "path": "/nested/another", "mustContain": "Nested another page" },
|
||||||
{ "path": "/nested/index", "status": 404, "mustContain": "Not Found" },
|
{ "path": "/nested/index", "status": 404, "mustContain": "Not Found" },
|
||||||
|
|||||||
76
packages/remix/test/unit.get-resolved-route-config.test.ts
vendored
Normal file
76
packages/remix/test/unit.get-resolved-route-config.test.ts
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { getResolvedRouteConfig } from '../src/utils';
|
||||||
|
import type {
|
||||||
|
ConfigRoute,
|
||||||
|
RouteManifest,
|
||||||
|
} from '@remix-run/dev/dist/config/routes';
|
||||||
|
import type { BaseFunctionConfig } from '@vercel/static-config';
|
||||||
|
|
||||||
|
describe('getResolvedRouteConfig()', () => {
|
||||||
|
const staticConfigsMap = new Map<ConfigRoute, BaseFunctionConfig | null>([
|
||||||
|
[{ id: 'root', file: 'root.tsx' }, null],
|
||||||
|
[
|
||||||
|
{ id: 'routes/edge', file: 'routes/edge.tsx', parentId: 'root' },
|
||||||
|
{ runtime: 'edge' },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'routes/edge/sfo1',
|
||||||
|
file: 'routes/edge/sfo1.tsx',
|
||||||
|
parentId: 'routes/edge',
|
||||||
|
},
|
||||||
|
{ regions: ['sfo1'] },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'routes/edge/iad1',
|
||||||
|
file: 'routes/edge/iad1.tsx',
|
||||||
|
parentId: 'routes/edge',
|
||||||
|
},
|
||||||
|
{ regions: ['iad1'] },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ id: 'routes/node', file: 'routes/node.tsx' },
|
||||||
|
{ runtime: 'nodejs', regions: ['sfo1'] },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'routes/node/mem',
|
||||||
|
file: 'routes/node/mem.tsx',
|
||||||
|
parentId: 'routes/node',
|
||||||
|
},
|
||||||
|
{ maxDuration: 5, memory: 3008 },
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const routes: RouteManifest = {};
|
||||||
|
for (const route of staticConfigsMap.keys()) {
|
||||||
|
routes[route.id] = route;
|
||||||
|
}
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ id: 'root', expected: { runtime: 'nodejs' } },
|
||||||
|
{ id: 'routes/edge', expected: { runtime: 'edge' } },
|
||||||
|
{
|
||||||
|
id: 'routes/edge/sfo1',
|
||||||
|
expected: { runtime: 'edge', regions: ['sfo1'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'routes/edge/iad1',
|
||||||
|
expected: { runtime: 'edge', regions: ['iad1'] },
|
||||||
|
},
|
||||||
|
{ id: 'routes/node', expected: { runtime: 'nodejs', regions: ['sfo1'] } },
|
||||||
|
{
|
||||||
|
id: 'routes/node/mem',
|
||||||
|
expected: {
|
||||||
|
runtime: 'nodejs',
|
||||||
|
regions: ['sfo1'],
|
||||||
|
maxDuration: 5,
|
||||||
|
memory: 3008,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])('should resolve config for "$id" route', ({ id, expected }) => {
|
||||||
|
const route = routes[id];
|
||||||
|
const config = getResolvedRouteConfig(route, routes, staticConfigsMap);
|
||||||
|
expect(config).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user