Skip to content

Commit

Permalink
feat(create-cloudflare): prompt user to correct the answer if the arg…
Browse files Browse the repository at this point in the history
… value is invalid (#7055)
  • Loading branch information
edmundhung authored Oct 25, 2024
1 parent fa8cc0d commit a034aa1
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/shiny-files-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-cloudflare": minor
---

feat: prompt user to correct the answer if the arg value is invalid
11 changes: 10 additions & 1 deletion packages/cli/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export type BasePromptConfig = {
helpText?: string;
// The value to use by default
defaultValue?: Arg;
// The error message to display if the initial value is invalid
initialErrorMessage?: string | null;
// Accept the initialValue/defaultValue as if the user pressed ENTER when prompted
acceptDefault?: boolean;
// The status label to be shown after submitting
Expand Down Expand Up @@ -141,7 +143,14 @@ export const inputPrompt = async <T = string>(

// Looks up the needed renderer by the current state ('initial', 'submitted', etc.)
const dispatchRender = (props: RenderProps, p: Prompt): string | void => {
const renderedLines = renderers[props.state](props, p);
let state = props.state;

if (state === "initial" && promptConfig.initialErrorMessage) {
state = "error";
props.error = promptConfig.initialErrorMessage;
}

const renderedLines = renderers[state](props, p);
return renderedLines.join("\n");
};

Expand Down
80 changes: 57 additions & 23 deletions packages/create-cloudflare/e2e-tests/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from "node:fs";
import { basename } from "node:path";
import { beforeAll, describe, expect } from "vitest";
import { version } from "../package.json";
import { getFrameworkToTest } from "./frameworkToTest";
Expand Down Expand Up @@ -133,30 +135,61 @@ describe.skipIf(experimental || frameworkToTest || isQuarantineMode())(
test({ experimental }).skipIf(process.platform === "win32")(
"Mixed args and interactive",
async ({ logStream, project }) => {
const { output } = await runC3(
[project.path, "--ts", "--no-deploy"],
[
{
matcher: /What would you like to start with\?/,
input: [keys.enter],
},
{
matcher: /Which template would you like to use\?/,
input: [keys.enter],
},
{
matcher: /Do you want to use git for version control/,
input: ["n"],
},
],
logStream,
const projectName = basename(project.path);
const existingProjectName = Array.from(projectName).reverse().join("");
const existingProjectPath = project.path.replace(
projectName,
existingProjectName,
);
const existingFilePath = `${existingProjectPath}/example.json`;

expect(project.path).toExist();
expect(output).toContain(`type Hello World Worker`);
expect(output).toContain(`lang TypeScript`);
expect(output).toContain(`no git`);
expect(output).toContain(`no deploy`);
try {
// Prepare an existing project with a file
fs.mkdirSync(existingProjectPath, { recursive: true });
fs.writeFileSync(existingFilePath, `"Hello World"`);

const { output } = await runC3(
[existingProjectPath, "--ts", "--no-deploy"],
[
// c3 will ask for a new project name as the provided one already exists
{
matcher:
/In which directory do you want to create your application/,
input: {
type: "text",
chunks: [project.path, keys.enter],
assertErrorMessage: `ERROR Directory \`${existingProjectPath}\` already exists and contains files that might conflict. Please choose a new name.`,
},
},
{
matcher: /What would you like to start with\?/,
input: [keys.enter],
},
{
matcher: /Which template would you like to use\?/,
input: [keys.enter],
},
{
matcher: /Do you want to use git for version control/,
input: ["n"],
},
],
logStream,
);

expect(project.path).toExist();
expect(output).toContain(`type Hello World Worker`);
expect(output).toContain(`lang TypeScript`);
expect(output).toContain(`no git`);
expect(output).toContain(`no deploy`);
} finally {
fs.rmSync(existingFilePath, {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 100,
});
}
},
);

Expand Down Expand Up @@ -243,8 +276,9 @@ describe.skipIf(experimental || frameworkToTest || isQuarantineMode())(
test({ experimental }).skipIf(process.platform === "win32")(
"Going back and forth between the category, type, framework and lang prompts",
async ({ logStream, project }) => {
const testProjectPath = "/test-project-path";
const { output } = await runC3(
["/invalid-project-name", "--git=false", "--no-deploy"],
[testProjectPath, "--git=false", "--no-deploy"],
[
{
matcher: /What would you like to start with\?/,
Expand Down
21 changes: 21 additions & 0 deletions packages/create-cloudflare/e2e-tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export type PromptHandler = {
matcher: RegExp;
input:
| string[]
| {
type: "text";
chunks: string[];
assertErrorMessage?: string;
}
| {
type: "select";
target: RegExp | string;
Expand Down Expand Up @@ -117,6 +122,22 @@ export const runC3 = async (
currentDialog.input.forEach((keystroke) => {
proc.stdin.write(keystroke);
});
} else if (currentDialog.input.type === "text") {
// text prompt handler
const { assertErrorMessage, chunks } = currentDialog.input;

if (
assertErrorMessage !== undefined &&
!text.includes(assertErrorMessage)
) {
throw new Error(
`The error message does not match; Expected "${assertErrorMessage}" but found "${text}".`,
);
}

chunks.forEach((keystroke) => {
proc.stdin.write(keystroke);
});
} else if (currentDialog.input.type === "select") {
// select prompt handler

Expand Down
5 changes: 4 additions & 1 deletion packages/create-cloudflare/src/helpers/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,11 +415,14 @@ export const processArgument = async <Key extends keyof C3Args>(
disableTelemetry: args[key] !== undefined,
async promise() {
const value = args[key];
const error = promptConfig.validate?.(value) ?? null;
const result = await inputPrompt<Required<C3Args>[Key]>({
...promptConfig,
// Accept the default value if the arg is already set
acceptDefault: promptConfig.acceptDefault ?? value !== undefined,
acceptDefault:
promptConfig.acceptDefault ?? (value !== undefined && !error),
defaultValue: value ?? promptConfig.defaultValue,
initialErrorMessage: error,
throwOnError: true,
});

Expand Down

0 comments on commit a034aa1

Please sign in to comment.