[next] Ensure manifests are specific to the included pages (#8172)

### Related Issues

This updates to filter the `routes-manifest` and `pages-manifest` to only include entries for the pages that are being included in the specific serverless function. This fixes the case where multiple dynamic routes could match a path but one is configured with `fallback: false` so shouldn't match when executing for a different dynamic route. 

A regression test for this specific scenario has been added in the `00-mixed-dynamic-routes` fixture. 

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
This commit is contained in:
JJ Kasper
2022-07-20 18:06:44 -05:00
committed by GitHub
parent 5dc6f48e44
commit 8cf67b549b
8 changed files with 209 additions and 14 deletions

View File

@@ -57,6 +57,7 @@ const CORRECT_NOT_FOUND_ROUTES_VERSION = 'v12.0.1';
const CORRECT_MIDDLEWARE_ORDER_VERSION = 'v12.1.7-canary.29';
const NEXT_DATA_MIDDLEWARE_RESOLVING_VERSION = 'v12.1.7-canary.33';
const EMPTY_ALLOW_QUERY_FOR_PRERENDERED_VERSION = 'v12.2.0';
const CORRECTED_MANIFESTS_VERSION = 'v12.2.0';
export async function serverBuild({
dynamicPages,
@@ -146,6 +147,10 @@ export async function serverBuild({
nextVersion,
CORRECT_MIDDLEWARE_ORDER_VERSION
);
const isCorrectManifests = semver.gte(
nextVersion,
CORRECTED_MANIFESTS_VERSION
);
let hasStatic500 = !!staticPages[path.join(entryDirectory, '500')];
if (lambdaPageKeys.length === 0) {
@@ -409,7 +414,7 @@ export async function serverBuild({
fsPath = path.join(requiredServerFilesManifest.appDir, file);
}
const relativePath = path.join(path.relative(baseDir, fsPath));
const relativePath = path.relative(baseDir, fsPath);
const { mode } = await fs.lstat(fsPath);
lstatSema.release();
@@ -676,18 +681,80 @@ export async function serverBuild({
);
for (const group of combinedGroups) {
const lambda = await createLambdaFromPseudoLayers({
files: launcherFiles,
layers: [
group.pseudoLayer,
[...group.pages, ...internalPages].reduce((prev, page) => {
const groupPageFiles: { [key: string]: PseudoFile } = {};
for (const page of [...group.pages, ...internalPages]) {
const pageFileName = path.normalize(
path.relative(baseDir, lambdaPages[page].fsPath)
);
prev[pageFileName] = compressedPages[page];
return prev;
}, {} as { [key: string]: PseudoFile }),
],
groupPageFiles[pageFileName] = compressedPages[page];
}
const updatedManifestFiles: { [name: string]: FileBlob } = {};
if (isCorrectManifests) {
// filter dynamic routes to only the included dynamic routes
// in this specific serverless function so that we don't
// accidentally match a dynamic route while resolving that
// is not actually in this specific serverless function
for (const manifest of [
'routes-manifest.json',
'server/pages-manifest.json',
] as const) {
const fsPath = path.join(entryPath, outputDirectory, manifest);
const relativePath = path.relative(baseDir, fsPath);
delete group.pseudoLayer[relativePath];
const manifestData = await fs.readJSON(fsPath);
const normalizedPages = new Set(
group.pages.map(page => {
page = `/${page.replace(/\.js$/, '')}`;
if (page === '/index') page = '/';
return page;
})
);
switch (manifest) {
case 'routes-manifest.json': {
const filterItem = (item: { page: string }) =>
normalizedPages.has(item.page);
manifestData.dynamicRoutes =
manifestData.dynamicRoutes?.filter(filterItem);
manifestData.staticRoutes =
manifestData.staticRoutes?.filter(filterItem);
break;
}
case 'server/pages-manifest.json': {
for (const key of Object.keys(manifestData)) {
if (isDynamicRoute(key) && !normalizedPages.has(key)) {
delete manifestData[key];
}
}
break;
}
default: {
throw new NowBuildError({
message: `Unexpected manifest value ${manifest}, please contact support if this continues`,
code: 'NEXT_MANIFEST_INVARIANT',
});
}
}
updatedManifestFiles[relativePath] = new FileBlob({
contentType: 'application/json',
data: JSON.stringify(manifestData),
});
}
}
const lambda = await createLambdaFromPseudoLayers({
files: {
...launcherFiles,
...updatedManifestFiles,
},
layers: [group.pseudoLayer, groupPageFiles],
handler: path.join(
path.relative(
baseDir,

View File

@@ -0,0 +1,8 @@
const path = require('path');
const { deployAndTest } = require('../../utils');
describe(`${__dirname.split(path.sep).pop()}`, () => {
it('should deploy and pass probe checks', async () => {
await deployAndTest(__dirname);
});
});

View File

@@ -0,0 +1,13 @@
{
"dependencies": {
"next": "canary",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}

View File

@@ -0,0 +1,17 @@
export default function Page(props) {
return (
<>
<p>/[...slug] page</p>
<p>{JSON.stringify(props)}</p>
</>
);
}
export const getServerSideProps = async ({ params }) => {
return {
props: {
params,
page: '[...slug]',
},
};
};

View File

@@ -0,0 +1,29 @@
export default function Page(props) {
return (
<>
<p>/[slug] page</p>
<p>{JSON.stringify(props)}</p>
</>
);
}
export const getStaticProps = async ({ params }) => {
return {
props: {
params,
page: '[slug]',
now: Date.now(),
},
};
};
export const getStaticPaths = async () => {
return {
paths: [
{ params: { slug: 'static-1' } },
{ params: { slug: 'static-2' } },
{ params: { slug: 'static-3' } },
],
fallback: false,
};
};

View File

@@ -0,0 +1,17 @@
export default function Page(props) {
return (
<>
<p>index page</p>
<p>{JSON.stringify(props)}</p>
</>
);
}
export const getStaticProps = async () => {
return {
props: {
page: 'index',
now: Date.now(),
},
};
};

View File

@@ -0,0 +1,36 @@
{
"version": 2,
"builds": [{ "src": "package.json", "use": "@vercel/next" }],
"probes": [
{
"path": "/",
"status": 200,
"mustContain": "index page"
},
{
"path": "/static-1",
"status": 200,
"mustContain": "[slug] page"
},
{
"path": "/static-2",
"status": 200,
"mustContain": "[slug] page"
},
{
"path": "/dynamic-1",
"status": 200,
"mustContain": "[...slug] page"
},
{
"path": "/dynamic-2",
"status": 200,
"mustContain": "[...slug] page"
},
{
"path": "/long/dynamic",
"status": 200,
"mustContain": "[...slug] page"
}
]
}

View File

@@ -1,5 +1,7 @@
const os = require('os');
const path = require('path');
const fs = require('fs-extra');
const { glob, getWriteableDirectory } = require('@vercel/build-utils');
const { glob } = require('@vercel/build-utils');
function runAnalyze(wrapper, context) {
if (wrapper.analyze) {
@@ -37,7 +39,13 @@ async function runBuildLambda(inputPath) {
config: build.config,
});
const workPath = await fs.realpath(await getWriteableDirectory());
const workPath = path.join(
os.tmpdir(),
`vercel-${Date.now()}-${Math.floor(Math.random() * 100)}`
);
await fs.ensureDir(workPath);
console.log('building in', workPath);
const buildResult = await wrapper.build({
files: inputFiles,
entrypoint,