create-cloudflare
Version:
A CLI for creating and deploying new applications to Cloudflare.
222 lines (187 loc) • 6.1 kB
text/typescript
import { join } from "path";
import { updateStatus, warn } from "@cloudflare/cli";
import { brandColor, dim } from "@cloudflare/cli/colors";
import { inputPrompt, spinner } from "@cloudflare/cli/interactive";
import { runFrameworkGenerator } from "frameworks/index";
import {
copyFile,
probePaths,
readFile,
readJSON,
usesEslint,
usesTypescript,
writeFile,
writeJSON,
} from "helpers/files";
import { detectPackageManager } from "helpers/packageManagers";
import { installPackages } from "helpers/packages";
import { getTemplatePath } from "../../../src/templates";
import type { TemplateConfig } from "../../../src/templates";
import type { C3Context } from "types";
const { npm, npx } = detectPackageManager();
const generate = async (ctx: C3Context) => {
const projectName = ctx.project.name;
await runFrameworkGenerator(ctx, [projectName]);
const wranglerConfig = readFile(join(getTemplatePath(ctx), "wrangler.jsonc"));
writeFile(join(ctx.project.path, "wrangler.jsonc"), wranglerConfig);
updateStatus("Created wrangler.jsonc file");
};
const updateNextConfig = (usesTs: boolean) => {
const s = spinner();
const configFile = `next.config.${usesTs ? "ts" : "mjs"}`;
s.start(`Updating \`${configFile}\``);
const configContent = readFile(configFile);
const updatedConfigFile =
`import { setupDevPlatform } from '@cloudflare/next-on-pages/next-dev';
// Here we use the @cloudflare/next-on-pages next-dev module to allow us to
// use bindings during local development (when running the application with
// \`next dev\`). This function is only necessary during development and
// has no impact outside of that. For more information see:
// https://github.com/cloudflare/next-on-pages/blob/main/internal-packages/next-dev/README.md
setupDevPlatform().catch(console.error);
`.replace(/\n\t*/g, "\n") + configContent;
writeFile(configFile, updatedConfigFile);
s.stop(`${brandColor(`updated`)} ${dim(`\`${configFile}\``)}`);
};
const configure = async (ctx: C3Context) => {
const projectPath = ctx.project.path;
// Add a compatible function handler example
const path = probePaths([
`${projectPath}/pages/api`,
`${projectPath}/src/pages/api`,
`${projectPath}/src/app/api`,
`${projectPath}/app/api`,
`${projectPath}/src/app`,
`${projectPath}/app`,
]);
if (!path) {
throw new Error("Could not find the `/api` or `/app` directory");
}
const usesTs = usesTypescript(ctx);
const installEslintPlugin = await shouldInstallNextOnPagesEslintPlugin(ctx);
if (installEslintPlugin) {
await writeEslintrc(ctx);
}
updateNextConfig(usesTs);
copyFile(
join(getTemplatePath(ctx), "README.md"),
join(projectPath, "README.md"),
);
updateStatus("Updated the README file");
await addDevDependencies(installEslintPlugin);
};
export const shouldInstallNextOnPagesEslintPlugin = async (
ctx: C3Context,
): Promise<boolean> => {
const eslintUsage = usesEslint(ctx);
if (!eslintUsage.used) {
return false;
}
if (eslintUsage.configType !== ".eslintrc.json") {
warn(
`Expected .eslintrc.json from Next.js scaffolding but found ${eslintUsage.configType} instead`,
);
return false;
}
return await inputPrompt({
type: "confirm",
question: "Do you want to use the next-on-pages eslint-plugin?",
label: "eslint-plugin",
defaultValue: true,
});
};
export const writeEslintrc = async (ctx: C3Context): Promise<void> => {
const eslintConfig = readJSON(`${ctx.project.path}/.eslintrc.json`) as {
plugins: string[];
extends: string | string[];
};
eslintConfig.plugins ??= [];
eslintConfig.plugins.push("eslint-plugin-next-on-pages");
if (typeof eslintConfig.extends === "string") {
eslintConfig.extends = [eslintConfig.extends];
}
eslintConfig.extends ??= [];
eslintConfig.extends.push("plugin:eslint-plugin-next-on-pages/recommended");
writeJSON(`${ctx.project.path}/.eslintrc.json`, eslintConfig);
};
const addDevDependencies = async (installEslintPlugin: boolean) => {
const packages = [
"@cloudflare/next-on-pages@1",
"vercel",
...(installEslintPlugin ? ["eslint-plugin-next-on-pages"] : []),
];
await installPackages(packages, {
dev: true,
startText: "Adding the Cloudflare Pages adapter",
doneText: `${brandColor(`installed`)} ${dim(packages.join(", "))}`,
});
};
const envInterfaceName = "CloudflareEnv";
const typesPath = "./env.d.ts";
export default {
configVersion: 1,
id: "next",
frameworkCli: "create-next-app",
platform: "pages",
hidden: true,
displayName: "Next.js",
path: "templates/next/pages",
generate,
configure,
copyFiles: {
async selectVariant(ctx) {
const isApp = probePaths([
`${ctx.project.path}/src/app`,
`${ctx.project.path}/app`,
]);
const isTypescript = usesTypescript(ctx);
const dir = isApp ? "app" : "pages";
return `${dir}/${isTypescript ? "ts" : "js"}`;
},
destinationDir(ctx) {
const srcPath = probePaths([`${ctx.project.path}/src`]);
return srcPath ? "./src" : "./";
},
variants: {
"app/ts": {
path: "./app/ts",
},
"app/js": {
path: "./app/js",
},
"pages/ts": {
path: "./pages/ts",
},
"pages/js": {
path: "./pages/js",
},
},
},
transformPackageJson: async (_, ctx) => {
const isNpm = npm === "npm";
const isBun = npm === "bun";
const isNpmOrBun = isNpm || isBun;
const nextOnPagesScope = isNpmOrBun ? "@cloudflare/" : "";
const nextOnPagesCommand = `${nextOnPagesScope}next-on-pages`;
const pmCommand = isNpmOrBun ? npx : npm;
const pagesBuildRunCommand = `${
isNpm ? "npm run" : isBun ? "bun" : pmCommand
} pages:build`;
return {
scripts: {
"pages:build": `${pmCommand} ${nextOnPagesCommand}`,
preview: `${pagesBuildRunCommand} && wrangler pages dev`,
deploy: `${pagesBuildRunCommand} && wrangler pages deploy`,
...(usesTypescript(ctx) && {
"cf-typegen": `wrangler types --env-interface ${envInterfaceName} ${typesPath}`,
}),
},
};
},
devScript: "dev",
previewScript: "preview",
deployScript: "deploy",
compatibilityFlags: ["nodejs_compat"],
typesPath,
envInterfaceName,
} as TemplateConfig;