Compare commits

..

15 Commits

Author SHA1 Message Date
Sean Massa
2fd3315221 Publish Stable
- vercel@28.16.3
 - @vercel/next@3.5.1
 - @vercel/remix@1.3.5
2023-02-21 08:53:23 -06:00
JJ Kasper
54ef027cbe [next] Fix rsc routes order (#9493)
This ensures we maintain the correct order for our rsc routes with
reference to middleware so that they match correctly. This also adds a
regression test to ensure it's working as expected.

Fixes: https://github.com/vercel/next.js/issues/45331
x-ref: [slack
thread](https://vercel.slack.com/archives/C035J346QQL/p1676926522772859?thread_ts=1676926096.412539&cid=C035J346QQL)
2023-02-20 17:54:17 -08:00
Shu Ding
6620c7f600 [next] Add initial vary header to all prerendered pages when RSC is enabled (#9481)
This PR changes the fallback headers that relate to RSC to the defaults that Next.js currently uses. Also, it sets the initial `vary` header to all prerendered routes when RSC is enabled (`routesManifest?.rsc`), even for the pages directory. 

That's because although the pages directory won't return any RSC payload, it can still be used in a project that contains app routes. When the app route requests a page route for RSC data, it's important for the browser to not accidentally cache that result hence we need the `vary` header to set there as well.

More related discussions can be found [here](https://linear.app/vercel/issue/NEXT-382/add-vary-rsc-etc-header-to-all-responses-to-ensure-browser-caching).
2023-02-20 19:35:06 +00:00
JJ Kasper
38f40f1c15 [next] Handle prerender-manifest v4 (#9489)
Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
2023-02-20 11:41:24 +01:00
Nathan Rajlich
63211b8b89 [remix] Add unit tests (#9469)
Moves parts of the `@vercel/remix` builder into util functions that have isolated unit tests. No functionality changes.
2023-02-17 18:46:33 +00:00
Ikko Eltociear Ashimine
83ee5ea2b8 [cli] Fix typo in get-latest-worker.js (#9470)
persistance -> persistence

Co-authored-by: Chris Barber <chris.barber@vercel.com>
2023-02-17 11:45:44 -05:00
Ethan Arrowood
f063645646 [examples] Update remix template (#9472)
Previous PR was merged automatically but need to fix a dependency
version
2023-02-17 09:42:26 -07:00
Ethan Arrowood
4f8c5e344d [examples] update remix template (#9455)
Updates our remix template so that it works with our new Remix changes
2023-02-17 15:10:17 +00:00
Nathan Rajlich
70a53515bd Publish Stable
- vercel@28.16.2
 - @vercel/fs-detectors@3.8.0
 - @vercel/next@3.5.0
 - @vercel/remix@1.3.4
 - @vercel/ruby@1.3.66
 - @vercel/static-build@1.3.10
2023-02-16 15:12:31 -08:00
Sean Massa
4d4f0fa672 [next] Add Operation Types to Next.js Lambdas (#9196)
In order to have Next.js Lambdas show their operation types more specifically in the build output in the dashboard, the builder needs to return the Lambdas with `operationType` set to the appropriate value.

This PR adds those values. This allows the Richer Deployment Outputs to show the different types of serverless functions:
<img width="228" alt="Screenshot 2023-02-03 at 3 49 42 PM" src="https://user-images.githubusercontent.com/41545/216717479-d02fbd4a-fa62-479d-8b65-bd77fdcdb26c.png">
2023-02-16 22:59:14 +00:00
Felix Haus
46c0fd153a [fs-detectors] Remove increments of 64 limit for function memory (#9465)
Missed this occurrence so it still prevents the upload of serverless
functions that have a mem value that is not dividable by 64.
Should be the last place before we can ship the documentation update.

#### Related PRs
- #9440

Co-authored-by: Steven <steven@ceriously.com>
2023-02-16 16:42:42 -05:00
Marc Greenstock
1c8b4717e3 [ruby] fix: HEAD requests (#9436)
When WEBrick receives `HEAD` requests it discards the body (i.e.
`req.body.nil? => true`), this causes Vercel to throw a
`BODY_NOT_A_STRING_FROM_FUNCTION` since it is expecting the serverless
function to respond with a string in the body.

---------

Co-authored-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Steven <steven@ceriously.com>
2023-02-16 16:41:56 -05:00
Nathan Rajlich
d52d26eaac [remix] Install Node globals (#9467)
Fixes https://github.com/vercel/community/discussions/1547.
Fixes https://github.com/vercel/community/discussions/1549.
2023-02-16 21:25:19 +00:00
Vincent Voyer
db65728fc4 Publish Stable
- vercel@28.16.1
2023-02-16 15:36:16 +01:00
Vincent Voyer
a788d06f85 [cli]: fix merging of vercel.json and build result crons (#9464)
Ensures that existing crons and crons from vercel.json are merged
together correctly.
2023-02-16 15:34:56 +01:00
53 changed files with 2266 additions and 2416 deletions

View File

@@ -13,10 +13,10 @@ function hydrate() {
});
}
if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate);
if (typeof requestIdleCallback === "function") {
requestIdleCallback(hydrate);
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
window.setTimeout(hydrate, 1);
setTimeout(hydrate, 1);
}

View File

@@ -1,21 +1,14 @@
import type { EntryContext } from "@remix-run/node";
import handleRequest from "@vercel/remix-entry-server";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
import type { EntryContext } from "@remix-run/server-runtime";
export default function handleRequest(
export default function (
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);
responseHeaders.set("Content-Type", "text/html");
return new Response("<!DOCTYPE html>" + markup, {
headers: responseHeaders,
status: responseStatusCode,
});
const remixServer = <RemixServer context={remixContext} url={request.url} />;
return handleRequest(request, responseStatusCode, responseHeaders, remixServer)
}

View File

@@ -0,0 +1,11 @@
export const config = {
runtime: 'edge'
};
export default function Edge() {
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
<h1>Welcome to Remix@Edge</h1>
</div>
);
}

View File

@@ -6,20 +6,20 @@
"dev": "remix dev"
},
"dependencies": {
"@remix-run/node": "^1.7.6",
"@remix-run/react": "^1.7.6",
"@remix-run/vercel": "^1.7.6",
"@vercel/analytics": "^0.1.5",
"@vercel/node": "^2.7.0",
"@remix-run/node": "^1.13.0",
"@remix-run/react": "^1.13.0",
"@remix-run/serve": "^1.13.0",
"@remix-run/server-runtime": "^1.13.0",
"@vercel/analytics": "^0.1.10",
"@vercel/remix-entry-server": "^0.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@remix-run/dev": "^1.7.6",
"@remix-run/eslint-config": "^1.7.6",
"@remix-run/serve": "^1.7.6",
"@remix-run/dev": "^1.13.0",
"@remix-run/eslint-config": "^1.13.0",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@types/react-dom": "^18.0.11",
"eslint": "^8.28.0",
"typescript": "^4.9.3"
},

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,10 @@
/** @type {import('@remix-run/dev').AppConfig} */
/**
* @type {import('@remix-run/dev').AppConfig}
*/
module.exports = {
serverBuildTarget: "vercel",
// When running locally in development mode, we use the built in remix
// server. This does not understand the vercel lambda module format,
// so we default back to the standard build output.
server: process.env.NODE_ENV === "development" ? undefined : "./server.js",
ignoredRouteFiles: ["**/.*"],
ignoredRouteFiles: ['**/.*'],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "api/index.js",
// serverBuildPath: "build/index.js",
// publicPath: "/build/",
};

View File

@@ -1,4 +0,0 @@
import { createRequestHandler } from "@remix-run/vercel";
import * as build from "@remix-run/dev/server-build";
export default createRequestHandler({ build, mode: process.env.NODE_ENV });

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "28.16.0",
"version": "28.16.3",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -44,13 +44,13 @@
"@vercel/build-utils": "6.3.0",
"@vercel/go": "2.3.7",
"@vercel/hydrogen": "0.0.53",
"@vercel/next": "3.4.7",
"@vercel/next": "3.5.1",
"@vercel/node": "2.9.6",
"@vercel/python": "3.1.49",
"@vercel/redwood": "1.1.5",
"@vercel/remix": "1.3.3",
"@vercel/ruby": "1.3.65",
"@vercel/static-build": "1.3.9"
"@vercel/remix": "1.3.5",
"@vercel/ruby": "1.3.66",
"@vercel/static-build": "1.3.10"
},
"devDependencies": {
"@alex_neo/jest-expect-message": "1.0.5",
@@ -96,7 +96,7 @@
"@vercel/client": "12.4.0",
"@vercel/error-utils": "1.0.8",
"@vercel/frameworks": "1.3.1",
"@vercel/fs-detectors": "3.7.14",
"@vercel/fs-detectors": "3.8.0",
"@vercel/fun": "1.0.4",
"@vercel/ncc": "0.24.0",
"@vercel/routing-utils": "2.1.9",

View File

@@ -751,12 +751,12 @@ function mergeImages(
}
function mergeCrons(
crons: BuildOutputConfig['crons'],
crons: BuildOutputConfig['crons'] = [],
buildResults: Iterable<BuildResult | BuildOutputConfig>
): BuildOutputConfig['crons'] {
for (const result of buildResults) {
if ('crons' in result && result.crons) {
crons = Object.assign({}, crons, result.crons);
crons = crons.concat(result.crons);
}
}
return crons;

View File

@@ -22,7 +22,7 @@ const { format, inspect } = require('util');
/**
* An simple output helper which accumulates error and debug log messages in
* memory for potential persistance to disk while immediately outputting errors
* memory for potential persistence to disk while immediately outputting errors
* and debug messages, when the `--debug` flag is set, to `stderr`.
*/
class WorkerOutput {

View File

@@ -0,0 +1,7 @@
{
"orgId": ".",
"projectId": ".",
"settings": {
"framework": null
}
}

View File

@@ -0,0 +1,9 @@
const fs = require('fs');
const path = require('path');
fs.rmSync(path.join(__dirname, '.vercel', 'output'), { recursive: true });
fs.mkdirSync(path.join(__dirname, '.vercel', 'output'));
fs.copyFileSync(
path.join(__dirname, 'output', 'config.json'),
path.join(__dirname, '.vercel', 'output', 'config.json')
);

View File

@@ -0,0 +1 @@
<h1>Vercel</h1>

View File

@@ -0,0 +1,9 @@
{
"version": 3,
"crons": [
{
"path": "/api/cron-job-build-output",
"schedule": "0 0 * * *"
}
]
}

View File

@@ -0,0 +1,5 @@
{
"scripts": {
"build": "node build"
}
}

View File

@@ -0,0 +1,8 @@
{
"crons": [
{
"path": "/api/cron-job",
"schedule": "0 0 * * *"
}
]
}

View File

@@ -0,0 +1,3 @@
export default function (req, res) {
res.send('Hello from cron job!');
}

View File

@@ -1124,6 +1124,32 @@ describe('build', () => {
}
});
it('should merge crons property from build output with vercel.json crons property', async () => {
const cwd = fixture('with-cron-merge');
const output = join(cwd, '.vercel', 'output');
try {
process.chdir(cwd);
const exitCode = await build(client);
expect(exitCode).toBe(0);
const config = await fs.readJSON(join(output, 'config.json'));
expect(config).toHaveProperty('crons', [
{
path: '/api/cron-job',
schedule: '0 0 * * *',
},
{
path: '/api/cron-job-build-output',
schedule: '0 0 * * *',
},
]);
} finally {
process.chdir(originalCwd);
delete process.env.__VERCEL_BUILD_RUNNING;
}
});
describe('should find packages with different main/module/browser keys', function () {
let output: string;

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/fs-detectors",
"version": "3.7.14",
"version": "3.8.0",
"description": "Vercel filesystem detectors",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",

View File

@@ -602,12 +602,11 @@ function validateFunctions({ functions = {} }: Options) {
if (
func.memory !== undefined &&
(func.memory < 128 || func.memory > 3008 || func.memory % 64 !== 0)
(func.memory < 128 || func.memory > 3008)
) {
return {
code: 'invalid_function_memory',
message:
'Functions must have a memory value between 128 and 3008 in steps of 64.',
message: 'Functions must have a memory value between 128 and 3008',
};
}

View File

@@ -473,7 +473,7 @@ describe('Test `detectBuilders`', () => {
});
it('invalid function memory', async () => {
const functions = { 'pages/index.ts': { memory: 200 } };
const functions = { 'pages/index.ts': { memory: 127 } };
const files = ['pages/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
@@ -484,6 +484,17 @@ describe('Test `detectBuilders`', () => {
expect(errors![0].code).toBe('invalid_function_memory');
});
it('should build with function memory not dividable by 64', async () => {
const functions = { 'api/index.ts': { memory: 1000 } };
const files = ['api/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
});
expect(builders![0].use).toBe('@vercel/node');
expect(errors).toBeNull();
});
it('missing runtime version', async () => {
const functions = { 'pages/index.ts': { runtime: 'haha' } };
const files = ['pages/index.ts'];
@@ -1720,7 +1731,7 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
});
it('invalid function memory', async () => {
const functions = { 'pages/index.ts': { memory: 200 } };
const functions = { 'pages/index.ts': { memory: 127 } };
const files = ['pages/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
@@ -1732,6 +1743,18 @@ describe('Test `detectBuilders` with `featHandleMiss=true`', () => {
expect(errors![0].code).toBe('invalid_function_memory');
});
it('should build with function memory not dividable by 64', async () => {
const functions = { 'api/index.ts': { memory: 1000 } };
const files = ['api/index.ts'];
const { builders, errors } = await detectBuilders(files, null, {
functions,
featHandleMiss,
});
expect(builders![0].use).toBe('@vercel/node');
expect(errors).toBeNull();
});
it('missing runtime version', async () => {
const functions = { 'pages/index.ts': { runtime: 'haha' } };
const files = ['pages/index.ts'];

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "3.4.7",
"version": "3.5.1",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",

View File

@@ -88,6 +88,8 @@ import {
PseudoLayerResult,
updateRouteSrc,
validateEntrypoint,
getOperationType,
isApiPage,
} from './utils';
export const version = 2;
@@ -1090,7 +1092,7 @@ export const build: BuildV2 = async ({
handler: '___next_launcher.cjs',
runtime: nodeVersion.runtime,
...lambdaOptions,
operationType: 'SSR',
operationType: 'SSR', // always SSR because we're in legacy mode
shouldAddHelpers: false,
shouldAddSourcemapSupport: false,
supportsMultiPayloads: !!process.env.NEXT_PRIVATE_MULTI_PAYLOAD,
@@ -1126,10 +1128,6 @@ export const build: BuildV2 = async ({
outputDirectory,
appPathRoutesManifest,
});
const isApiPage = (page: string) =>
page
.replace(/\\/g, '/')
.match(/(serverless|server)\/pages\/api(\/|\.js$)/);
const canUsePreviewMode = Object.keys(pages).some(page =>
isApiPage(pages[page].fsPath)
@@ -1598,6 +1596,10 @@ export const build: BuildV2 = async ({
internalPages: [],
});
for (const group of initialApiLambdaGroups) {
group.isApiLambda = true;
}
debug(
JSON.stringify(
{
@@ -1819,6 +1821,10 @@ export const build: BuildV2 = async ({
path.relative(baseDir, entryPath),
'___next_launcher.cjs'
),
operationType: getOperationType({
prerenderManifest,
pageFileName,
}),
runtime: nodeVersion.runtime,
nextVersion,
...lambdaOptions,
@@ -1839,6 +1845,7 @@ export const build: BuildV2 = async ({
path.relative(baseDir, entryPath),
'___next_launcher.cjs'
),
operationType: getOperationType({ pageFileName }), // can only be API or SSR
runtime: nodeVersion.runtime,
nextVersion,
...lambdaOptions,
@@ -2040,6 +2047,11 @@ export const build: BuildV2 = async ({
pageLambdaMap[page] = group.lambdaIdentifier;
}
const operationType = getOperationType({
group,
prerenderManifest,
});
lambdas[group.lambdaIdentifier] =
await createLambdaFromPseudoLayers({
files: {
@@ -2051,6 +2063,7 @@ export const build: BuildV2 = async ({
path.relative(baseDir, entryPath),
'___next_launcher.cjs'
),
operationType,
runtime: nodeVersion.runtime,
nextVersion,
});
@@ -2103,6 +2116,8 @@ export const build: BuildV2 = async ({
...Object.entries(prerenderManifest.fallbackRoutes),
...Object.entries(prerenderManifest.blockingFallbackRoutes),
].forEach(([, { dataRouteRegex, dataRoute }]) => {
if (!dataRoute || !dataRouteRegex) return;
dataRoutes.push({
// Next.js provided data route regex
src: dataRouteRegex.replace(

View File

@@ -43,6 +43,7 @@ import {
getMiddlewareBundle,
getFilesMapFromReasons,
UnwrapPromise,
getOperationType,
} from './utils';
import {
nodeFileTrace,
@@ -748,6 +749,10 @@ export async function serverBuild({
internalPages,
});
for (const group of apiLambdaGroups) {
group.isApiLambda = true;
}
debug(
JSON.stringify(
{
@@ -856,6 +861,8 @@ export async function serverBuild({
}
}
const operationType = getOperationType({ group, prerenderManifest });
const lambda = await createLambdaFromPseudoLayers({
files: {
...launcherFiles,
@@ -869,6 +876,7 @@ export async function serverBuild({
),
'___next_launcher.cjs'
),
operationType,
memory: group.memory,
runtime: nodeVersion.runtime,
maxDuration: group.maxDuration,
@@ -1142,6 +1150,9 @@ export async function serverBuild({
}
const rscHeader = routesManifest.rsc?.header?.toLowerCase() || '__rsc__';
const rscVaryHeader =
routesManifest?.rsc?.varyHeader ||
'RSC, Next-Router-State-Tree, Next-Router-Prefetch';
const completeDynamicRoutes: typeof dynamicRoutes = [];
if (appDir) {
@@ -1414,7 +1425,9 @@ export async function serverBuild({
},
],
dest: path.posix.join('/', entryDirectory, '/index.rsc'),
headers: { vary: rscVaryHeader },
continue: true,
override: true,
},
{
src: `^${path.posix.join(
@@ -1429,7 +1442,9 @@ export async function serverBuild({
},
],
dest: path.posix.join('/', entryDirectory, '/$1.rsc'),
headers: { vary: rscVaryHeader },
continue: true,
override: true,
},
]
: []),

View File

@@ -849,16 +849,18 @@ export type NextPrerenderedRoutes = {
staticRoutes: {
[route: string]: {
initialRevalidate: number | false;
dataRoute: string;
dataRoute: string | null;
srcRoute: string | null;
initialStatus?: number;
initialHeaders?: Record<string, string>;
};
};
blockingFallbackRoutes: {
[route: string]: {
routeRegex: string;
dataRoute: string;
dataRouteRegex: string;
dataRoute: string | null;
dataRouteRegex: string | null;
};
};
@@ -866,16 +868,16 @@ export type NextPrerenderedRoutes = {
[route: string]: {
fallback: string;
routeRegex: string;
dataRoute: string;
dataRouteRegex: string;
dataRoute: string | null;
dataRouteRegex: string | null;
};
};
omittedRoutes: {
[route: string]: {
routeRegex: string;
dataRoute: string;
dataRouteRegex: string;
dataRoute: string | null;
dataRouteRegex: string | null;
};
};
@@ -1068,6 +1070,30 @@ export async function getPrerenderManifest(
previewModeId: string;
};
notFoundRoutes?: string[];
}
| {
version: 4;
routes: {
[route: string]: {
initialRevalidateSeconds: number | false;
srcRoute: string | null;
dataRoute: string | null;
initialStatus?: number;
initialHeaders?: Record<string, string>;
};
};
dynamicRoutes: {
[route: string]: {
routeRegex: string;
fallback: string | false;
dataRoute: string | null;
dataRouteRegex: string | null;
};
};
preview: {
previewModeId: string;
};
notFoundRoutes?: string[];
} = JSON.parse(await fs.readFile(pathPrerenderManifest, 'utf8'));
switch (manifest.version) {
@@ -1122,7 +1148,8 @@ export async function getPrerenderManifest(
return ret;
}
case 2:
case 3: {
case 3:
case 4: {
const routes = Object.keys(manifest.routes);
const lazyRoutes = Object.keys(manifest.dynamicRoutes);
@@ -1143,6 +1170,15 @@ export async function getPrerenderManifest(
routes.forEach(route => {
const { initialRevalidateSeconds, dataRoute, srcRoute } =
manifest.routes[route];
let initialStatus: undefined | number;
let initialHeaders: undefined | Record<string, string>;
if (manifest.version === 4) {
initialStatus = manifest.routes[route].initialStatus;
initialHeaders = manifest.routes[route].initialHeaders;
}
ret.staticRoutes[route] = {
initialRevalidate:
initialRevalidateSeconds === false
@@ -1150,6 +1186,8 @@ export async function getPrerenderManifest(
: Math.max(1, initialRevalidateSeconds),
dataRoute,
srcRoute,
initialStatus,
initialHeaders,
};
});
@@ -1313,6 +1351,7 @@ export type LambdaGroup = {
maxDuration?: number;
isStreaming?: boolean;
isPrerenders?: boolean;
isApiLambda: boolean;
pseudoLayer: PseudoLayer;
pseudoLayerBytes: number;
pseudoLayerUncompressedBytes: number;
@@ -1419,6 +1458,7 @@ export async function getPageLambdaGroups({
pages: [page],
...opts,
isPrerenders: isPrerenderRoute,
isApiLambda: !!isApiPage(page),
pseudoLayerBytes: initialPseudoLayer.pseudoLayerBytes,
pseudoLayerUncompressedBytes: initialPseudoLayerUncompressed,
pseudoLayer: Object.assign({}, initialPseudoLayer.pseudoLayer),
@@ -1650,7 +1690,7 @@ export const onPrerenderRouteInitial = (
const { initialRevalidate, srcRoute, dataRoute } = pr;
const route = srcRoute || routeKey;
const isAppPathRoute = appDir && dataRoute?.endsWith('.rsc');
const isAppPathRoute = appDir && (!dataRoute || dataRoute?.endsWith('.rsc'));
const routeNoLocale = routesManifest?.i18n
? normalizeLocalePath(routeKey, routesManifest.i18n.locales).pathname
@@ -1808,7 +1848,9 @@ export const onPrerenderRoute =
let initialRevalidate: false | number;
let srcRoute: string | null;
let dataRoute: string;
let dataRoute: string | null;
let initialStatus: number | undefined;
let initialHeaders: Record<string, string> | undefined;
if (isFallback || isBlocking) {
const pr = isFallback
@@ -1831,44 +1873,60 @@ export const onPrerenderRoute =
dataRoute = prerenderManifest.omittedRoutes[routeKey].dataRoute;
} else {
const pr = prerenderManifest.staticRoutes[routeKey];
({ initialRevalidate, srcRoute, dataRoute } = pr);
({
initialRevalidate,
srcRoute,
dataRoute,
initialHeaders,
initialStatus,
} = pr);
}
let isAppPathRoute = false;
// TODO: leverage manifest to determine app paths more accurately
if (appDir && srcRoute && dataRoute.endsWith('.rsc')) {
if (appDir && srcRoute && (!dataRoute || dataRoute?.endsWith('.rsc'))) {
isAppPathRoute = true;
}
const isOmittedOrNotFound = isOmitted || isNotFound;
const htmlFsRef =
isBlocking || (isNotFound && !static404Page)
? // Blocking pages do not have an HTML fallback
null
: new FileFsRef({
fsPath: path.join(
isAppPathRoute && !isOmittedOrNotFound && appDir
? appDir
: pagesDir,
isFallback
? // Fallback pages have a special file.
addLocaleOrDefault(
prerenderManifest.fallbackRoutes[routeKey].fallback,
routesManifest,
locale
)
: // Otherwise, the route itself should exist as a static HTML
// file.
`${
isOmittedOrNotFound
? addLocaleOrDefault('/404', routesManifest, locale)
: routeFileNoExt
}.html`
),
});
let htmlFsRef: FileFsRef | null;
if (appDir && !dataRoute && isAppPathRoute && !(isBlocking || isFallback)) {
const contentType = initialHeaders?.['content-type'];
htmlFsRef = new FileFsRef({
fsPath: path.join(appDir, `${routeFileNoExt}.body`),
contentType: contentType || 'text/html;charset=utf-8',
});
} else {
htmlFsRef =
isBlocking || (isNotFound && !static404Page)
? // Blocking pages do not have an HTML fallback
null
: new FileFsRef({
fsPath: path.join(
isAppPathRoute && !isOmittedOrNotFound && appDir
? appDir
: pagesDir,
isFallback
? // Fallback pages have a special file.
addLocaleOrDefault(
prerenderManifest.fallbackRoutes[routeKey].fallback,
routesManifest,
locale
)
: // Otherwise, the route itself should exist as a static HTML
// file.
`${
isOmittedOrNotFound
? addLocaleOrDefault('/404', routesManifest, locale)
: routeFileNoExt
}.html`
),
});
}
const jsonFsRef =
// JSON data does not exist for fallback or blocking pages
isFallback || isBlocking || (isNotFound && !static404Page)
isFallback || isBlocking || (isNotFound && !static404Page) || !dataRoute
? null
: new FileFsRef({
fsPath: path.join(
@@ -1906,16 +1964,20 @@ export const onPrerenderRoute =
);
let lambda: undefined | Lambda;
let outputPathData = path.posix.join(entryDirectory, dataRoute);
let outputPathData: null | string = null;
if (nonDynamicSsg || isFallback || isOmitted) {
outputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
// ensure we escape "$" correctly while replacing as "$" is a special
// character, we need to do double escaping as first is for the initial
// replace on the routeFile and then the second on the outputPath
`${routeFileNoExt.replace(/\$/g, '$$$$')}.json`
);
if (dataRoute) {
outputPathData = path.posix.join(entryDirectory, dataRoute);
if (nonDynamicSsg || isFallback || isOmitted) {
outputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
// ensure we escape "$" correctly while replacing as "$" is a special
// character, we need to do double escaping as first is for the initial
// replace on the routeFile and then the second on the outputPath
`${routeFileNoExt.replace(/\$/g, '$$$$')}.json`
);
}
}
if (isSharedLambdas) {
@@ -1952,7 +2014,7 @@ export const onPrerenderRoute =
if (htmlFsRef == null || jsonFsRef == null) {
throw new NowBuildError({
code: 'NEXT_HTMLFSREF_JSONFSREF',
message: 'invariant: htmlFsRef != null && jsonFsRef != null',
message: `invariant: htmlFsRef != null && jsonFsRef != null ${routeFileNoExt}`,
});
}
@@ -1964,7 +2026,10 @@ export const onPrerenderRoute =
) {
htmlFsRef.contentType = htmlContentType;
prerenders[outputPathPage] = htmlFsRef;
prerenders[outputPathData] = jsonFsRef;
if (outputPathData) {
prerenders[outputPathData] = jsonFsRef;
}
}
}
const isNotFoundPreview =
@@ -2026,11 +2091,10 @@ export const onPrerenderRoute =
allowQuery = [];
}
}
const rscVaryHeader =
routesManifest?.rsc?.varyHeader ||
'__rsc__, __next_router_state_tree__, __next_router_prefetch__';
const rscContentTypeHeader =
routesManifest?.rsc?.contentTypeHeader || 'application/octet-stream';
const rscEnabled = !!routesManifest?.rsc;
const rscVaryHeader = routesManifest?.rsc?.varyHeader || 'RSC, Next-Router-State-Tree, Next-Router-Prefetch';
const rscContentTypeHeader = routesManifest?.rsc?.contentTypeHeader || 'text/x-component';
prerenders[outputPathPage] = new Prerender({
expiration: initialRevalidate,
@@ -2039,27 +2103,8 @@ export const onPrerenderRoute =
fallback: htmlFsRef,
group: prerenderGroup,
bypassToken: prerenderManifest.bypassToken,
...(isNotFound
? {
initialStatus: 404,
}
: {}),
...(isAppPathRoute
? {
initialHeaders: {
vary: rscVaryHeader,
},
}
: {}),
});
prerenders[outputPathData] = new Prerender({
expiration: initialRevalidate,
lambda,
allowQuery,
fallback: jsonFsRef,
group: prerenderGroup,
bypassToken: prerenderManifest.bypassToken,
initialStatus,
initialHeaders,
...(isNotFound
? {
@@ -2067,16 +2112,42 @@ export const onPrerenderRoute =
}
: {}),
...(isAppPathRoute
...(rscEnabled
? {
initialHeaders: {
'content-type': rscContentTypeHeader,
...initialHeaders,
vary: rscVaryHeader,
},
}
: {}),
});
if (outputPathData) {
prerenders[outputPathData] = new Prerender({
expiration: initialRevalidate,
lambda,
allowQuery,
fallback: jsonFsRef,
group: prerenderGroup,
bypassToken: prerenderManifest.bypassToken,
...(isNotFound
? {
initialStatus: 404,
}
: {}),
...(rscEnabled
? {
initialHeaders: {
'content-type': rscContentTypeHeader,
vary: rscVaryHeader,
},
}
: {}),
});
}
++prerenderGroup;
if (routesManifest?.i18n && isBlocking) {
@@ -2090,29 +2161,30 @@ export const onPrerenderRoute =
path.posix.join(entryDirectory, localeRouteFileNoExt),
isServerMode
);
const localeOutputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
`${localeRouteFileNoExt}${
localeRouteFileNoExt !== origRouteFileNoExt &&
origRouteFileNoExt === '/index'
? '/index'
: ''
}.json`
);
const origPrerenderPage = prerenders[outputPathPage];
const origPrerenderData = prerenders[outputPathData];
prerenders[localeOutputPathPage] = {
...origPrerenderPage,
group: prerenderGroup,
} as Prerender;
prerenders[localeOutputPathData] = {
...origPrerenderData,
group: prerenderGroup,
} as Prerender;
if (outputPathData) {
const localeOutputPathData = outputPathData.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
`${localeRouteFileNoExt}${
localeRouteFileNoExt !== origRouteFileNoExt &&
origRouteFileNoExt === '/index'
? '/index'
: ''
}.json`
);
const origPrerenderData = prerenders[outputPathData];
prerenders[localeOutputPathData] = {
...origPrerenderData,
group: prerenderGroup,
} as Prerender;
}
++prerenderGroup;
}
}
@@ -2489,9 +2561,13 @@ export async function getMiddlewareBundle({
// app/index/page -> index/index
if (shortPath.startsWith('pages/')) {
shortPath = shortPath.replace(/^pages\//, '');
} else if (shortPath.startsWith('app/') && shortPath.endsWith('/page')) {
} else if (
shortPath.startsWith('app/') &&
(shortPath.endsWith('/page') || shortPath.endsWith('/route'))
) {
shortPath =
shortPath.replace(/^app\//, '').replace(/(^|\/)page$/, '') || 'index';
shortPath.replace(/^app\//, '').replace(/(^|\/)(page|route)$/, '') ||
'index';
}
if (routesManifest?.basePath) {
@@ -2677,3 +2753,49 @@ function transformSourceMap(
return { ...sourcemap, sources };
}
interface LambdaGroupTypeInterface {
isApiLambda: boolean;
isPrerenders?: boolean;
}
export function getOperationType({
group,
prerenderManifest,
pageFileName,
}: {
group?: LambdaGroupTypeInterface;
prerenderManifest?: NextPrerenderedRoutes;
pageFileName?: string;
}) {
if (group?.isApiLambda || isApiPage(pageFileName)) {
return 'API';
}
if (group?.isPrerenders) {
return 'ISR';
}
if (pageFileName && prerenderManifest) {
const { blockingFallbackRoutes = {}, fallbackRoutes = {} } =
prerenderManifest;
if (
pageFileName in blockingFallbackRoutes ||
pageFileName in fallbackRoutes
) {
return 'ISR';
}
}
return 'SSR';
}
export function isApiPage(page: string | undefined) {
if (!page) {
return false;
}
return page
.replace(/\\/g, '/')
.match(/(serverless|server)\/pages\/api(\/|\.js$)/);
}

View File

@@ -0,0 +1,21 @@
import Link from 'next/link';
const paths = ['/', '/shop', '/product', '/who-we-are', '/about', '/contact'];
export default function Page({ params }) {
return (
<>
<p>variant: {params.variant}</p>
<p>slug: {params.rest?.join('/')}</p>
<ul>
{paths.map(path => {
return (
<li key={path}>
<Link href={path}>to {path}</Link>
</li>
);
})}
</ul>
</>
);
}

View File

@@ -0,0 +1,10 @@
export default function Root({ children }) {
return (
<html className="this-is-the-document-html">
<head>
<title>{`hello world`}</title>
</head>
<body className="this-is-the-document-body">{children}</body>
</html>
);
}

View File

@@ -0,0 +1,12 @@
/* eslint-env jest */
const path = require('path');
const { deployAndTest } = require('../../utils');
const ctx = {};
describe(`${__dirname.split(path.sep).pop()}`, () => {
it('should deploy and pass probe checks', async () => {
const info = await deployAndTest(__dirname);
Object.assign(ctx, info);
});
});

View File

@@ -0,0 +1,11 @@
import { NextResponse } from 'next/server';
export function middleware(request) {
request.nextUrl.pathname = `/no-variant${request.nextUrl.pathname}`;
return NextResponse.rewrite(request.nextUrl);
}
// See "Matching Paths" below to learn more
export const config = {
matcher: ['/', '/shop', '/product', '/who-we-are', '/about', '/contact'],
};

View File

@@ -0,0 +1,14 @@
module.exports = {
experimental: {
appDir: true,
runtime: 'nodejs',
},
rewrites: async () => {
return [
{
source: '/rewritten-to-dashboard',
destination: '/dashboard',
},
];
},
};

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"next": "canary",
"react": "experimental",
"react-dom": "experimental"
}
}

View File

@@ -0,0 +1,3 @@
export default function handler(req, res) {
return res.json({ hello: 'world' });
}

View File

@@ -0,0 +1,7 @@
export default function Page(props) {
return (
<>
<p>hello from pages/blog/[slug]</p>
</>
);
}

View File

@@ -0,0 +1 @@
hello world

View File

@@ -0,0 +1,82 @@
{
"builds": [
{
"src": "package.json",
"use": "@vercel/next"
}
],
"probes": [
{
"path": "/",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"mustContain": "no-variant",
"mustNotContain": "index"
},
{
"path": "/",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
},
"mustContain": ":{",
"mustNotContain": "<html"
},
{
"path": "/shop",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"mustContain": "no-variant",
"mustNotContain": "shop.rsc"
},
{
"path": "/shop",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
},
"mustContain": ":{",
"mustNotContain": "<html"
},
{
"path": "/no-variant/shop",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"mustContain": "no-variant",
"mustNotContain": "shop.rsc"
},
{
"path": "/no-variant/shop",
"status": 200,
"fetchOptions": {
"redirect": "manual"
},
"responseHeaders": {
"vary": "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
},
"headers": {
"RSC": "1"
},
"mustContain": ":{",
"mustNotContain": "<html"
}
]
}

View File

@@ -134,41 +134,70 @@ it('should build using server build', async () => {
expect(output['index'].allowQuery).toBe(undefined);
expect(output['index'].memory).toBe(512);
expect(output['index'].maxDuration).toBe(5);
expect(output['index'].operationType).toBe('SSR');
expect(output['another'].type).toBe('Lambda');
expect(output['another'].memory).toBe(512);
expect(output['another'].maxDuration).toBe(5);
expect(output['another'].allowQuery).toBe(undefined);
expect(output['another'].operationType).toBe('SSR');
expect(output['dynamic/[slug]'].type).toBe('Lambda');
expect(output['dynamic/[slug]'].memory).toBe(undefined);
expect(output['dynamic/[slug]'].maxDuration).toBe(5);
expect(output['dynamic/[slug]'].operationType).toBe('SSR');
expect(output['fallback/[slug]'].type).toBe('Prerender');
expect(output['fallback/[slug]'].allowQuery).toEqual(['slug']);
expect(output['fallback/[slug]'].lambda.operationType).toBe('ISR');
expect(output['_next/data/testing-build-id/fallback/[slug].json'].type).toBe(
'Prerender'
);
expect(
output['_next/data/testing-build-id/fallback/[slug].json'].allowQuery
).toEqual(['slug']);
expect(
output['_next/data/testing-build-id/fallback/[slug].json'].lambda
.operationType
).toBe('ISR');
expect(output['fallback/first'].type).toBe('Prerender');
expect(output['fallback/first'].allowQuery).toEqual([]);
expect(output['fallback/first'].lambda.operationType).toBe('ISR');
expect(output['_next/data/testing-build-id/fallback/first.json'].type).toBe(
'Prerender'
);
expect(
output['_next/data/testing-build-id/fallback/first.json'].allowQuery
).toEqual([]);
expect(
output['_next/data/testing-build-id/fallback/first.json'].lambda
.operationType
).toBe('ISR');
expect(output['api'].type).toBe('Lambda');
expect(output['api'].allowQuery).toBe(undefined);
expect(output['api'].memory).toBe(128);
expect(output['api'].maxDuration).toBe(5);
expect(output['api'].operationType).toBe('API');
expect(output['api/another'].type).toBe('Lambda');
expect(output['api/another'].allowQuery).toBe(undefined);
expect(output['api/another'].operationType).toBe('API');
expect(output['api/blog/[slug]'].type).toBe('Lambda');
expect(output['api/blog/[slug]'].allowQuery).toBe(undefined);
expect(output['api/blog/[slug]'].operationType).toBe('API');
expect(output['static'].type).toBe('FileFsRef');
expect(output['static'].allowQuery).toBe(undefined);
expect(output['static'].operationType).toBe(undefined);
expect(output['ssg'].type).toBe('Prerender');
expect(output['ssg'].allowQuery).toEqual([]);
expect(output['ssg'].lambda.operationType).toBe('ISR');
expect(output['index'] === output['another']).toBe(true);
expect(output['dynamic/[slug]'] !== output['fallback/[slug]'].lambda).toBe(

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/remix",
"version": "1.3.3",
"version": "1.3.5",
"license": "MIT",
"main": "./dist/index.js",
"homepage": "https://vercel.com/docs",
@@ -11,8 +11,9 @@
},
"scripts": {
"build": "node build.js",
"test-e2e": "pnpm test test/integration.test.ts",
"test": "jest --env node --verbose --bail --runInBand"
"test": "jest --env node --verbose --bail --runInBand",
"test-unit": "pnpm test test/unit.*test.*",
"test-e2e": "pnpm test test/integration.test.ts"
},
"files": [
"dist",

View File

@@ -4,7 +4,11 @@ import {
Headers as NodeHeaders,
Request as NodeRequest,
writeReadableStreamToWritable,
installGlobals,
} from '@remix-run/node';
installGlobals();
import build from './index.js';
const handleRequest = createRemixRequestHandler(build, process.env.NODE_ENV);

View File

@@ -1,7 +1,6 @@
import { Project } from 'ts-morph';
import { promises as fs } from 'fs';
import { basename, dirname, extname, join, relative, sep } from 'path';
import { pathToRegexp, Key } from 'path-to-regexp';
import {
debug,
download,
@@ -30,7 +29,12 @@ import type {
BuildResultV2Typical,
} from '@vercel/build-utils';
import type { ConfigRoute } from '@remix-run/dev/dist/config/routes';
import { findConfig } from './utils';
import {
findConfig,
getPathFromRoute,
getRegExpFromPath,
isLayoutRoute,
} from './utils';
const _require: typeof require = eval('require');
@@ -173,12 +177,13 @@ module.exports = config;`;
}
}
const { serverBuildPath, routes: remixRoutes } = remixConfig;
const { serverBuildPath } = remixConfig;
const remixRoutes = Object.values(remixConfig.routes);
// Figure out which pages should be edge functions
const edgePages = new Set<ConfigRoute>();
const project = new Project();
for (const route of Object.values(remixRoutes)) {
for (const route of remixRoutes) {
const routePath = join(remixConfig.appDirectory, route.file);
const staticConfig = getConfig(project, routePath);
const isEdge =
@@ -228,27 +233,11 @@ module.exports = config;`;
},
];
for (const route of Object.values(remixRoutes)) {
for (const route of remixRoutes) {
// Layout routes don't get a function / route added
const isLayoutRoute = Object.values(remixRoutes).some(
r => r.parentId === route.id
);
if (isLayoutRoute) continue;
// Build up the full request path
let currentRoute: ConfigRoute | undefined = route;
const pathParts: string[] = [];
do {
if (currentRoute.index) pathParts.push('index');
if (currentRoute.path) pathParts.push(currentRoute.path);
if (currentRoute.parentId) {
currentRoute = remixRoutes[currentRoute.parentId];
} else {
currentRoute = undefined;
}
} while (currentRoute);
const path = join(...pathParts.reverse());
if (isLayoutRoute(route.id, remixRoutes)) continue;
const path = getPathFromRoute(route, remixConfig.routes);
const isEdge = edgePages.has(route);
const fn =
isEdge && edgeFunction
@@ -264,13 +253,8 @@ module.exports = config;`;
output[path] = fn;
// If this is a dynamic route then add a Vercel route
const keys: Key[] = [];
// Replace "/*" at the end to handle "splat routes"
const splatPath = '/:params+';
const rePath =
path === '*' ? splatPath : `/${path.replace(/\/\*$/, splatPath)}`;
const re = pathToRegexp(rePath, keys);
if (keys.length > 0) {
const re = getRegExpFromPath(path);
if (re) {
routes.push({
src: re.source,
dest: path,

View File

@@ -1,5 +1,10 @@
import { existsSync } from 'fs';
import { join } from 'path';
import { existsSync } from 'fs';
import { pathToRegexp, Key } from 'path-to-regexp';
import type {
ConfigRoute,
RouteManifest,
} from '@remix-run/dev/dist/config/routes';
const configExts = ['.js', '.cjs', '.mjs'];
@@ -12,3 +17,39 @@ export function findConfig(dir: string, basename: string): string | undefined {
return undefined;
}
export function isLayoutRoute(
routeId: string,
routes: Pick<ConfigRoute, 'id' | 'parentId'>[]
): boolean {
return routes.some(r => r.parentId === routeId);
}
export function getPathFromRoute(
route: ConfigRoute,
routes: RouteManifest
): string {
let currentRoute: ConfigRoute | undefined = route;
const pathParts: string[] = [];
do {
if (currentRoute.index) pathParts.push('index');
if (currentRoute.path) pathParts.push(currentRoute.path);
if (currentRoute.parentId) {
currentRoute = routes[currentRoute.parentId];
} else {
currentRoute = undefined;
}
} while (currentRoute);
const path = pathParts.reverse().join('/');
return path;
}
export function getRegExpFromPath(path: string): RegExp | false {
const keys: Key[] = [];
// Replace "/*" at the end to handle "splat routes"
const splatPath = '/:params+';
const rePath =
path === '*' ? splatPath : `/${path.replace(/\/\*$/, splatPath)}`;
const re = pathToRegexp(rePath, keys);
return keys.length > 0 ? re : false;
}

View File

@@ -0,0 +1,13 @@
import { json } from '@remix-run/server-runtime';
import { useLoaderData } from '@remix-run/react';
import type { LoaderArgs } from '@remix-run/server-runtime';
export const loader = ({ request }: LoaderArgs) => {
const instanceOfRequest = request instanceof Request;
return json({ instanceOfRequest });
};
export default function InstanceOf() {
const data = useLoaderData<typeof loader>();
return <div>{`InstanceOfRequest: ${data.instanceOfRequest}`}</div>;
}

View File

@@ -16,6 +16,7 @@
{ "path": "/nested", "mustContain": "Nested index page" },
{ "path": "/nested/another", "mustContain": "Nested another page" },
{ "path": "/nested/index", "mustContain": "Not Found" },
{ "path": "/asdf", "mustContain": "Not Found" }
{ "path": "/asdf", "mustContain": "Not Found" },
{ "path": "/instanceof", "mustContain": "InstanceOfRequest: true" }
]
}

View File

@@ -0,0 +1,17 @@
import { join } from 'path';
import { findConfig } from '../src/utils';
const fixture = (name: string) => join(__dirname, 'fixtures', name);
describe('findConfig()', () => {
it.each([
{ name: '01-remix-basics', config: 'remix.config.js' },
{ name: '02-remix-basics-mjs', config: 'remix.config.mjs' },
{ name: '03-with-pnpm', config: 'remix.config.js' },
{ name: '04-with-npm9-linked', config: 'remix.config.js' },
])('should find `$config` from "$name"', ({ name, config }) => {
const dir = fixture(name);
const resolved = findConfig(dir, 'remix.config');
expect(resolved).toEqual(join(dir, config));
});
});

View File

@@ -0,0 +1,73 @@
import { getPathFromRoute } from '../src/utils';
import type { RouteManifest } from '@remix-run/dev/dist/config/routes';
describe('getPathFromRoute()', () => {
const routes: RouteManifest = {
root: { path: '', id: 'root', file: 'root.tsx' },
'routes/$foo.$bar.$baz': {
path: ':foo/:bar/:baz',
id: 'routes/$foo.$bar.$baz',
parentId: 'root',
file: 'routes/$foo.$bar.$baz.tsx',
},
'routes/api.hello': {
path: 'api/hello',
id: 'routes/api.hello',
parentId: 'root',
file: 'routes/api.hello.tsx',
},
'routes/projects': {
path: 'projects',
id: 'routes/projects',
parentId: 'root',
file: 'routes/projects.tsx',
},
'routes/projects/index': {
path: undefined,
index: true,
id: 'routes/projects/indexx',
parentId: 'routes/projects',
file: 'routes/projects/indexx.tsx',
},
'routes/projects/$': {
path: '*',
id: 'routes/projects/$',
parentId: 'routes/projects',
file: 'routes/projects/$.tsx',
},
'routes/index': {
path: undefined,
index: true,
id: 'routes/index',
parentId: 'root',
file: 'routes/index.tsx',
},
'routes/node': {
path: 'node',
id: 'routes/node',
parentId: 'root',
file: 'routes/node.tsx',
},
'routes/$': {
path: '*',
id: 'routes/$',
parentId: 'root',
file: 'routes/$.tsx',
},
};
it.each([
{ id: 'root', expected: '' },
{ id: 'routes/index', expected: 'index' },
{ id: 'routes/api.hello', expected: 'api/hello' },
{ id: 'routes/projects', expected: 'projects' },
{ id: 'routes/projects/index', expected: 'projects/index' },
{ id: 'routes/projects/$', expected: 'projects/*' },
{ id: 'routes/$foo.$bar.$baz', expected: ':foo/:bar/:baz' },
{ id: 'routes/node', expected: 'node' },
{ id: 'routes/$', expected: '*' },
])('should return `$expected` for "$id" route', ({ id, expected }) => {
const route = routes[id];
expect(getPathFromRoute(route, routes)).toEqual(expected);
});
});

View File

@@ -0,0 +1,124 @@
import { getRegExpFromPath } from '../src/utils';
describe('getRegExpFromPath()', () => {
describe('paths without parameters', () => {
it.each([{ path: 'index' }, { path: 'api/hello' }, { path: 'projects' }])(
'should return `false` for "$path"',
({ path }) => {
expect(getRegExpFromPath(path)).toEqual(false);
}
);
});
describe.each([
{
path: '*',
urls: [
{
url: '/',
expected: false,
},
{
url: '/foo',
expected: true,
},
{
url: '/projects/foo',
expected: true,
},
{
url: '/projects/another',
expected: true,
},
{
url: '/to/infinity/and/beyond',
expected: true,
},
],
},
{
path: 'projects/*',
urls: [
{
url: '/',
expected: false,
},
{
url: '/foo',
expected: false,
},
{
url: '/projects/foo',
expected: true,
},
{
url: '/projects/another',
expected: true,
},
],
},
{
path: ':foo',
urls: [
{
url: '/',
expected: false,
},
{
url: '/foo',
expected: true,
},
{
url: '/projects/foo',
expected: false,
},
{
url: '/projects/another',
expected: false,
},
],
},
{
path: 'blog/:id/edit',
urls: [
{
url: '/',
expected: false,
},
{
url: '/foo',
expected: false,
},
{
url: '/blog/123/edit',
expected: true,
},
{
url: '/blog/456/edit',
expected: true,
},
{
url: '/blog/123/456/edit',
expected: false,
},
{
url: '/blog/123/another',
expected: false,
},
],
},
])('with path "$path"', ({ path, urls }) => {
const re = getRegExpFromPath(path) as RegExp;
it('should return RegExp', () => {
expect(re).toBeInstanceOf(RegExp);
});
it.each(urls)(
'should match URL "$url" - $expected',
({ url, expected }) => {
expect(re.test(url)).toEqual(expected);
}
);
});
});

View File

@@ -0,0 +1,21 @@
import { isLayoutRoute } from '../src/utils';
describe('isLayoutRoute()', () => {
const routes = [
{ id: 'root' },
{ id: 'routes/auth', parentId: 'root' },
{ id: 'routes/login', parentId: 'routes/auth' },
{ id: 'routes/logout', parentId: 'routes/auth' },
{ id: 'routes/index', parentId: 'root' },
];
it.each([
{ id: 'root', expected: true },
{ id: 'routes/auth', expected: true },
{ id: 'routes/index', expected: false },
{ id: 'routes/login', expected: false },
{ id: 'routes/logout', expected: false },
])('should return `$expected` for "$id" route', ({ id, expected }) => {
expect(isLayoutRoute(id, routes)).toEqual(expected);
});
});

View File

@@ -1,7 +1,7 @@
{
"name": "@vercel/ruby",
"author": "Nathan Cahill <nathan@nathancahill.com>",
"version": "1.3.65",
"version": "1.3.66",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/ruby",

View File

@@ -1,5 +1,8 @@
{
"version": 2,
"builds": [{ "src": "index.rb", "use": "@vercel/ruby" }],
"probes": [{ "path": "/", "mustContain": "gem:RANDOMNESS_PLACEHOLDER" }]
"probes": [
{ "path": "/", "mustContain": "gem:RANDOMNESS_PLACEHOLDER" },
{ "path": "/", "method": "HEAD", "status": 200 }
]
}

View File

@@ -73,7 +73,7 @@ def webrick_handler(httpMethod, path, body, headers)
{
:statusCode => res.code.to_i,
:headers => res_headers,
:body => res.body,
:body => res.body.nil? ? "" : res.body,
}
end

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/static-build",
"version": "1.3.9",
"version": "1.3.10",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/build-step",
@@ -44,7 +44,7 @@
"@types/semver": "7.3.13",
"@vercel/build-utils": "6.3.0",
"@vercel/frameworks": "1.3.1",
"@vercel/fs-detectors": "3.7.14",
"@vercel/fs-detectors": "3.8.0",
"@vercel/ncc": "0.24.0",
"@vercel/routing-utils": "2.1.9",
"@vercel/static-config": "2.0.13",

12
pnpm-lock.yaml generated
View File

@@ -208,19 +208,19 @@ importers:
'@vercel/client': 12.4.0
'@vercel/error-utils': 1.0.8
'@vercel/frameworks': 1.3.1
'@vercel/fs-detectors': 3.7.14
'@vercel/fs-detectors': 3.8.0
'@vercel/fun': 1.0.4
'@vercel/go': 2.3.7
'@vercel/hydrogen': 0.0.53
'@vercel/ncc': 0.24.0
'@vercel/next': 3.4.7
'@vercel/next': 3.5.1
'@vercel/node': 2.9.6
'@vercel/python': 3.1.49
'@vercel/redwood': 1.1.5
'@vercel/remix': 1.3.3
'@vercel/remix': 1.3.5
'@vercel/routing-utils': 2.1.9
'@vercel/ruby': 1.3.65
'@vercel/static-build': 1.3.9
'@vercel/ruby': 1.3.66
'@vercel/static-build': 1.3.10
'@zeit/source-map-support': 0.6.2
ajv: 6.12.2
alpha-sort: 2.0.1
@@ -989,7 +989,7 @@ importers:
'@types/semver': 7.3.13
'@vercel/build-utils': 6.3.0
'@vercel/frameworks': 1.3.1
'@vercel/fs-detectors': 3.7.14
'@vercel/fs-detectors': 3.8.0
'@vercel/gatsby-plugin-vercel-analytics': 1.0.7
'@vercel/gatsby-plugin-vercel-builder': 1.1.7
'@vercel/ncc': 0.24.0

View File

@@ -384,7 +384,7 @@ async function fetchDeploymentUrl(url, opts) {
for (let i = 0; i < 50; i += 1) {
const resp = await fetch(url, opts);
const text = await resp.text();
if (text && !text.includes('Join Free')) {
if (typeof text !== 'undefined' && !text.includes('Join Free')) {
return { resp, text };
}