Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a custom not-found edge route for next applications using the app router #4625

Merged
merged 6 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-bulldogs-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-cloudflare": minor
---

Create a custom not-found edge route for Next.js applications using the app router
2 changes: 1 addition & 1 deletion packages/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const stripAnsi = (str: string) => {
return str.replace(regex, "");
};

export const crash = (msg?: string): never => {
export const crash: (msg?: string) => never = (msg) => {
if (msg) {
process.stderr.write(red(msg));
process.stderr.write("\n");
Expand Down
4 changes: 2 additions & 2 deletions packages/create-cloudflare/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ export const runCli = async (args: Partial<C3Args>) => {
});

if (!type) {
return crash("An application type must be specified to continue.");
crash("An application type must be specified to continue.");
}

if (!Object.keys(templateMap).includes(type)) {
return crash(`Unknown application type provided: ${type}.`);
crash(`Unknown application type provided: ${type}.`);
}

const validatedArgs: C3Args = {
Expand Down
2 changes: 1 addition & 1 deletion packages/create-cloudflare/src/frameworks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const getFrameworkCli = (
withVersion = true
) => {
if (!ctx.framework) {
return crash("Framework not specified.");
crash("Framework not specified.");
}

const framework = ctx.framework
Expand Down
53 changes: 35 additions & 18 deletions packages/create-cloudflare/src/frameworks/next/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mkdirSync } from "fs";
import { updateStatus, warn } from "@cloudflare/cli";
import { existsSync, mkdirSync } from "fs";
import { crash, updateStatus, warn } from "@cloudflare/cli";
import { brandColor, dim } from "@cloudflare/cli/colors";
import { processArgument } from "helpers/args";
import { installPackages, runFrameworkGenerator } from "helpers/command";
Expand All @@ -18,6 +18,8 @@ import {
apiAppDirHelloTs,
apiPagesDirHelloJs,
apiPagesDirHelloTs,
appDirNotFoundJs,
appDirNotFoundTs,
} from "./templates";
import type { C3Args, FrameworkConfig, PagesGeneratorContext } from "types";

Expand Down Expand Up @@ -53,26 +55,41 @@ const getApiTemplate = (
const configure = async (ctx: PagesGeneratorContext) => {
const projectName = ctx.project.name;

// Add a compatible function handler example
const path = probePaths(
[
`${projectName}/pages/api`,
`${projectName}/src/pages/api`,
`${projectName}/src/app/api`,
`${projectName}/app/api`,
`${projectName}/src/app`,
`${projectName}/app`,
],
"Could not find the `/api` or `/app` directory"
);
const path = probePaths([
`${projectName}/pages/api`,
`${projectName}/src/pages/api`,
`${projectName}/src/app/api`,
`${projectName}/app/api`,
`${projectName}/src/app`,
`${projectName}/app`,
]);

if (!path) {
crash("Could not find the `/api` or `/app` directory");
}

// App directory template may not generate an API route handler, so we update the path to add an `api` directory.
const apiPath = path.replace(/\/app$/, "/app/api");

const [handlerPath, handlerFile] = getApiTemplate(
apiPath,
usesTypescript(projectName)
);
const usesTs = usesTypescript(projectName);

const appDirPath = probePaths([
`${projectName}/src/app`,
`${projectName}/app`,
]);
if (appDirPath) {
// Add a custom app not-found edge route as recommended in next-on-pages
// (see: https://github.com/cloudflare/next-on-pages/blob/2b5c8f25/packages/next-on-pages/docs/gotchas.md#not-found)
const notFoundPath = `${appDirPath}/not-found.${usesTs ? "tsx" : "js"}`;
if (!existsSync(notFoundPath)) {
const notFoundContent = usesTs ? appDirNotFoundTs : appDirNotFoundJs;
writeFile(notFoundPath, notFoundContent);
updateStatus("Created a custom edge not-found route");
}
}

// Add a compatible function handler example
const [handlerPath, handlerFile] = getApiTemplate(apiPath, usesTs);
writeFile(handlerPath, handlerFile);
updateStatus("Created an example API route handler");

Expand Down
124 changes: 124 additions & 0 deletions packages/create-cloudflare/src/frameworks/next/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,127 @@ export async function GET(request) {
return new Response(JSON.stringify({ name: 'John Doe' }))
}
`;

// Simplified and adjusted version of the Next.js built-in not-found component (https://github.com/vercel/next.js/blob/1c65c5575/packages/next/src/client/components/not-found-error.tsx)
export const appDirNotFoundJs = `
export const runtime = "edge";

export default function NotFound() {
return (
<>
<title>404: This page could not be found.</title>
<div style={styles.error}>
<div>
<style
dangerouslySetInnerHTML={{
__html: \`body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\`,
}}
/>
<h1 className="next-error-h1" style={styles.h1}>
404
</h1>
<div style={styles.desc}>
<h2 style={styles.h2}>This page could not be found.</h2>
</div>
</div>
</div>
</>
);
}

const styles = {
error: {
fontFamily:
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
height: "100vh",
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},

desc: {
display: "inline-block",
},

h1: {
display: "inline-block",
margin: "0 20px 0 0",
padding: "0 23px 0 0",
fontSize: 24,
fontWeight: 500,
verticalAlign: "top",
lineHeight: "49px",
},

h2: {
fontSize: 14,
fontWeight: 400,
lineHeight: "49px",
margin: 0,
},
};
`;

// Simplified and adjusted version of the Next.js built-in not-found component (https://github.com/vercel/next.js/blob/1c65c5575/packages/next/src/client/components/not-found-error.tsx)
export const appDirNotFoundTs = `
export const runtime = "edge";

export default function NotFound() {
return (
<>
<title>404: This page could not be found.</title>
<div style={styles.error}>
<div>
<style
dangerouslySetInnerHTML={{
__html: \`body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\`,
}}
/>
<h1 className="next-error-h1" style={styles.h1}>
404
</h1>
<div style={styles.desc}>
<h2 style={styles.h2}>This page could not be found.</h2>
</div>
</div>
</div>
</>
);
}

const styles = {
error: {
fontFamily:
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
height: "100vh",
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},

desc: {
display: "inline-block",
},

h1: {
display: "inline-block",
margin: "0 20px 0 0",
padding: "0 23px 0 0",
fontSize: 24,
fontWeight: 500,
verticalAlign: "top",
lineHeight: "49px",
},

h2: {
fontSize: 14,
fontWeight: 400,
lineHeight: "49px",
margin: 0,
},
} as const;
`;
11 changes: 3 additions & 8 deletions packages/create-cloudflare/src/helpers/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,15 @@ export const writeJSON = (path: string, object: object, stringifySpace = 2) => {
writeFile(path, JSON.stringify(object, null, stringifySpace));
};

// Probes a list of paths and returns the first one that exists
// If one isn't found, throws an error with the given message
export const probePaths = (
paths: string[],
errorMsg = "Failed to find required file."
) => {
// Probes a list of paths and returns the first one that exists or null if none does
export const probePaths = (paths: string[]) => {
for (const path of paths) {
if (existsSync(path)) {
return path;
}
}

crash(errorMsg);
process.exit(1); // hack to make typescript happy
dario-piotrowicz marked this conversation as resolved.
Show resolved Hide resolved
return null;
};

export const usesTypescript = (projectRoot = ".") => {
Expand Down
Loading