[next] Re-enable shared lambdas by default (#4757)

This re-enables the shared lambdas optimization by default and also adds additional tests to ensure that when a `now.json` or `vercel.json` contains either `routes` or `functions` configs the shared lambda optimization is disabled. 

Additional test deployments done:

- minimal `now.json` with `builds` config [deploy](https://shared-lambdas-tests-d646fsqju.vercel.app)
- `now.json` with `functions` config [deploy](https://shared-lambdas-tests-ahnuosp4s.vercel.app)
- `now.json` with `routes` config [deploy](https://shared-lambdas-tests-gulam3jda.vercel.app)
- minimal `vercel.json` with `builds` config [deploy](https://shared-lambdas-tests-7ic7wzirs.vercel.app)
- `vercel.json` with `functions` config [deploy](https://shared-lambdas-tests-7ic7wzirs.vercel.app)
- `vercel.json` with `routes` config [deploy](https://shared-lambdas-tests-rja2391tq.vercel.app)
This commit is contained in:
JJ Kasper
2020-07-01 14:44:09 -05:00
committed by GitHub
parent b29db2fd1d
commit e0ec6c792b
46 changed files with 183 additions and 1162 deletions

View File

@@ -71,7 +71,7 @@ import {
syncEnvVars,
validateEntrypoint,
} from './utils';
// import findUp from 'find-up';
import findUp from 'find-up';
import { Sema } from 'async-sema';
interface BuildParamsMeta {
@@ -231,47 +231,39 @@ export const build = async ({
});
}
// let nowJsonPath = Object.keys(files).find(file => {
// return file.endsWith('now.json') || file.endsWith('vercel.json')
// })
const nowJsonPath = await findUp(['now.json', 'vercel.json'], {
cwd: path.join(workPath, path.dirname(entrypoint)),
});
// if (nowJsonPath) nowJsonPath = files[nowJsonPath].fsPath
let hasLegacyRoutes = false;
const hasFunctionsConfig = !!config.functions;
// if (!nowJsonPath) {
// nowJsonPath = await findUp(['now.json', 'vercel.json'], {
// cwd: path.join(workPath, path.dirname(entrypoint))
// })
// }
if (nowJsonPath) {
const nowJsonData = JSON.parse(await readFile(nowJsonPath, 'utf8'));
// let hasLegacyRoutes = false;
// const hasFunctionsConfig = !!config.functions;
if (Array.isArray(nowJsonData.routes) && nowJsonData.routes.length > 0) {
hasLegacyRoutes = true;
console.warn(
`WARNING: your application is being opted out of @vercel/next's optimized lambdas mode due to legacy routes in ${path.basename(
nowJsonPath
)}. http://err.sh/vercel/vercel/next-legacy-routes-optimized-lambdas`
);
}
}
// if (nowJsonPath) {
// const nowJsonData = JSON.parse(await readFile(nowJsonPath, 'utf8'));
// if (Array.isArray(nowJsonData.routes) && nowJsonData.routes.length > 0) {
// hasLegacyRoutes = true;
// console.warn(
// `WARNING: your application is being opted out of @vercel/next's optimized lambdas mode due to legacy routes in ${path.basename(
// nowJsonPath
// )}. http://err.sh/vercel/vercel/next-legacy-routes-optimized-lambdas`
// );
// }
// }
// if (hasFunctionsConfig) {
// console.warn(
// `WARNING: Your application is being opted out of "@vercel/next" optimized lambdas mode due to \`functions\` config.\nMore info: http://err.sh/vercel/vercel/next-functions-config-optimized-lambdas`
// );
// }
if (hasFunctionsConfig) {
console.warn(
`WARNING: Your application is being opted out of "@vercel/next" optimized lambdas mode due to \`functions\` config.\nMore info: http://err.sh/vercel/vercel/next-functions-config-optimized-lambdas`
);
}
// default to true but still allow opting out with the config
const isSharedLambdas = !!config.sharedLambdas;
// !hasLegacyRoutes &&
// !hasFunctionsConfig &&
// typeof config.sharedLambdas === 'undefined'
// ? true
// : !!config.sharedLambdas;
const isSharedLambdas =
!hasLegacyRoutes &&
!hasFunctionsConfig &&
typeof config.sharedLambdas === 'undefined'
? true
: !!config.sharedLambdas;
if (meta.isDev) {
let childProcess: ChildProcess | undefined;

View File

@@ -1,12 +1,10 @@
{
"version": 2,
"uploadNowJson": true,
"builds": [
{
"src": "package.json",
"use": "@vercel/next",
"config": {
"sharedLambdas": true
}
"use": "@vercel/next"
}
],
"probes": [
@@ -29,6 +27,12 @@
{
"path": "/another/invite/hello",
"mustContain": "hello from /[teamSlug]/[project]/[id]"
},
{
"logMustNotContain": "WARNING: your application is being opted out of @vercel/next's optimized lambdas mode due to legacy routes"
},
{
"logMustNotContain": "WARNING: Your application is being opted out of \"@vercel/next\" optimized lambdas mode due to `functions` config"
}
]
}

View File

@@ -17,6 +17,9 @@
"path": "/api/memory",
"status": 200,
"mustContain": "128"
},
{
"logMustContain": "WARNING: Your application is being opted out of \"@vercel/next\" optimized lambdas mode due to `functions` config"
}
]
}

View File

@@ -1,5 +1,6 @@
{
"version": 2,
"uploadNowJson": true,
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
"probes": [
{
@@ -61,6 +62,12 @@
"x-hello": "world",
"x-another": "value"
}
},
{
"logMustNotContain": "WARNING: your application is being opted out of @vercel/next's optimized lambdas mode due to legacy routes"
},
{
"logMustNotContain": "WARNING: Your application is being opted out of \"@vercel/next\" optimized lambdas mode due to `functions` config"
}
]
}

View File

@@ -1,11 +1,15 @@
{
"version": 2,
"uploadNowJson": true,
"builds": [{ "src": "packages/web/next.config.js", "use": "@vercel/next" }],
"routes": [{ "src": "/(.*)", "dest": "/packages/web/$1", "continue": true }],
"probes": [
{
"path": "/",
"mustContain": "hello world <!-- -->6"
},
{
"logMustContain": "WARNING: your application is being opted out of @vercel/next's optimized lambdas mode due to legacy routes"
}
]
}

View File

@@ -1,159 +0,0 @@
/* eslint-env jest */
const fetch = require('node-fetch');
const cheerio = require('cheerio');
module.exports = function(ctx) {
it('should revalidate content properly from pathname', async () => {
const res = await fetch(`${ctx.deploymentUrl}/another`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const initialTime = $('#time').text();
const initialRandom = $('#random').text();
expect($('#hello').text()).toBe('hello: world');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/another`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
expect($('#hello').text()).toBe('hello: world');
expect(initialTime).not.toBe($('#time').text());
expect(initialRandom).not.toBe($('#random').text());
});
it('should revalidate content properly from dynamic pathname', async () => {
const res = await fetch(`${ctx.deploymentUrl}/blog/post-123`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const initialTime = $('#time').text();
const initialRandom = $('#random').text();
expect($('#post').text()).toBe('Post: post-123');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/blog/post-123`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
expect($('#post').text()).toBe('Post: post-123');
expect(initialTime).not.toBe($('#time').text());
expect(initialRandom).not.toBe($('#random').text());
});
it('should revalidate content properly from dynamic pathnames', async () => {
const res = await fetch(`${ctx.deploymentUrl}/blog/post-123/comment-321`);
expect(res.status).toBe(200);
let $ = cheerio.load(await res.text());
const initialTime = $('#time').text();
const initialRandom = $('#random').text();
expect($('#post').text()).toBe('Post: post-123');
expect($('#comment').text()).toBe('Comment: comment-321');
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(`${ctx.deploymentUrl}/blog/post-123/comment-321`);
expect(res2.status).toBe(200);
$ = cheerio.load(await res2.text());
expect($('#post').text()).toBe('Post: post-123');
expect($('#comment').text()).toBe('Comment: comment-321');
expect(initialTime).not.toBe($('#time').text());
expect(initialRandom).not.toBe($('#random').text());
});
it('should revalidate content properly from /_next/data pathname', async () => {
const res = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/another.json`
);
expect(res.status).toBe(200);
const { pageProps: data } = await res.json();
const initialTime = data.time;
const initialRandom = data.random;
expect(data.world).toBe('world');
expect(isNaN(initialTime)).toBe(false);
expect(isNaN(initialRandom)).toBe(false);
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/another.json`
);
expect(res2.status).toBe(200);
const { pageProps: data2 } = await res2.json();
expect(data2.world).toBe('world');
expect(isNaN(data2.time)).toBe(false);
expect(isNaN(data2.random)).toBe(false);
expect(initialTime).not.toBe(data2.time);
expect(initialRandom).not.toBe(data2.random);
});
it('should revalidate content properly from /_next/data dynamic pathname', async () => {
const res = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123.json`
);
expect(res.status).toBe(200);
const { pageProps: data } = await res.json();
const initialTime = data.time;
const initialRandom = data.random;
expect(data.post).toBe('post-123');
expect(isNaN(initialTime)).toBe(false);
expect(isNaN(initialRandom)).toBe(false);
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123.json`
);
expect(res2.status).toBe(200);
const { pageProps: data2 } = await res2.json();
expect(data2.post).toBe('post-123');
expect(isNaN(data2.time)).toBe(false);
expect(isNaN(data2.random)).toBe(false);
expect(initialTime).not.toBe(data2.time);
expect(initialRandom).not.toBe(data2.random);
});
it('should revalidate content properly from /_next/data dynamic pathnames', async () => {
const res = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123/comment-321.json`
);
expect(res.status).toBe(200);
const { pageProps: data } = await res.json();
const initialTime = data.time;
const initialRandom = data.random;
expect(data.post).toBe('post-123');
expect(data.comment).toBe('comment-321');
expect(isNaN(initialTime)).toBe(false);
expect(isNaN(initialRandom)).toBe(false);
// wait for revalidation to occur
await new Promise(resolve => setTimeout(resolve, 2000));
const res2 = await fetch(
`${ctx.deploymentUrl}/_next/data/testing-build-id/blog/post-123/comment-321.json`
);
expect(res2.status).toBe(200);
const { pageProps: data2 } = await res2.json();
expect(data2.post).toBe('post-123');
expect(data2.comment).toBe('comment-321');
expect(isNaN(data2.time)).toBe(false);
expect(isNaN(data2.random)).toBe(false);
expect(initialTime).not.toBe(data2.time);
expect(initialRandom).not.toBe(data2.random);
});
};

View File

@@ -1,5 +0,0 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
};

View File

@@ -1,146 +0,0 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next",
"config": {
"sharedLambdas": true
}
}
],
"probes": [
{
"path": "/lambda",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "MISS"
}
},
{
"path": "/forever",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "PRERENDER"
}
},
{ "delay": 2000 },
{
"path": "/forever",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "HIT"
}
},
{
"path": "/blog/post-3",
"status": 200,
"mustContain": "loading..."
},
{ "delay": 2000 },
{
"path": "/blog/post-3",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "/HIT|STALE/"
}
},
{
"path": "/_next/data/testing-build-id/blog/post-4.json",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "MISS"
}
},
{ "delay": 2000 },
{
"path": "/_next/data/testing-build-id/blog/post-4.json",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "/HIT|STALE/"
}
},
{
"path": "/blog/post-3",
"status": 200,
"mustContain": "post-3"
},
{
"path": "/blog/post-3/comment-3",
"status": 200,
"mustContain": "loading..."
},
{
"path": "/_next/data/testing-build-id/lambda.json",
"status": 404
},
{
"path": "/_next/data/testing-build-id/forever.json",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "/PRERENDER|HIT/"
}
},
{ "delay": 2000 },
{
"path": "/blog/post-3/comment-3",
"status": 200,
"mustContain": "comment-3"
},
{
"path": "/_next/data/testing-build-id/forever.json",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "HIT"
}
},
{
"path": "/_next/data/testing-build-id/another2.json",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "PRERENDER"
}
},
{ "delay": 2000 },
{
"path": "/_next/data/testing-build-id/another2.json",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "HIT"
}
},
{
"path": "/nofallback/one",
"status": 200,
"mustContain": "one"
},
{
"path": "/nofallback/two",
"status": 200,
"mustContain": "two"
},
{
"path": "/nofallback/nope",
"status": 404,
"mustContain": "This page could not be found"
},
{
"path": "/_next/data/testing-build-id/nofallback/one.json",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "/HIT|STALE|PRERENDER/"
}
},
{
"path": "/_next/data/testing-build-id/nofallback/two.json",
"status": 200,
"responseHeaders": {
"x-vercel-cache": "/HIT|STALE|PRERENDER/"
}
},
{
"path": "/_next/data/testing-build-id/nofallback/nope.json",
"status": 404
}
]
}

View File

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

View File

@@ -1,23 +0,0 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function getStaticProps() {
return {
props: {
world: 'world',
random: Math.random(),
time: new Date().getTime(),
},
unstable_revalidate: 1,
};
}
export default ({ world, time, random }) => {
return (
<>
<p id="hello">hello: {world}</p>
<span id="time">time: {time}</span>
<span id="random">random: {random}</span>
</>
);
};

View File

@@ -1,21 +0,0 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function getStaticProps() {
return {
props: {
world: 'world',
time: new Date().getTime(),
},
unstable_revalidate: 5,
};
}
export default ({ world, time }) => {
return (
<>
<p>hello: {world}</p>
<span>time: {time}</span>
</>
);
};

View File

@@ -1,40 +0,0 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function getStaticPaths() {
return {
paths: [
'/blog/post-1/comment-1',
{ params: { post: 'post-2', comment: 'comment-2' } },
'/blog/post-1337/comment-1337',
'/blog/post-123/comment-321'
],
fallback: true,
};
}
// eslint-disable-next-line camelcase
export async function getStaticProps({ params }) {
return {
props: {
post: params.post,
random: Math.random(),
comment: params.comment,
time: new Date().getTime(),
},
unstable_revalidate: 1,
};
}
export default ({ post, comment, time, random }) => {
if (!post) return <p>loading...</p>;
return (
<>
<p id='post'>Post: {post}</p>
<p id='comment'>Comment: {comment}</p>
<span id='time'>time: {time}</span>
<span id='random'>random: {random}</span>
</>
);
};

View File

@@ -1,43 +0,0 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function getStaticPaths() {
return {
paths: [
'/blog/post-1',
{ params: { post: 'post-2' } },
'/blog/post-123',
],
fallback: true,
};
}
// eslint-disable-next-line camelcase
export async function getStaticProps({ params }) {
if (params.post === 'post-10') {
await new Promise(resolve => {
setTimeout(() => resolve(), 1000);
});
}
return {
props: {
post: params.post,
random: Math.random(),
time: (await import('perf_hooks')).performance.now(),
},
unstable_revalidate: 1,
};
}
export default ({ post, time, random }) => {
if (!post) return <p>loading...</p>;
return (
<>
<p id='post'>Post: {post}</p>
<span id='time'>time: {time}</span>
<span id='random'>random: {random}</span>
</>
);
};

View File

@@ -1,21 +0,0 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function getStaticProps() {
return {
props: {
world: 'world',
time: new Date().getTime(),
},
unstable_revalidate: false,
};
}
export default ({ world, time }) => {
return (
<>
<p>hello: {world}</p>
<span>time: {time}</span>
</>
);
};

View File

@@ -1 +0,0 @@
export default () => 'Hi';

View File

@@ -1,5 +0,0 @@
const Page = ({ data }) => <p>{data} world</p>;
Page.getInitialProps = () => ({ data: 'hello' });
export default Page;

View File

@@ -1,31 +0,0 @@
import React from 'react';
// eslint-disable-next-line camelcase
export async function getStaticPaths() {
return {
paths: ['/nofallback/one', { params: { slug: 'two' } }],
fallback: false,
};
}
// eslint-disable-next-line camelcase
export async function getStaticProps({ params }) {
return {
props: {
slug: params.slug,
time: (await import('perf_hooks')).performance.now(),
},
unstable_revalidate: 10,
};
}
export default ({ slug, time }) => {
return (
<>
<p>
Slug ({slug.length}): {slug}
</p>
<span>time: {time}</span>
</>
);
};

View File

@@ -1,5 +1,6 @@
{
"version": 2,
"uploadNowJson": true,
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
"probes": [
{
@@ -133,6 +134,12 @@
{
"path": "/_next/data/testing-build-id/nofallback/nope.json",
"status": 404
},
{
"logMustNotContain": "WARNING: your application is being opted out of @vercel/next's optimized lambdas mode due to legacy routes"
},
{
"logMustNotContain": "WARNING: Your application is being opted out of \"@vercel/next\" optimized lambdas mode due to `functions` config"
}
]
}

View File

@@ -1,217 +0,0 @@
module.exports = {
generateBuildId() {
return 'testing-build-id';
},
async rewrites() {
return [
{
source: '/to-another',
destination: '/another/one',
},
{
source: '/nav',
destination: '/404',
},
{
source: '/hello-world',
destination: '/static/hello.txt',
},
{
source: '/',
destination: '/another',
},
{
source: '/another',
destination: '/multi-rewrites',
},
{
source: '/first',
destination: '/hello',
},
{
source: '/second',
destination: '/hello-again',
},
{
source: '/to-hello',
destination: '/hello',
},
{
source: '/(.*)-:id(\\d+).html',
destination: '/blog/:id',
},
{
source: '/blog/post-1',
destination: '/blog/post-2',
},
{
source: '/test/:path',
destination: '/:path',
},
{
source: '/test-overwrite/:something/:another',
destination: '/params/this-should-be-the-value',
},
{
source: '/params/:something',
destination: '/with-params',
},
{
source: '/query-rewrite/:section/:name',
destination: '/with-params?first=:section&second=:name',
},
{
source: '/hidden/_next/:path*',
destination: '/_next/:path*',
},
{
source: '/api-hello',
destination: '/api/hello',
},
{
source: '/api-hello-regex/(.*)',
destination: '/api/hello?name=:1',
},
{
source: '/api-hello-param/:name',
destination: '/api/hello?hello=:name',
},
{
source: '/api-dynamic-param/:name',
destination: '/api/dynamic/:name?hello=:name',
},
{
source: '/:path/post-321',
destination: '/with-params',
},
{
source: '/a/catch-all/:path*',
destination: '/a/catch-all',
},
];
},
async redirects() {
return [
{
source: '/redirect/me/to-about/:lang',
destination: '/:lang/about',
permanent: false,
},
{
source: '/docs/router-status/:code',
destination: '/docs/v2/network/status-codes#:code',
statusCode: 301,
},
{
source: '/docs/github',
destination: '/docs/v2/advanced/now-for-github',
statusCode: 301,
},
{
source: '/docs/v2/advanced/:all(.*)',
destination: '/docs/v2/more/:all',
statusCode: 301,
},
{
source: '/hello/:id/another',
destination: '/blog/:id',
permanent: false,
},
{
source: '/redirect1',
destination: '/',
permanent: false,
},
{
source: '/redirect2',
destination: '/',
statusCode: 301,
},
{
source: '/redirect3',
destination: '/another',
statusCode: 302,
},
{
source: '/redirect4',
destination: '/',
permanent: true,
},
{
source: '/redir-chain1',
destination: '/redir-chain2',
statusCode: 301,
},
{
source: '/redir-chain2',
destination: '/redir-chain3',
statusCode: 302,
},
{
source: '/redir-chain3',
destination: '/',
statusCode: 303,
},
{
source: '/to-external',
destination: 'https://google.com',
permanent: false,
},
{
source: '/query-redirect/:section/:name',
destination: '/with-params?first=:section&second=:name',
permanent: false,
},
{
source: '/named-like-unnamed/:0',
destination: '/:0',
permanent: false,
},
{
source: '/redirect-override',
destination: '/thank-you-next',
permanent: false,
},
];
},
async headers() {
return [
{
source: '/add-header',
headers: [
{
key: 'x-custom-header',
value: 'hello world',
},
{
key: 'x-another-header',
value: 'hello again',
},
],
},
{
source: '/my-headers/(.*)',
headers: [
{
key: 'x-first-header',
value: 'first',
},
{
key: 'x-second-header',
value: 'second',
},
],
},
{
source: '/:path*',
headers: [
{
key: 'x-something',
value: 'applied-everywhere',
},
],
},
];
},
};

View File

@@ -1,246 +0,0 @@
{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/next",
"config": {
"sharedLambdas": true
}
}
],
"probes": [
// should handle one-to-one rewrite successfully
{
"path": "/first",
"mustContain": "hello"
},
// should handle chained rewrites successfully
{
"path": "/",
"mustContain": "multi-rewrites"
},
// should not match dynamic route immediately after applying header
{
"path": "/blog/post-321",
"mustContain": "with-params"
},
{
"path": "/blog/post-321",
"mustNotContain": "post-321"
},
// should handle chained redirects successfully
{
"path": "/redir-chain1",
"status": 301,
"responseHeaders": {
"location": "//redir-chain2/"
},
"fetchOptions": {
"redirect": "manual"
}
},
{
"path": "/redir-chain2",
"status": 302,
"responseHeaders": {
"location": "//redir-chain3/"
},
"fetchOptions": {
"redirect": "manual"
}
},
{
"path": "/redir-chain3",
"status": 303,
"responseHeaders": {
"location": "//$/"
},
"fetchOptions": {
"redirect": "manual"
}
},
// should redirect successfully with permanent: false
{
"path": "/redirect1",
"status": 307,
"responseHeaders": {
"location": "//$/"
},
"fetchOptions": {
"redirect": "manual"
}
},
// should redirect with params successfully
{
"path": "/hello/123/another",
"status": 307,
"responseHeaders": {
"location": "//blog/123/"
},
"fetchOptions": {
"redirect": "manual"
}
},
// should redirect with hash successfully
{
"path": "/docs/router-status/500",
"status": 301,
"responseHeaders": {
"location": "/#500$/"
},
"fetchOptions": {
"redirect": "manual"
}
},
// should redirect successfully with provided statusCode
{
"path": "/redirect2",
"status": 301,
"responseHeaders": {
"location": "//$/"
},
"fetchOptions": {
"redirect": "manual"
}
},
// should server static files through a rewrite
{
"path": "/hello-world",
"mustContain": "hello world!"
},
// should rewrite with params successfully
{
"path": "/test/hello",
"mustContain": "Hello"
},
// should double redirect successfully
{
"path": "/docs/github",
"mustContain": "hi there"
},
// should allow params in query for rewrite
{
"path": "/query-rewrite/hello/world?a=b",
"mustContain": "\"a\":\"b\""
},
{
"path": "/query-rewrite/hello/world?a=b",
"mustContain": "\"section\":\"hello\""
},
{
"path": "/query-rewrite/hello/world?a=b",
"mustContain": "\"name\":\"world\""
},
{
"path": "/query-rewrite/hello/world?a=b",
"mustContain": "\"first\":\"hello\""
},
{
"path": "/query-rewrite/hello/world?a=b",
"mustContain": "\"second\":\"world\""
},
// should not allow rewrite to override page file
{
"path": "/nav",
"mustContain": "to-hello"
},
// show allow redirect to override the page
{
"path": "/redirect-override",
"status": 307,
"responseHeaders": {
"location": "//thank-you-next$/"
},
"fetchOptions": {
"redirect": "manual"
}
},
// should match a page after a rewrite
{
"path": "/to-hello",
"mustContain": "Hello"
},
// should match dynamic route after rewrite
{
"path": "/blog/post-1",
"mustContain": "post-2"
},
// should match public file after rewrite
{
"path": "/blog/data.json",
"mustContain": "\"hello\": \"world\""
},
// should match /_next file after rewrite
{
"path": "/hidden/_next/__NEXT_SCRIPT__(/hello)",
"mustContain": "createElement"
},
// should allow redirecting to external resource
{
"path": "/to-external",
"status": 307,
"responseHeaders": {
"location": "/google.com/"
},
"fetchOptions": {
"redirect": "manual"
}
},
// should apply headers for exact match
{
"path": "/add-header",
"responseHeaders": {
"x-custom-header": "hello world",
"x-another-header": "hello again"
}
},
// should apply headers for multi match
{
"path": "/my-headers/first",
"responseHeaders": {
"x-first-header": "first",
"x-second-header": "second"
}
},
// should handle basic api rewrite successfully
{
"path": "/api-hello",
"mustContain": "{\"query\":{}}"
},
// should handle api rewrite with param successfully
{
"path": "/api-hello-param/hello",
"mustContain": "{\"query\":{\"hello\":\"hello\",\"name\":\"hello\"}}"
},
// should handle encoded value in the pathname correctly
{
"path": "/redirect/me/to-about/%5Cgoogle.com",
"status": 307,
"responseHeaders": {
"location": "/%5Cgoogle.com/about/"
},
"fetchOptions": {
"redirect": "manual"
}
},
// should apply un-named multi-match correctly
{
"path": "/hello/post-123.html",
"status": 200,
"mustContain": "123"
},
// should rewrite to catch-all with dash in segment name
{
"path": "/catchall-dash/hello/world",
"status": 200,
"mustContain": "hello/world"
},
// should rewrite and normalize catch-all rewrite param
{
"path": "/a/catch-all/hello/world",
"status": 200,
"mustContain": "hello/world"
}
]
}

View File

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

View File

@@ -1,9 +0,0 @@
import { useRouter } from 'next/router';
export default () => <p>{useRouter().query.path?.join('/')}</p>;
export const getServerSideProps = () => {
return {
props: {},
};
};

View File

@@ -1 +0,0 @@
export default () => 'hi'

View File

@@ -1 +0,0 @@
export default async (req, res) => res.json({ query: req.query })

View File

@@ -1 +0,0 @@
export default async (req, res) => res.json({ query: req.query });

View File

@@ -1,11 +0,0 @@
import { useRouter } from 'next/router'
const Page = () => (
<>
<p>post: {useRouter().query.post}</p>
</>
)
Page.getInitialProps = () => ({ hello: 'world' })
export default Page

View File

@@ -1,17 +0,0 @@
import { useRouter } from 'next/router'
const Page = () => {
return (
<p>path: {useRouter().query['hello-world']?.join('/')}</p>
)
}
export default Page
export const getServerSideProps = () => {
return {
props: {
hello: 'world'
}
}
}

View File

@@ -1,10 +0,0 @@
import Link from 'next/link';
export default () => (
<>
<h3 id="hello-again">Hello again</h3>
<Link href="/nav">
<a id="to-nav">to nav</a>
</Link>
</>
);

View File

@@ -1,14 +0,0 @@
import Link from 'next/link';
const Page = () => (
<>
<h3 id="hello">Hello</h3>
<Link href="/nav">
<a id="to-nav">to nav</a>
</Link>
</>
);
Page.getInitialProps = () => ({ hello: 'world' });
export default Page;

View File

@@ -1 +0,0 @@
export default () => 'multi-rewrites';

View File

@@ -1,13 +0,0 @@
import Link from 'next/link';
export default () => (
<>
<h3 id="nav">Nav</h3>
<Link href="/hello" as="/first">
<a id="to-hello">to hello</a>
</Link>
<Link href="/hello-again" as="/second">
<a id="to-hello-again">to hello-again</a>
</Link>
</>
);

View File

@@ -1,10 +0,0 @@
import { useRouter } from 'next/router';
const Page = () => {
const { query } = useRouter();
return <p>{JSON.stringify(query)}</p>;
};
Page.getInitialProps = () => ({ a: 'b' });
export default Page;

View File

@@ -1 +0,0 @@
export default () => 'got to the page';

View File

@@ -1,10 +0,0 @@
import { useRouter } from 'next/router';
const Page = () => {
const { query } = useRouter();
return <p>{JSON.stringify(query)}</p>;
};
Page.getInitialProps = () => ({ hello: 'GIPGIP' });
export default Page;

View File

@@ -1,5 +1,6 @@
{
"version": 2,
"uploadNowJson": true,
"routes": [
{ "src": "/(.*)", "dest": "/packages/webapp/$1", "continue": true }
],
@@ -31,6 +32,9 @@
{
"path": "/non-existent",
"status": 404
},
{
"logMustContain": "WARNING: your application is being opted out of @vercel/next's optimized lambdas mode due to legacy routes"
}
]
}

View File

@@ -1,5 +1,6 @@
{
"version": 2,
"uploadNowJson": true,
"routes": [
{ "src": "/(.*)", "dest": "/packages/webapp/$1", "continue": true }
],
@@ -25,6 +26,9 @@
{
"path": "/non-existent",
"status": 404
},
{
"logMustContain": "WARNING: your application is being opted out of @vercel/next's optimized lambdas mode due to legacy routes"
}
]
}

View File

@@ -15,8 +15,8 @@ it(
buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'standard'));
expect(output['index']).toBeDefined();
expect(output.goodbye).toBeDefined();
expect(output.__NEXT_PAGE_LAMBDA_0).not.toBeDefined();
expect(output.goodbye).not.toBeDefined();
expect(output.__NEXT_PAGE_LAMBDA_0).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)
@@ -34,27 +34,29 @@ it(
FOUR_MINUTES
);
// it(
// 'Should opt-out of shared lambdas when routes are detected',
// async () => {
// const {
// buildResult: { output },
// } = await runBuildLambda(path.join(__dirname, '../fixtures/26-mono-repo-404-lambda'));
// expect(output['packages/webapp/404']).toBeDefined();
// expect(output['packages/webapp/index']).toBeDefined();
// expect(output['packages/webapp/__NEXT_PAGE_LAMBDA_0']).not.toBeDefined();
// const filePaths = Object.keys(output);
// const hasUnderScoreAppStaticFile = filePaths.some(filePath =>
// filePath.match(/static.*\/pages\/_app\.js$/)
// );
// const hasUnderScoreErrorStaticFile = filePaths.some(filePath =>
// filePath.match(/static.*\/pages\/_error\.js$/)
// );
// expect(hasUnderScoreAppStaticFile).toBeTruthy();
// expect(hasUnderScoreErrorStaticFile).toBeTruthy();
// },
// FOUR_MINUTES
// );
it(
'Should opt-out of shared lambdas when routes are detected',
async () => {
const {
buildResult: { output },
} = await runBuildLambda(
path.join(__dirname, '../fixtures/26-mono-repo-404-lambda')
);
expect(output['packages/webapp/404']).toBeDefined();
expect(output['packages/webapp/index']).toBeDefined();
expect(output['packages/webapp/__NEXT_PAGE_LAMBDA_0']).not.toBeDefined();
const filePaths = Object.keys(output);
const hasUnderScoreAppStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_app\.js$/)
);
const hasUnderScoreErrorStaticFile = filePaths.some(filePath =>
filePath.match(/static.*\/pages\/_error\.js$/)
);
expect(hasUnderScoreAppStaticFile).toBeTruthy();
expect(hasUnderScoreErrorStaticFile).toBeTruthy();
},
FOUR_MINUTES
);
it(
'Should build the monorepo example',
@@ -63,8 +65,8 @@ it(
buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'monorepo'));
expect(output['www/index']).toBeDefined();
expect(output['www/__NEXT_PAGE_LAMBDA_0']).not.toBeDefined();
expect(output['www/index']).not.toBeDefined();
expect(output['www/__NEXT_PAGE_LAMBDA_0']).toBeDefined();
expect(output['www/static/test.txt']).toBeDefined();
expect(output['www/data.txt']).toBeDefined();
const filePaths = Object.keys(output);
@@ -164,9 +166,9 @@ it(
buildResult: { output },
} = await runBuildLambda(path.join(__dirname, 'serverless-config'));
expect(output.index).toBeDefined();
expect(output.goodbye).toBeDefined();
expect(output.__NEXT_PAGE_LAMBDA_0).not.toBeDefined();
expect(output.index).not.toBeDefined();
expect(output.goodbye).not.toBeDefined();
expect(output.__NEXT_PAGE_LAMBDA_0).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)
@@ -201,9 +203,9 @@ it(
path.join(__dirname, 'serverless-config-monorepo-missing')
);
expect(output['nested/index']).toBeDefined();
expect(output['nested/goodbye']).toBeDefined();
expect(output['nested/__NEXT_PAGE_LAMBDA_0']).not.toBeDefined();
expect(output['nested/index']).not.toBeDefined();
expect(output['nested/goodbye']).not.toBeDefined();
expect(output['nested/__NEXT_PAGE_LAMBDA_0']).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)
@@ -235,9 +237,9 @@ it(
path.join(__dirname, 'serverless-config-monorepo-present')
);
expect(output['nested/index']).toBeDefined();
expect(output['nested/goodbye']).toBeDefined();
expect(output['nested/__NEXT_PAGE_LAMBDA_0']).not.toBeDefined();
expect(output['nested/index']).not.toBeDefined();
expect(output['nested/goodbye']).not.toBeDefined();
expect(output['nested/__NEXT_PAGE_LAMBDA_0']).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)
@@ -303,8 +305,8 @@ it(
} = await runBuildLambda(path.join(__dirname, 'serverless-config-object'));
expect(output['index']).toBeDefined();
expect(output.goodbye).toBeDefined();
expect(output.__NEXT_PAGE_LAMBDA_0).not.toBeDefined();
expect(output.goodbye).not.toBeDefined();
expect(output.__NEXT_PAGE_LAMBDA_0).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)
@@ -338,8 +340,8 @@ it(
} = await runBuildLambda(path.join(__dirname, 'serverless-no-config'));
expect(output['index']).toBeDefined();
expect(output.goodbye).toBeDefined();
expect(output.__NEXT_PAGE_LAMBDA_0).not.toBeDefined();
expect(output.goodbye).not.toBeDefined();
expect(output.__NEXT_PAGE_LAMBDA_0).toBeDefined();
const filePaths = Object.keys(output);
const serverlessError = filePaths.some(filePath =>
filePath.match(/_error/)

View File

@@ -24,9 +24,12 @@ beforeAll(async () => {
}
process.env.NEXT_TELEMETRY_DISABLED = '1';
const builderPath = path.resolve(__dirname, '..');
builderUrl = await packAndDeploy(builderPath);
console.log('builderUrl', builderUrl);
if (!builderUrl) {
const builderPath = path.resolve(__dirname, '..');
builderUrl = await packAndDeploy(builderPath);
console.log('builderUrl', builderUrl);
}
});
const fixturesPath = path.resolve(__dirname, 'fixtures');

View File

@@ -5,9 +5,11 @@ const _fetch = require('node-fetch');
const fetch = require('./fetch-retry.js');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function nowDeploy(bodies, randomness) {
async function nowDeploy(bodies, randomness, uploadNowJson) {
const files = Object.keys(bodies)
.filter(n => n !== 'vercel.json' && n !== 'now.json')
.filter(n =>
uploadNowJson ? true : n !== 'vercel.json' && n !== 'now.json'
)
.map(n => ({
sha: digestOfFile(bodies[n]),
size: bodies[n].length,

View File

@@ -57,6 +57,8 @@ async function testDeployment(
// we use json5 to allow comments for probes
const nowJson = json5.parse(bodies[configName]);
const uploadNowJson = nowJson.uploadNowJson;
delete nowJson.uploadNowJson;
if (process.env.VERCEL_BUILDER_DEBUG) {
if (!nowJson.build) {
@@ -94,8 +96,14 @@ async function testDeployment(
bodies[configName] = Buffer.from(JSON.stringify(nowJson));
delete bodies['probe.js'];
const { deploymentId, deploymentUrl } = await nowDeploy(bodies, randomness);
const { deploymentId, deploymentUrl } = await nowDeploy(
bodies,
randomness,
uploadNowJson
);
let nextBuildManifest;
let deploymentLogs;
for (const probe of nowJson.probes || []) {
console.log('testing', JSON.stringify(probe));
@@ -104,6 +112,60 @@ async function testDeployment(
continue;
}
if (probe.logMustContain || probe.logMustNotContain) {
const shouldContain = !!probe.logMustContain;
const toCheck = probe.logMustContain || probe.logMustNotContain;
if (probe.logMustContain && probe.logMustNotContain) {
throw new Error(
`probe can not check logMustContain and logMustNotContain in the same check`
);
}
if (!deploymentLogs) {
try {
const logsRes = await fetch(
`https://vercel.com/api/v1/now/deployments/${deploymentId}/events?limit=-1`
);
if (!logsRes.ok) {
throw new Error(
`fetching logs failed with status ${logsRes.status}`
);
}
deploymentLogs = await logsRes.json();
} catch (err) {
throw new Error(
`Failed to get deployment logs for probe: ${err.message}`
);
}
}
let found = false;
for (const log of deploymentLogs) {
if (log.text && log.text.includes(toCheck)) {
if (shouldContain) {
found = true;
break;
} else {
throw new Error(
`Expected deployment logs not to contain ${toCheck}, but found ${log.text}`
);
}
}
}
if (!found && shouldContain) {
throw new Error(
`Expected deployment logs to contain ${toCheck}, it was not found`
);
} else {
console.log('finished testing', JSON.stringify(probe));
continue;
}
}
const nextScriptIndex = probe.path.indexOf('__NEXT_SCRIPT__(');
if (nextScriptIndex > -1) {

View File

@@ -2190,6 +2190,11 @@
semver "^7.3.2"
tsutils "^3.17.1"
"@vercel/static-build@0.17.4-canary.1":
version "0.17.4-canary.1"
resolved "https://registry.yarnpkg.com/@vercel/static-build/-/static-build-0.17.4-canary.1.tgz#42ba89d5dee7ced3a20ec0a3a2986d293405530a"
integrity sha512-SWvu9zm6sQGuQeouHDnxOYxDlgvKSk7pb4m2QuVufFXrhOkx7OL6e91riHXCpmscv8Pb9mD7DFVl2o78FK7viA==
"@zeit/dns-cached-resolve@2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@zeit/dns-cached-resolve/-/dns-cached-resolve-2.1.0.tgz#78583010df1683fdb7b05949b75593c9a8641bc1"