Skip to content

Commit c3ff1c2

Browse files
C3: offer eslint-plugin-next-on-pages (#3604)
Add the option to add the eslint-plugin-next-on-pages eslint plugin to developers creating a new Next.js app with eslint enabled
1 parent 99032c1 commit c3ff1c2

File tree

4 files changed

+138
-12
lines changed

4 files changed

+138
-12
lines changed

.changeset/honest-ghosts-switch.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"create-cloudflare": minor
3+
---
4+
5+
Add the option to add the `eslint-plugin-next-on-pages` eslint plugin
6+
to developers creating a new Next.js app with eslint enabled

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

+14-6
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,9 @@ describe("E2E: Web frameworks", () => {
8181
});
8282
};
8383

84-
test.each(["astro", "hono", "next", "react", "remix", "vue"])(
85-
"%s",
86-
async (name) => {
87-
await runCli(name, {});
88-
}
89-
);
84+
test.each(["astro", "hono", "react", "remix", "vue"])("%s", async (name) => {
85+
await runCli(name, {});
86+
});
9087

9188
test("Nuxt", async () => {
9289
await runCli("nuxt", {
@@ -98,6 +95,17 @@ describe("E2E: Web frameworks", () => {
9895
});
9996
});
10097

98+
test("next", async () => {
99+
await runCli("next", {
100+
promptHandlers: [
101+
{
102+
matcher: /Do you want to use the next-on-pages eslint-plugin\?/,
103+
input: ["y"],
104+
},
105+
],
106+
});
107+
});
108+
101109
test("qwik", async () => {
102110
await runCli("qwik", {
103111
promptHandlers: [

packages/create-cloudflare/src/frameworks/next/index.ts

+61-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { mkdirSync } from "fs";
2-
import { updateStatus } from "helpers/cli";
2+
import { updateStatus, warn } from "helpers/cli";
33
import { brandColor, dim } from "helpers/colors";
44
import { installPackages, runFrameworkGenerator } from "helpers/command";
5-
import { probePaths, usesTypescript, writeFile } from "helpers/files";
5+
import {
6+
probePaths,
7+
readJSON,
8+
usesEslint,
9+
usesTypescript,
10+
writeFile,
11+
writeJSON,
12+
} from "helpers/files";
13+
import { processArgument } from "helpers/interactive";
614
import { detectPackageManager } from "helpers/packages";
715
import { getFrameworkVersion } from "../index";
816
import {
@@ -11,7 +19,7 @@ import {
1119
apiPagesDirHelloJs,
1220
apiPagesDirHelloTs,
1321
} from "./templates";
14-
import type { PagesGeneratorContext, FrameworkConfig } from "types";
22+
import type { PagesGeneratorContext, FrameworkConfig, C3Args } from "types";
1523

1624
const { npm, npx, dlx } = detectPackageManager();
1725

@@ -72,16 +80,65 @@ const configure = async (ctx: PagesGeneratorContext) => {
7280
writeFile(handlerPath, handlerFile);
7381
updateStatus("Created an example API route handler");
7482

83+
const installEslintPlugin = await shouldInstallNextOnPagesEslintPlugin(ctx);
84+
85+
if (installEslintPlugin) {
86+
await writeEslintrc(ctx);
87+
}
88+
7589
// Add some dev dependencies
7690
process.chdir(projectName);
77-
const packages = ["@cloudflare/next-on-pages@1", "vercel"];
91+
const packages = [
92+
"@cloudflare/next-on-pages@1",
93+
"vercel",
94+
...(installEslintPlugin ? ["eslint-plugin-next-on-pages"] : []),
95+
];
7896
await installPackages(packages, {
7997
dev: true,
8098
startText: "Adding the Cloudflare Pages adapter",
8199
doneText: `${brandColor(`installed`)} ${dim(packages.join(", "))}`,
82100
});
83101
};
84102

103+
export const shouldInstallNextOnPagesEslintPlugin = async (
104+
ctx: PagesGeneratorContext
105+
): Promise<boolean> => {
106+
const eslintUsage = usesEslint(ctx);
107+
108+
if (!eslintUsage.used) return false;
109+
110+
if (eslintUsage.configType !== ".eslintrc.json") {
111+
warn(
112+
`Expected .eslintrc.json from Next.js scaffolding but found ${eslintUsage.configType} instead`
113+
);
114+
return false;
115+
}
116+
117+
return await processArgument(ctx.args, "eslint-plugin" as keyof C3Args, {
118+
type: "confirm",
119+
question: "Do you want to use the next-on-pages eslint-plugin?",
120+
label: "eslint-plugin",
121+
defaultValue: true,
122+
});
123+
};
124+
125+
export const writeEslintrc = async (
126+
ctx: PagesGeneratorContext
127+
): Promise<void> => {
128+
const eslintConfig = readJSON(`${ctx.project.name}/.eslintrc.json`);
129+
130+
eslintConfig.plugins ??= [];
131+
eslintConfig.plugins.push("eslint-plugin-next-on-pages");
132+
133+
if (typeof eslintConfig.extends === "string") {
134+
eslintConfig.extends = [eslintConfig.extends];
135+
}
136+
eslintConfig.extends ??= [];
137+
eslintConfig.extends.push("plugin:eslint-plugin-next-on-pages/recommended");
138+
139+
writeJSON(`${ctx.project.name}/.eslintrc.json`, eslintConfig, 2);
140+
};
141+
85142
const config: FrameworkConfig = {
86143
generate,
87144
configure,

packages/create-cloudflare/src/helpers/files.ts

+57-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs, { existsSync } from "fs";
22
import { crash } from "./cli";
3+
import type { PagesGeneratorContext } from "types";
34

45
export const writeFile = (path: string, content: string) => {
56
try {
@@ -22,8 +23,12 @@ export const readJSON = (path: string) => {
2223
return contents ? JSON.parse(contents) : contents;
2324
};
2425

25-
export const writeJSON = (path: string, object: object) => {
26-
writeFile(path, JSON.stringify(object));
26+
export const writeJSON = (
27+
path: string,
28+
object: object,
29+
stringifySpace?: number | string
30+
) => {
31+
writeFile(path, JSON.stringify(object, null, stringifySpace));
2732
};
2833

2934
// Probes a list of paths and returns the first one that exists
@@ -46,6 +51,56 @@ export const usesTypescript = (projectRoot = ".") => {
4651
return existsSync(`${projectRoot}/tsconfig.json`);
4752
};
4853

54+
const eslintRcExts = ["js", "cjs", "yaml", "yml", "json"] as const;
55+
56+
type EslintRcFileName = `.eslintrc.${typeof eslintRcExts[number]}`;
57+
58+
type EslintUsageInfo =
59+
| {
60+
used: true;
61+
configType: EslintRcFileName | "eslint.config.js" | "package.json";
62+
}
63+
| {
64+
used: false;
65+
};
66+
67+
/*
68+
checks if eslint is used and if so returns the configuration type
69+
(for the various configuration types see:
70+
- https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file-formats
71+
- https://eslint.org/docs/latest/use/configure/configuration-files-new )
72+
*/
73+
export const usesEslint = (ctx: PagesGeneratorContext): EslintUsageInfo => {
74+
for (const ext of eslintRcExts) {
75+
const eslintRcFilename = `.eslintrc.${ext}` as EslintRcFileName;
76+
if (existsSync(`${ctx.project.path}/${eslintRcFilename}`)) {
77+
return {
78+
used: true,
79+
configType: eslintRcFilename,
80+
};
81+
}
82+
}
83+
84+
if (existsSync(`${ctx.project.path}/eslint.config.js`)) {
85+
return {
86+
used: true,
87+
configType: "eslint.config.js",
88+
};
89+
}
90+
91+
try {
92+
const pkgJson = readJSON(`${ctx.project.path}/package.json`);
93+
if (pkgJson.eslintConfig) {
94+
return {
95+
used: true,
96+
configType: "package.json",
97+
};
98+
}
99+
} catch {}
100+
101+
return { used: false };
102+
};
103+
49104
// Generate a compatibility date flag
50105
export const compatDateFlag = () => {
51106
const date = new Date();

0 commit comments

Comments
 (0)