Skip to content

Commit

Permalink
C3: Fix name argument and infer type argument from flags (#3525)
Browse files Browse the repository at this point in the history
* Add back initialValue prop to interactive options

* Try to infer type argument if missing

* Adding changeset

* Add initialValue back to confirmInput and fix tests

* Refactor input prompts and argument validation

* Improve wrangler-defaults test

* Refactor --wrangler-defaults and add a -y flag

* Proposed tweak to `parseArgs()`

* Skip the hono e2e test

---------

Co-authored-by: Pete Bacon Darwin <[email protected]>
  • Loading branch information
jculvey and petebacondarwin authored Jul 7, 2023
1 parent 5a74cb5 commit 1ce3296
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 317 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-fans-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-cloudflare": patch
---

C3: Infer missing --type argument from --framework or --existing-script
31 changes: 25 additions & 6 deletions packages/create-cloudflare/e2e-tests/pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,32 @@ describe("E2E", () => {
}
});

const runCli = async (framework: string) => {
const runCli = async (framework: string, args: string[] = []) => {
const projectPath = join(dummyPath, "test");

const argv = [
projectPath,
"--type",
"webFramework",
"--framework",
framework,
"--no-deploy",
"--no-git",
"--wrangler-defaults",
];

const result = await execa("node", ["./dist/cli.js", ...argv], {
stderr: process.stderr,
});
if (args.length > 0) {
argv.push(...args);
} else {
argv.push("--no-git");
}

// For debugging purposes, uncomment the following to see the exact
// command the test uses. You can then run this via the command line.
// console.log("COMMAND: ", `node ${["./dist/cli.js", ...argv].join(" ")}`);

const result = await execa("node", ["./dist/cli.js", ...argv], {
stderr: process.stderr,
});

const { exitCode } = result;

// Some baseline assertions for each framework
Expand Down Expand Up @@ -116,4 +122,17 @@ describe("E2E", () => {
test("Vue", async () => {
await runCli("vue");
});

// This test blows up in CI due to Github providing an unusual git user email address.
// E.g.
// ```
// fatal: empty ident name (for <[email protected].
// internal.cloudapp.net>) not allowed
// ```
test.skip("Hono (wrangler defaults)", async () => {
const { projectPath } = await runCli("hono", ["--wrangler-defaults"]);

// verify that wrangler-defaults defaults to `true` for using git
expect(join(projectPath, ".git")).toExist();
});
});
124 changes: 67 additions & 57 deletions packages/create-cloudflare/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,80 @@
#!/usr/bin/env node
// import { TextPrompt, SelectPrompt, ConfirmPrompt } from "@clack/core";
import Haikunator from "haikunator";
import { crash, logRaw, startSection } from "helpers/cli";
import { dim, brandColor } from "helpers/colors";
import { selectInput, textInput } from "helpers/interactive";
import { dim } from "helpers/colors";
import { processArgument } from "helpers/interactive";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { version } from "../package.json";
import { validateProjectDirectory } from "./common";
import { runPagesGenerator } from "./pages";
import { runWorkersGenerator } from "./workers";
import type { Option } from "helpers/interactive";
import type { PagesGeneratorArgs } from "types";
import type { C3Args } from "types";

export const C3_DEFAULTS = {
projectName: new Haikunator().haikunate({ tokenHex: true }),
type: "hello-world",
framework: "angular",
deploy: true,
git: true,
open: true,
ts: true,
};

const WRANGLER_DEFAULTS = {
...C3_DEFAULTS,
deploy: false,
};

export const main = async (argv: string[]) => {
const args = await parseArgs(argv);

printBanner();

const validatedArgs: PagesGeneratorArgs = {
const projectName = await processArgument<string>(args, "projectName", {
type: "text",
question: `In which directory do you want to create your application?`,
helpText: "also used as application name",
defaultValue: C3_DEFAULTS.projectName,
label: "dir",
validate: (value) =>
validateProjectDirectory(String(value) || C3_DEFAULTS.projectName),
format: (val) => `./${val}`,
});

// If not specified, attempt to infer the `type` argument from other flags
if (!args.type) {
if (args.framework) {
args.type = "webFramework";
} else if (args.existingScript) {
args.type = "pre-existing";
}
}

const templateOptions = Object.entries(templateMap)
.filter(([_, { hidden }]) => !hidden)
.map(([value, { label }]) => ({ value, label }));

const type = await processArgument<string>(args, "type", {
type: "select",
question: "What type of application do you want to create?",
label: "type",
options: templateOptions,
defaultValue: C3_DEFAULTS.type,
});

if (!type || !Object.keys(templateMap).includes(type)) {
return crash("An application type must be specified to continue.");
}

const validatedArgs: C3Args = {
...args,
projectName: await validateName(args.projectName, {
acceptDefault: args.wranglerDefaults,
}),
type: await validateType(args.type, {
acceptDefault: args.wranglerDefaults,
}),
type,
projectName,
};

const { handler } = templateMap[validatedArgs.type];
const { handler } = templateMap[type];
await handler(validatedArgs);
};

Expand All @@ -36,7 +83,7 @@ const printBanner = () => {
startSection(`Create an application with Cloudflare`, "Step 1 of 3");
};

const parseArgs = async (argv: string[]) => {
export const parseArgs = async (argv: string[]): Promise<Partial<C3Args>> => {
const args = await yargs(hideBin(argv))
.scriptName("create-cloudflare")
.usage("$0 [args]")
Expand All @@ -56,62 +103,25 @@ const parseArgs = async (argv: string[]) => {
type: "string",
hidden: templateMap["pre-existing"].hidden,
})
.option("accept-defaults", {
alias: "y",
type: "boolean",
})
.option("wrangler-defaults", { type: "boolean", hidden: true })
.version(version)
.help().argv;

return {
...(args.wranglerDefaults && WRANGLER_DEFAULTS),
...(args.acceptDefaults && C3_DEFAULTS),
projectName: args._[0] as string | undefined,
...args,
};
};

const validateName = async (
name: string | undefined,
{ acceptDefault = false } = {}
): Promise<string> => {
const defaultValue = name ?? new Haikunator().haikunate({ tokenHex: true });
return textInput({
question: `In which directory do you want to create your application?`,
helpText: "also used as application name",
renderSubmitted: (value: string) => {
return `${brandColor("dir")} ${dim(value)}`;
},
defaultValue,
acceptDefault,
validate: (value) => validateProjectDirectory(value || defaultValue),
format: (val: string) => `./${val}`,
});
};

const validateType = async (
type: string | undefined,
{ acceptDefault = false } = {}
) => {
const templateOptions = Object.entries(templateMap)
.filter(([_, { hidden }]) => !hidden)
.map(([value, { label }]) => ({ value, label }));

type = await selectInput({
question: "What type of application do you want to create?",
options: templateOptions,
renderSubmitted: (option: Option) => {
return `${brandColor("type")} ${dim(option.label)}`;
},
defaultValue: type ?? "hello-world",
acceptDefault,
});

if (!type || !Object.keys(templateMap).includes(type)) {
crash("An application type must be specified to continue.");
}

return type;
};

type TemplateConfig = {
label: string;
handler: (args: PagesGeneratorArgs) => Promise<void>;
handler: (args: C3Args) => Promise<void>;
hidden?: boolean;
};

Expand Down
42 changes: 19 additions & 23 deletions packages/create-cloudflare/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import {
runCommands,
wranglerLogin,
} from "helpers/command";
import { confirmInput, selectInput, spinner } from "helpers/interactive";
import { inputPrompt, processArgument, spinner } from "helpers/interactive";
import { detectPackageManager } from "helpers/packages";
import { poll } from "helpers/poll";
import type { Option } from "helpers/interactive";
import type { PagesGeneratorArgs, PagesGeneratorContext } from "types";
import { C3_DEFAULTS } from "./cli";
import type { C3Args, PagesGeneratorContext } from "types";

const { npm } = detectPackageManager();

Expand All @@ -38,7 +38,7 @@ export const validateProjectDirectory = (relativePath: string) => {
}
};

export const setupProjectDirectory = (args: PagesGeneratorArgs) => {
export const setupProjectDirectory = (args: C3Args) => {
// Crash if the directory already exists
const path = resolve(args.projectName);
const err = validateProjectDirectory(path);
Expand All @@ -61,16 +61,15 @@ export const setupProjectDirectory = (args: PagesGeneratorArgs) => {
export const offerToDeploy = async (ctx: PagesGeneratorContext) => {
startSection(`Deploy with Cloudflare`, `Step 3 of 3`);

ctx.args.deploy = await confirmInput({
const label = `deploy via \`${npm} run ${
ctx.framework?.config.deployCommand ?? "deploy"
}\``;

ctx.args.deploy = await processArgument(ctx.args, "deploy", {
type: "confirm",
question: "Do you want to deploy your application?",
renderSubmitted: (value: boolean) =>
`${brandColor(value ? `yes` : `no`)} ${dim(
`deploying via \`${npm} run ${
ctx.framework?.config.deployCommand ?? "deploy"
}\``
)}`,
defaultValue: ctx.args.deploy ?? (ctx.args.wranglerDefaults ? false : true), // if --wrangler-defaults, default to false, otherwise default to true
acceptDefault: ctx.args.wranglerDefaults,
label,
defaultValue: C3_DEFAULTS.deploy,
});

if (!ctx.args.deploy) return;
Expand Down Expand Up @@ -136,14 +135,12 @@ export const chooseAccount = async (ctx: PagesGeneratorContext) => {
value: id,
}));

accountId = await selectInput({
accountId = await inputPrompt({
type: "select",
question: "Which account do you want to use?",
options: accountOptions,
renderSubmitted: (option: Option) => {
return `${brandColor("account")} ${dim(option.label)}`;
},
label: "account",
defaultValue: accountOptions[0].value,
acceptDefault: ctx.args.wranglerDefaults,
});
}
const accountName = Object.keys(accounts).find(
Expand Down Expand Up @@ -230,12 +227,11 @@ export const offerGit = async (ctx: PagesGeneratorContext) => {

if (insideGitRepo) return;

ctx.args.git ??= await confirmInput({
ctx.args.git = await processArgument(ctx.args, "git", {
type: "confirm",
question: "Do you want to use git for version control?",
renderSubmitted: (value: boolean) =>
`${brandColor("git")} ${dim(value ? `yes` : `no`)}`,
defaultValue: true,
acceptDefault: ctx.args.wranglerDefaults,
label: "git",
defaultValue: C3_DEFAULTS.git,
});

if (ctx.args.git) {
Expand Down
21 changes: 7 additions & 14 deletions packages/create-cloudflare/src/frameworks/gatsby/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { brandColor, dim } from "helpers/colors";
import { runFrameworkGenerator } from "helpers/command";
import { compatDateFlag } from "helpers/files";
import { confirmInput, textInput } from "helpers/interactive";
import { inputPrompt } from "helpers/interactive";
import { detectPackageManager } from "helpers/packages";
import { getFrameworkVersion } from "../index";
import type { PagesGeneratorContext, FrameworkConfig } from "types";
Expand All @@ -11,26 +10,20 @@ const { npm, dlx } = detectPackageManager();
const generate = async (ctx: PagesGeneratorContext) => {
const defaultTemplate = "https://github.com/gatsbyjs/gatsby-starter-blog";

const useTemplate = await confirmInput({
const useTemplate = await inputPrompt({
type: "confirm",
question: "Would you like to use a template?",
renderSubmitted: (value: boolean) => {
const status = value ? "yes" : "no";
return `${brandColor(`template`)} ${status}`;
},
label: "template",
defaultValue: true,
acceptDefault: false,
});

let templateUrl = "";
if (useTemplate) {
templateUrl = await textInput({
templateUrl = await inputPrompt({
type: "text",
question: `Please specify the url of the template you'd like to use`,
renderSubmitted: (value: string) => {
const result = `Using template \`${value}\``;
return `${brandColor("template")} ${dim(result)}`;
},
label: "template",
defaultValue: defaultTemplate,
acceptDefault: false,
});
}

Expand Down
Loading

0 comments on commit 1ce3296

Please sign in to comment.