Skip to content

Commit 20d1242

Browse files
Support Next.js in autoconfig
1 parent b1d0f1b commit 20d1242

File tree

11 files changed

+280
-5
lines changed

11 files changed

+280
-5
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
@@ -487,7 +487,7 @@ describe("Create Cloudflare CLI", () => {
487487
npm create cloudflare -- --framework next -- --ts
488488
pnpm create cloudflare --framework next -- --ts
489489
Allowed Values:
490-
gatsby, svelte
490+
gatsby, svelte, next
491491
--platform=<value>
492492
Whether the application should be deployed to Pages or Workers. This is only applicable for Frameworks templates that support both Pages and Workers.
493493
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
@@ -659,6 +659,25 @@ function getExperimentalFrameworkTestConfig(): NamedFrameworkTestConfig[] {
659659
nodeCompat: false,
660660
verifyTypes: false,
661661
},
662+
{
663+
name: "next",
664+
argv: ["--platform", "workers"],
665+
flags: ["--yes"],
666+
testCommitMessage: true,
667+
unsupportedOSs: ["win32"],
668+
unsupportedPms: ["npm", "yarn"],
669+
verifyDeploy: {
670+
route: "/",
671+
expectedText: "Generated by create next app",
672+
},
673+
verifyPreview: {
674+
previewArgs: ["--inspector-port=0"],
675+
route: "/",
676+
expectedText: "Generated by create next app",
677+
},
678+
nodeCompat: true,
679+
verifyTypes: false,
680+
},
662681
];
663682
}
664683

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";
@@ -190,6 +191,7 @@ export function getFrameworkMap({ experimental = false }): TemplateMap {
190191
return {
191192
gatsby: gatsbyTemplate,
192193
svelte: svelteTemplate,
194+
next: nextExperimentalTemplate,
193195
};
194196
} else {
195197
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: 4 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

@@ -9,6 +10,9 @@ export function getFramework(id: string) {
910
if (id === "svelte-kit") {
1011
return new SvelteKit();
1112
}
13+
if (id === "next") {
14+
return new NextJs();
15+
}
1216

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

0 commit comments

Comments
 (0)