Skip to content

Commit f6f7300

Browse files
Support Next.js in autoconfig
1 parent 9eaa9e2 commit f6f7300

File tree

12 files changed

+284
-8
lines changed

12 files changed

+284
-8
lines changed

.changeset/fair-words-repair.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-cloudflare": patch
3+
---
4+
5+
Support Next.js in `--experimental` mode

.changeset/yellow-taxes-spend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Support Next.js projects in autoconfig

packages/create-cloudflare/e2e/tests/cli/cli.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ describe("Create Cloudflare CLI", () => {
555555
npm create cloudflare -- --framework next -- --ts
556556
pnpm create cloudflare --framework next -- --ts
557557
Allowed Values:
558-
gatsby, svelte, docusaurus, astro, tanstack-start
558+
gatsby, svelte, docusaurus, astro, tanstack-start, next
559559
--platform=<value>
560560
Whether the application should be deployed to Pages or Workers. This is only applicable for Frameworks templates that support both Pages and Workers.
561561
Allowed Values:

packages/create-cloudflare/e2e/tests/frameworks/test-config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,25 @@ function getExperimentalFrameworkTestConfig(
745745
nodeCompat: true,
746746
verifyTypes: false,
747747
},
748+
{
749+
name: "next",
750+
argv: ["--platform", "workers"],
751+
flags: ["--yes"],
752+
testCommitMessage: true,
753+
unsupportedOSs: ["win32"],
754+
unsupportedPms: ["npm", "yarn"],
755+
verifyDeploy: {
756+
route: "/",
757+
expectedText: "Generated by create next app",
758+
},
759+
verifyPreview: {
760+
previewArgs: ["--inspector-port=0"],
761+
route: "/",
762+
expectedText: "Generated by create next app",
763+
},
764+
nodeCompat: true,
765+
verifyTypes: false,
766+
},
748767
];
749768
}
750769

