Skip to content

Commit

Permalink
Fix prerendered dynamic ISR functions change in Vercel CLI (#834)
Browse files Browse the repository at this point in the history
  • Loading branch information
james-elicx authored Jul 28, 2024
1 parent 78accfd commit 2527917
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/warm-rivers-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudflare/next-on-pages': patch
---

Account for the Vercel CLI no longer generating prerender configs for dynamic ISR functions.
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export type CollectedFunctions = {
export type FunctionInfo = {
relativePath: string;
config: VercelFunctionConfig;
sourcePath?: string;
outputPath?: string;
outputByteSize?: number;
route?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export async function checkInvalidFunctions(
await tryToFixI18nFunctions(collectedFunctions, opts);

await tryToFixInvalidFuncsWithValidIndexAlternative(collectedFunctions);
await tryToFixInvalidDynamicISRFuncs(collectedFunctions);

if (collectedFunctions.invalidFunctions.size > 0) {
await printInvalidFunctionsErrorMessage(
Expand Down Expand Up @@ -309,3 +310,49 @@ async function tryToFixInvalidFuncsWithValidIndexAlternative({
}
}
}

/**
* Tries to fix invalid dynamic ISR functions that have valid prerendered children.
*
* The Vercel CLI might not generated a prerender config for a dynamic ISR function, depending
* on the Vercel CLI version. Therefore, we also check if valid prerendered routes were created
* for the dynamic route to determine if the function can be ignored.
*
* @param collectedFunctions Collected functions from the Vercel build output.
*/
async function tryToFixInvalidDynamicISRFuncs({
prerenderedFunctions,
invalidFunctions,
ignoredFunctions,
}: CollectedFunctions) {
if (invalidFunctions.size === 0) {
return;
}

const prerenderedFunctionEntries = [...prerenderedFunctions.values()];

for (const [fullPath, fnInfo] of invalidFunctions.entries()) {
const fnPathWithoutRscOrFuncExt = fnInfo.relativePath.replace(
/(\.rsc)?\.func$/,
'',
);

const isDynamicISRFunc =
fnInfo.config.operationType === 'ISR' &&
/\/\[[\w-]+\]$/.test(fnPathWithoutRscOrFuncExt);

if (isDynamicISRFunc) {
const matchingPrerenderedChildFunc = prerenderedFunctionEntries.find(
fnInfo => fnInfo.sourcePath === fnPathWithoutRscOrFuncExt,
);

if (matchingPrerenderedChildFunc) {
ignoredFunctions.set(fullPath, {
reason: 'invalid dynamic isr route with valid prerendered children',
...fnInfo,
});
invalidFunctions.delete(fullPath);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export async function processPrerenderFunctions(
headers: config.initialHeaders,
overrides: getRouteOverrides(destRoute),
};
fnInfo.sourcePath = config.sourcePath;
} else {
invalidFunctions.set(path, fnInfo);
prerenderedFunctions.delete(path);
Expand Down
10 changes: 10 additions & 0 deletions packages/next-on-pages/src/utils/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,13 @@ export function getFileHash(path: string): Buffer | undefined {
return undefined;
}
}

/**
* Add a trailing slash to a path name if it doesn't already have one.
*
* @param path Path name to add a trailing slash to.
* @returns Path name with a trailing slash added.
*/
export function addTrailingSlash(path: string) {
return path.endsWith('/') ? path : `${path}/`;
}
10 changes: 7 additions & 3 deletions packages/next-on-pages/tests/_helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,11 +304,14 @@ export function createInvalidFuncDir(
* Create a fake prerender config file for testing.
*
* @param path Path name for the file in the build output.
* @param ext File extension for the fallback file in the build output.
* @param opts File extension for the fallback in the build output and prerender config options.
* @returns The stringified prerender config file contents.
*/
export function mockPrerenderConfigFile(path: string, ext?: string): string {
const extension = ext || (path.endsWith('.rsc') ? 'rsc' : 'html');
export function mockPrerenderConfigFile(
path: string,
opts: { ext?: string; sourcePath?: string } = {},
): string {
const extension = opts.ext || (path.endsWith('.rsc') ? 'rsc' : 'html');
const fsPath = `${path}.prerender-fallback.${extension}`;

const config: VercelPrerenderConfig = {
Expand All @@ -318,6 +321,7 @@ export function mockPrerenderConfigFile(path: string, ext?: string): string {
mode: 0,
fsPath,
},
sourcePath: opts.sourcePath,
initialHeaders: {
...((path.endsWith('.rsc') || path.endsWith('.json')) && {
'content-type': 'text/x-component',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,125 @@ describe('checkInvalidFunctions', () => {
ignoredFunctions.has(resolve(functionsDir, 'index.action.func')),
).toEqual(true);
});

test('should ignore dynamic isr routes with prerendered children', async () => {
const mockedConsoleWarn = mockConsole('warn');

const { collectedFunctions, restoreFsMock } = await collectFunctionsFrom({
functions: {
'[dynamic-1].func': prerenderFuncDir,
'[dynamic-1].rsc.func': prerenderFuncDir,
'dynamic-1-child.func': prerenderFuncDir,
'dynamic-1-child.prerender-config.json': mockPrerenderConfigFile(
'dynamic-1-child',
{ sourcePath: '/[dynamic-1]' },
),
'dynamic-1-child.prerender-fallback.html': '',
nested: {
'[dynamic-2].func': prerenderFuncDir,
'dynamic-2-child.func': prerenderFuncDir,
'dynamic-2-child.prerender-config.json': mockPrerenderConfigFile(
'dynamic-2-child',
{ sourcePath: '/nested/[dynamic-2]' },
),
'dynamic-2-child.prerender-fallback.html': '',
},
},
});

const opts = {
functionsDir,
outputDir: resolve('.vercel/output/static'),
vercelConfig: { version: 3 as const },
};

await processEdgeFunctions(collectedFunctions);
await processPrerenderFunctions(collectedFunctions, opts);
await checkInvalidFunctions(collectedFunctions, opts);
restoreFsMock();

const { prerenderedFunctions, invalidFunctions, ignoredFunctions } =
collectedFunctions;

expect(prerenderedFunctions.size).toEqual(2);
expect(invalidFunctions.size).toEqual(0);
expect(ignoredFunctions.size).toEqual(3);

expect(getRouteInfo(prerenderedFunctions, 'dynamic-1-child.func')).toEqual({
path: '/dynamic-1-child.html',
overrides: ['/dynamic-1-child'],
headers: { vary: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch' },
});
expect(
getRouteInfo(prerenderedFunctions, 'nested/dynamic-2-child.func'),
).toEqual({
path: '/nested/dynamic-2-child.html',
overrides: ['/nested/dynamic-2-child'],
headers: { vary: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch' },
});

expect([...ignoredFunctions.keys()]).toEqual([
resolve(functionsDir, '[dynamic-1].func'),
resolve(functionsDir, '[dynamic-1].rsc.func'),
resolve(functionsDir, 'nested/[dynamic-2].func'),
]);

mockedConsoleWarn.restore();
});

test('should not ignore dynamic isr routes when there are no prerendered children', async () => {
const processExitMock = vi
.spyOn(process, 'exit')
.mockImplementation(async () => undefined as never);
const mockedConsoleWarn = mockConsole('warn');
const mockedConsoleError = mockConsole('error');

const { collectedFunctions, restoreFsMock } = await collectFunctionsFrom({
functions: {
'[dynamic-1].func': prerenderFuncDir,
'edge-route.func': edgeFuncDir,
},
});

const opts = {
functionsDir,
outputDir: resolve('.vercel/output/static'),
vercelConfig: { version: 3 as const },
};

await processEdgeFunctions(collectedFunctions);
await processPrerenderFunctions(collectedFunctions, opts);
await checkInvalidFunctions(collectedFunctions, opts);
restoreFsMock();

const {
edgeFunctions,
prerenderedFunctions,
invalidFunctions,
ignoredFunctions,
} = collectedFunctions;

expect(edgeFunctions.size).toEqual(1);
expect(prerenderedFunctions.size).toEqual(0);
expect(invalidFunctions.size).toEqual(1);
expect(ignoredFunctions.size).toEqual(0);

expect(getRouteInfo(edgeFunctions, 'edge-route.func')).toEqual({
path: '/edge-route',
overrides: [],
});

expect([...invalidFunctions.keys()]).toEqual([
resolve(functionsDir, '[dynamic-1].func'),
]);

expect(processExitMock).toHaveBeenCalledWith(1);
mockedConsoleError.expectCalls([
/The following routes were not configured to run with the Edge Runtime(?:.|\n)+- \/\[dynamic-1\]/,
]);

processExitMock.mockRestore();
mockedConsoleError.restore();
mockedConsoleWarn.restore();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('processPrerenderFunctions', () => {
'favicon.ico.func': prerenderFuncDir,
'favicon.ico.prerender-config.json': mockPrerenderConfigFile(
'favicon.ico',
'body',
{ ext: 'body' },
),
'favicon.ico.prerender-fallback.body': 'favicon.ico',
},
Expand Down Expand Up @@ -123,7 +123,7 @@ describe('processPrerenderFunctions', () => {
'data.json.func': prerenderFuncDir,
'data.json.prerender-config.json': mockPrerenderConfigFile(
'data.json',
'json',
{ ext: 'json' },
),
'data.json.prerender-fallback.json': 'data.json',
},
Expand Down

0 comments on commit 2527917

Please sign in to comment.