Skip to content

Commit

Permalink
fix: prerendered favicon handling (#238)
Browse files Browse the repository at this point in the history
* fix: prerendered favicon handling

* dumb typo

* chore: changeset
  • Loading branch information
james-elicx authored May 12, 2023
1 parent c70151d commit e2d2046
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/tame-feet-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudflare/next-on-pages': patch
---

Fix the prerendered route handling for favicons.
9 changes: 8 additions & 1 deletion src/buildApplication/fixPrerenderedRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ async function getRoutePath(
/**
* Retrieves the new destination for the prerendered file, if no file already exists.
*
* @example
* ```ts
* // index.prerender-fallback.html -> index.html
* // index.rsc.prerender-fallback.rsc -> index.rsc
* // favicon.ico.prerender-fallback.body -> favicon.ico
* ```
*
* @param config.fallback Fallback file configuration.
* @param dirName Directory name to use for the route.
* @param outputDir Vercel build output directory.
Expand All @@ -91,7 +98,7 @@ async function getRouteDest(
const destRoute = normalizePath(
join(
dirName,
fallback.fsPath.replace(/(?:\.rsc)?\.prerender-fallback/gi, '')
fallback.fsPath.replace(/\.prerender-fallback(?:\.rsc)?(?:\.body)?/gi, '')
)
);
const destFile = join(outputDir, 'static', destRoute);
Expand Down
49 changes: 2 additions & 47 deletions src/buildApplication/generateFunctionsMap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readFile, writeFile, mkdir, rm, readdir, copyFile } from 'fs/promises';
import { readFile, writeFile, mkdir, readdir } from 'fs/promises';
import { exit } from 'process';
import { dirname, join, relative, resolve } from 'path';
import type { Node } from 'acorn';
Expand Down Expand Up @@ -143,7 +143,7 @@ async function processDirectoryRecursively(
const filepath = join(dir, file);
if (await validateDir(filepath)) {
const dirResultsPromise = file.endsWith('.func')
? processFuncDirectory(setup, file, filepath)
? processFuncDirectory(setup, filepath)
: processDirectoryRecursively(setup, filepath);
const dirResults = await dirResultsPromise;
dirResults.invalidFunctions?.forEach(fn => invalidFunctions.add(fn));
Expand Down Expand Up @@ -181,7 +181,6 @@ type FunctionConfig = {

async function processFuncDirectory(
setup: ProcessingSetup,
file: string,
filepath: string
): Promise<Partial<DirectoryProcessingResults>> {
const relativePath = relative(setup.functionsDir, filepath);
Expand All @@ -191,10 +190,6 @@ async function processFuncDirectory(
);

if (functionConfig?.runtime !== 'edge') {
if (file === 'favicon.ico.func') {
await tryToFixFaviconFunc();
return {};
}
return {
invalidFunctions: new Set([relativePath]),
};
Expand Down Expand Up @@ -422,46 +417,6 @@ async function buildWebpackChunkFiles(
}
}

/**
* Next.js generates a favicon nodejs function instead of providing the favicon as
* a standard static asset. Since it is not using the edge runtime we can't use the
* function itself, so this function tries to see if it can find the standard favicon
* in the .vercel/output/static/static/media/metadata directory generated by Next.js
* and moves that one in the static output directory instead.
*/
async function tryToFixFaviconFunc(): Promise<void> {
try {
const staticMediaMetadata = resolve(
'.vercel',
'output',
'static',
'static',
'media',
'metadata'
);
const files = await readdir(staticMediaMetadata);
const favicon = files.find(file =>
/^favicon\.[a-zA-Z0-9]+\.ico$/.test(file)
);
if (favicon) {
const faviconFilePath = join(staticMediaMetadata, favicon);
const vercelStaticFavicon = resolve(
'.vercel',
'output',
'static',
'favicon.ico'
);
await copyFile(faviconFilePath, vercelStaticFavicon);
}
// let's delete the .vercel/output/static/static directory so that extra media
// files are not uploaded unnecessarily to Cloudflare Pages
const staticStaticDir = resolve('.vercel', 'output', 'static', 'static');
await rm(staticStaticDir, { recursive: true, force: true });
} catch {
cliWarn('Warning: No static favicon file found');
}
}

type ProcessingSetup = {
functionsDir: string;
distFunctionsDir: string;
Expand Down
10 changes: 6 additions & 4 deletions tests/_helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,13 @@ export function createInvalidFuncDir(data: string) {
* 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.
* @returns The stringified prerender config file contents.
*/
export function mockPrerenderConfigFile(path: string): string {
const fsPath = path.endsWith('.rsc')
? `${path}.prerender-fallback.rsc`
: `${path}.prerender-fallback.html`;
export function mockPrerenderConfigFile(path: string, ext?: string): string {
const extension = ext || (path.endsWith('.rsc') ? 'rsc' : 'html');
const fsPath = `${path}.prerender-fallback.${extension}`;

const config: VercelPrerenderConfig = {
type: 'Prerender',
fallback: {
Expand All @@ -270,6 +271,7 @@ export function mockPrerenderConfigFile(path: string): string {
},
initialHeaders: {
...(path.endsWith('.rsc') && { 'content-type': 'text/x-component' }),
...(path.endsWith('.ico') && { 'content-type': 'image/x-icon' }),
vary: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch',
},
};
Expand Down
27 changes: 27 additions & 0 deletions tests/src/buildApplication/generateFunctionsMap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,33 @@ describe('generateFunctionsMap', async () => {
});
});

test('succeeds for prerendered favicon', async () => {
const { functionsMap, prerenderedRoutes } =
await generateFunctionsMapFrom({
'page.func': validFuncDir,
'page.rsc.func': validFuncDir,
'favicon.ico.func': invalidFuncDir,
'favicon.ico.prerender-config.json': mockPrerenderConfigFile(
'favicon.ico',
'body'
),
'favicon.ico.prerender-fallback.body': 'favicon.ico',
});

expect(functionsMap.size).toEqual(2);
expect(functionsMap.get('/page')).toMatch(/\/page\.func\.js$/);
expect(functionsMap.get('/page.rsc')).toMatch(/\/page\.rsc\.func\.js$/);

expect(prerenderedRoutes.size).toEqual(1);
expect(prerenderedRoutes.get('/favicon.ico')).toEqual({
headers: {
'content-type': 'image/x-icon',
vary: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch',
},
overrides: [],
});
});

test('succeeds for nested prerendered routes', async () => {
const { functionsMap, prerenderedRoutes } =
await generateFunctionsMapFrom({
Expand Down

0 comments on commit e2d2046

Please sign in to comment.