packages/create-cloudflare/src/templates.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import workflowsTemplate from "templates/hello-world-workflows/c3";
3232
import helloWorldWorkerTemplate from "templates/hello-world/c3";
3333
import honoTemplate from "templates/hono/c3";
3434
import nextTemplate from "templates/next/c3";
35+
import nextExperimentalTemplate from "templates/next/experimental_c3";
3536
import nuxtTemplate from "templates/nuxt/c3";
3637
import openapiTemplate from "templates/openapi/c3";
3738
import preExistingTemplate from "templates/pre-existing/c3";
@@ -241,6 +242,7 @@ export function getFrameworkMap({ experimental = false }): TemplateMap {
241242
docusaurus: docusaurusTemplate,
242243
astro: astroTemplate,
243244
"tanstack-start": tanStackStartTemplate,
245+
next: nextExperimentalTemplate,
244246
};
245247
} else {
246248
return {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { runFrameworkGenerator } from "frameworks/index";
2+
import type { TemplateConfig } from "../../src/templates";
3+
import type { C3Context } from "types";
4+
5+
const generate = async (ctx: C3Context) => {
6+
await runFrameworkGenerator(ctx, [
7+
ctx.project.name,
8+
"--skip-install",
9+
]);
10+
};
11+
12+
const envInterfaceName = "CloudflareEnv";
13+
const typesPath = "./cloudflare-env.d.ts";
14+
export default {
15+
configVersion: 1,
16+
id: "next",
17+
frameworkCli: "create-next-app",
18+
platform: "workers",
19+
displayName: "Next.js",
20+
generate,
21+
devScript: "dev",
22+
previewScript: "preview",
23+
deployScript: "deploy",
24+
typesPath,
25+
envInterfaceName,
26+
} as TemplateConfig;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* This is used to provide telemetry with a sanitised error
3+
* message that could not have any user-identifying information.
4+
* Set to `true` to duplicate `message`.
5+
* */
6+
type TelemetryMessage = {
7+
telemetryMessage?: string | true;
8+
};
9+
10+
/**
11+
* Base class for errors where something in a autoconfig frameworks' configuration goes
12+
* something wrong. These are not reported to Sentry.
13+
*/
14+
export class AutoConfigFrameworkConfigurationError extends Error {
15+
telemetryMessage: string | undefined;
16+
constructor(
17+
message?: string | undefined,
18+
options?:
19+
| ({
20+
cause?: unknown;
21+
} & TelemetryMessage)
22+
| undefined
23+
) {
24+
super(message, options);
25+
// Restore prototype chain:
26+
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
27+
Object.setPrototypeOf(this, new.target.prototype);
28+
this.telemetryMessage =
29+
options?.telemetryMessage === true ? message : options?.telemetryMessage;
30+
}
31+
}

packages/wrangler/src/autoconfig/frameworks/get-framework.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Astro } from "./astro";
2+
import { NextJs } from "./next";
23
import { Static } from "./static";
34
import { SvelteKit } from "./sveltekit";
45
import { TanstackStart } from "./tanstack";
@@ -15,6 +16,8 @@ export function getFramework(detectedFramework?: {
1516
return new SvelteKit(detectedFramework.name);
1617
case "tanstack-start":
1718
return new TanstackStart(detectedFramework.name);
19+
case "next":
20+
return new NextJs(detectedFramework.name);
1821
default:
1922
return new Static(detectedFramework?.name);
2023
}

packages/wrangler/src/autoconfig/frameworks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type ConfigurationOptions = {
99

1010
export type ConfigurationResults = {
1111
wranglerConfig: RawConfig;
12+
buildCommand?: string;
1213
};
1314

1415
export abstract class Framework {
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { statSync } from "node:fs";
2+
import { readFile, writeFile } from "node:fs/promises";
3+
import { brandColor, dim } from "@cloudflare/cli/colors";
4+
import { spinner } from "@cloudflare/cli/interactive";
5+
import { getPackageManager } from "../../package-manager";
6+
import { dedent } from "../../utils/dedent";
7+
import { installPackages } from "../c3-vendor/packages";
8+
import { AutoConfigFrameworkConfigurationError } from "../errors";
9+
import { appendToGitIgnore } from "../git";
10+
import { usesTypescript } from "../uses-typescript";
11+
import { Framework } from ".";
12+
import type { ConfigurationOptions, ConfigurationResults } from ".";
13+
14+
export class NextJs extends Framework {
15+
preview = "opennextjs-cloudflare build && opennextjs-cloudflare preview";
16+
deploy = "opennextjs-cloudflare build && opennextjs-cloudflare deploy";
17+
18+
async configure({
19+
dryRun,
20+
projectPath,
21+
}: ConfigurationOptions): Promise<ConfigurationResults> {
22+
const usesTs = usesTypescript(projectPath);
23+
24+
const nextConfigPath = probeForNextConfigPath(usesTs);
25+
if (!nextConfigPath) {
26+
throw new AutoConfigFrameworkConfigurationError(
27+
"No Next.js configuration file could be detected. Note: only next.config.ts and next.config.mjs files are supported."
28+
);
29+
}
30+
31+
if (!dryRun) {
32+
await installPackages(["@opennextjs/cloudflare@^1.12.0"], {
33+
startText: "Installing @opennextjs/cloudflare adapter",
34+
doneText: `${brandColor("installed")}`,
35+
});
36+
37+
await updateNextConfig(nextConfigPath);
38+
39+
await createOpenNextConfigFile(projectPath);
40+
41+
await appendToGitIgnore(
42+
projectPath,
43+
dedent`
44+
# OpenNext
45+
.open-next
46+
`,
47+
{
48+
startText: "Adding open-next section to .gitignore file",
49+
doneText: `${brandColor(`added`)} open-next section to .gitignore file`,
50+
}
51+
);
52+
}
53+
54+
const { npx } = await getPackageManager();
55+
56+
return {
57+
wranglerConfig: {
58+
main: ".open-next/worker.js",
59+
compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"],
60+
assets: {
61+
binding: "ASSETS",
62+
directory: ".open-next/assets",
63+
},
64+
},
65+
buildCommand: `${npx} @opennextjs/cloudflare build`,
66+
};
67+
}
68+
69+
configurationDescription = "Configuring project for Next.js with OpenNext";
70+
}
71+
72+
function probeForNextConfigPath(usesTs: boolean): string | undefined {
73+
const pathsToProbe = [
74+
...(usesTs ? ["next.config.ts"] : []),
75+
"next.config.mjs",
76+
];
77+
78+
for (const path of pathsToProbe) {
79+
const stats = statSync(path, {
80+
throwIfNoEntry: false,
81+
});
82+
if (stats?.isFile()) {
83+
return path;
84+
}
85+
}
86+
}
87+
88+
async function updateNextConfig(nextConfigPath: string) {
89+
const s = spinner();
90+
91+
s.start(`Updating \`${nextConfigPath}\``);
92+
93+
const configContent = await readFile(nextConfigPath);
94+
95+
const updatedConfigFile =
96+
configContent +
97+
`
98+
// added by create cloudflare to enable calling \`getCloudflareContext()\` in \`next dev\`
99+
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
100+
initOpenNextCloudflareForDev();
101+
`.replace(/\n\t*/g, "\n");
102+
103+
await writeFile(nextConfigPath, updatedConfigFile);
104+
105+
s.stop(`${brandColor(`updated`)} ${dim(`\`${nextConfigPath}\``)}`);
106+
}
107+
108+
async function createOpenNextConfigFile(projectPath: string) {
109+
const s = spinner();
110+
111+
s.start("Creating open-next.config.ts file");
112+
113+
await writeFile(
114+
// TODO: this always saves the file as open-next.config.ts, is a js alternative also supported?
115+
// (since the project might not be using TypeScript)
116+
`${projectPath}/open-next.config.ts`,
117+
dedent`import { defineCloudflareConfig } from "@opennextjs/cloudflare";
118+
119+
export default defineCloudflareConfig({
120+
// Uncomment to enable R2 cache,
121+
// It should be imported as:
122+
// \`import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";\`
123+
// See https://opennext.js.org/cloudflare/caching for more details
124+
// incrementalCache: r2IncrementalCache,
125+
});
126+
`
127+
);
128+
129+
s.stop(`${brandColor("created")} open-next.config.ts file`);
130+
}

0 commit comments

Comments
 (0)