Skip to content

Commit

Permalink
feat(create-cloudflare): allow users to change prompt answer (#6428)
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung authored Aug 15, 2024
1 parent 0d85f24 commit 37dc86f
Show file tree
Hide file tree
Showing 11 changed files with 449 additions and 250 deletions.
7 changes: 7 additions & 0 deletions .changeset/gold-books-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"create-cloudflare": minor
---

feat: allow users to change prompt answer

It is now possible to change the answer for category and type while the project has not been created locally.
30 changes: 11 additions & 19 deletions packages/cli/args.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
import { getRenderers, inputPrompt } from "./interactive";
import { crash, logRaw } from ".";
import { inputPrompt } from "./interactive";
import type { Arg, PromptConfig } from "./interactive";

export const processArgument = async <T>(
args: Record<string, Arg>,
name: string,
promptConfig: PromptConfig
) => {
let value = args[name];
const renderSubmitted = getRenderers(promptConfig).submit;
const value = args[name];
const result = await inputPrompt<T>({
...promptConfig,
// Accept the default value if the arg is already set
acceptDefault: promptConfig.acceptDefault ?? value !== undefined,
defaultValue: value ?? promptConfig.defaultValue,
});

// If the value has already been set via args, use that
if (value !== undefined) {
const error = promptConfig.validate?.(value);
if (error) {
crash(error);
}
// Update value in args before returning the result
args[name] = result as Arg;

const lines = renderSubmitted({ value });
logRaw(lines.join("\n"));

return value as T;
}

value = await inputPrompt(promptConfig);

return value as T;
return result;
};
3 changes: 3 additions & 0 deletions packages/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const shapes = {
radioInactive: "○",
radioActive: "●",

backActive: "◀",
backInactive: "◁",

bar: "│",
leftT: "├",
rigthT: "┤",
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export type Option = {
description?: string;
value: string; // underlying key
hidden?: boolean;
activeIcon?: string;
inactiveIcon?: string;
};

export type BasePromptConfig = {
Expand Down Expand Up @@ -325,8 +327,8 @@ const getSelectRenderers = (

const indicator =
isInListOfValues || (active && !Array.isArray(value))
? color(shapes.radioActive)
: color(shapes.radioInactive);
? color(opt.activeIcon ?? shapes.radioActive)
: color(opt.inactiveIcon ?? shapes.radioInactive);

return `${space(2)}${indicator} ${text} ${sublabel}`;
};
Expand Down
107 changes: 103 additions & 4 deletions packages/create-cloudflare/e2e-tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,17 +236,18 @@ describe.skipIf(frameworkToTest || isQuarantineMode())(
matcher: /What would you like to start with\?/,
input: {
type: "select",
searchBy: "description",
target:
target: "Demo application",
assertDescriptionText:
"Select from a range of starter applications using various Cloudflare products",
},
},
{
matcher: /Which template would you like to use\?/,
input: {
type: "select",
searchBy: "description",
target: "Get started building a basic API on Workers",
target: "API starter (OpenAPI compliant)",
assertDescriptionText:
"Get started building a basic API on Workers",
},
},
],
Expand All @@ -258,5 +259,103 @@ describe.skipIf(frameworkToTest || isQuarantineMode())(
expect(output).toContain(`type API starter (OpenAPI compliant)`);
},
);

test.skipIf(process.platform === "win32")(
"Going back and forth between the category, type, framework and lang prompts",
async () => {
const { output } = await runC3(
[projectPath, "--git=false", "--no-deploy"],
[
{
matcher: /What would you like to start with\?/,
input: {
type: "select",
target: "Demo application",
},
},
{
matcher: /Which template would you like to use\?/,
input: {
type: "select",
target: "Queue consumer & producer Worker",
},
},
{
matcher: /Which language do you want to use\?/,
input: {
type: "select",
target: "Go back",
},
},
{
matcher: /Which template would you like to use\?/,
input: {
type: "select",
target: "Go back",
assertDefaultSelection: "Queue consumer & producer Worker",
},
},
{
matcher: /What would you like to start with\?/,
input: {
type: "select",
target: "Framework Starter",
assertDefaultSelection: "Demo application",
},
},
{
matcher: /Which development framework do you want to use\?/,
input: {
type: "select",
target: "Go back",
},
},
{
matcher: /What would you like to start with\?/,
input: {
type: "select",
target: "Hello World example",
assertDefaultSelection: "Framework Starter",
},
},
{
matcher: /Which template would you like to use\?/,
input: {
type: "select",
target: "Hello World Worker Using Durable Objects",
},
},
{
matcher: /Which language do you want to use\?/,
input: {
type: "select",
target: "Go back",
},
},
{
matcher: /Which template would you like to use\?/,
input: {
type: "select",
target: "Hello World Worker",
assertDefaultSelection:
"Hello World Worker Using Durable Objects",
},
},
{
matcher: /Which language do you want to use\?/,
input: {
type: "select",
target: "JavaScript",
},
},
],
logStream,
);

expect(projectPath).toExist();
expect(output).toContain(`type Hello World Worker`);
expect(output).toContain(`lang JavaScript`);
},
);
},
);
62 changes: 47 additions & 15 deletions packages/create-cloudflare/e2e-tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export type PromptHandler = {
| {
type: "select";
target: RegExp | string;
searchBy?: "label" | "description";
assertDefaultSelection?: string;
assertDescriptionText?: string;
};
};

Expand Down Expand Up @@ -87,7 +88,8 @@ export const runC3 = async (
// so we store the current PromptHandler if we have already matched the question
let currentSelectDialog: PromptHandler | undefined;
const handlePrompt = (data: string) => {
const lines: string[] = data.toString().split("\n");
const text = stripAnsi(data.toString());
const lines = text.split("\n");
const currentDialog = currentSelectDialog ?? promptHandlers[0];

if (!currentDialog) {
Expand All @@ -111,25 +113,49 @@ export const runC3 = async (
} else if (currentDialog.input.type === "select") {
// select prompt handler

// Our select prompt options start with ○ for unselected options and ● for the current selection
const currentSelection = lines.find((line) => line.startsWith("●"));
// FirstFrame: The first onData call for the current select dialog
const isFirstFrameOfCurrentSelectDialog =
currentSelectDialog === undefined;

// Our select prompt options start with ○ / ◁ for unselected options and ● / ◀ for the current selection
const selectedOptionRegex = /^(●|◀)\s/;
const currentSelection = lines
.find((line) => line.match(selectedOptionRegex))
?.replace(selectedOptionRegex, "");

if (!currentSelection) {
// sometimes `lines` contain only the 'clear screen' ANSI codes and not the prompt options
return;
}

const { target, searchBy } = currentDialog.input;
const searchText =
searchBy === "description"
? lines
.filter((line) => !line.startsWith("●") && !line.startsWith("○"))
.join(" ")
: currentSelection;
const { target, assertDefaultSelection, assertDescriptionText } =
currentDialog.input;

if (
isFirstFrameOfCurrentSelectDialog &&
assertDefaultSelection !== undefined &&
assertDefaultSelection !== currentSelection
) {
throw new Error(
`The default selection does not match; Expected "${assertDefaultSelection}" but found "${currentSelection}".`,
);
}

const matchesSelectionTarget =
typeof target === "string"
? searchText.includes(target)
: target.test(searchText);
? currentSelection.includes(target)
: target.test(currentSelection);
const description = text.replaceAll("\n", " ");

if (
matchesSelectionTarget &&
assertDescriptionText !== undefined &&
!description.includes(assertDescriptionText)
) {
throw new Error(
`The description does not match; Expected "${assertDescriptionText}" but found "${description}".`,
);
}

if (matchesSelectionTarget) {
// matches selection, so hit enter
Expand Down Expand Up @@ -217,8 +243,14 @@ export const waitForExit = async (
await new Promise((resolve, rejects) => {
proc.stdout.on("data", (data) => {
stdout.push(data);
if (onData) {
onData(data);
try {
if (onData) {
onData(data);
}
} catch (error) {
// Close the input stream so the process can exit properly
proc.stdin.end();
throw error;
}
});

Expand Down
18 changes: 9 additions & 9 deletions packages/create-cloudflare/src/__tests__/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
import { beforeEach, describe, expect, test, vi } from "vitest";
import {
addWranglerToGitIgnore,
deriveCorrelatedArgs,
downloadRemoteTemplate,
inferLanguageArg,
} from "../templates";
import type { PathLike } from "fs";
import type { C3Args, C3Context } from "types";
Expand Down Expand Up @@ -283,33 +283,33 @@ describe("downloadRemoteTemplate", () => {
});
});

describe("inferLanguageArg", () => {
test("should infer as TypeScript if `--ts` is specified", async () => {
describe("deriveCorrelatedArgs", () => {
test("should derive the lang as TypeScript if `--ts` is specified", () => {
const args: Partial<C3Args> = {
ts: true,
};

inferLanguageArg(args);
deriveCorrelatedArgs(args);

expect(args.lang).toBe("ts");
});

test("should infer as JavaScript if `--ts=false` is specified", async () => {
test("should derive the lang as JavaScript if `--ts=false` is specified", () => {
const args: Partial<C3Args> = {
ts: false,
};

inferLanguageArg(args);
deriveCorrelatedArgs(args);

expect(args.lang).toBe("js");
});

test("should crash only if both the lang and ts arguments are specified", async () => {
test("should crash if both the lang and ts arguments are specified", () => {
let args: Partial<C3Args> = {
lang: "ts",
};

inferLanguageArg(args);
deriveCorrelatedArgs(args);

expect(args.lang).toBe("ts");
expect(crash).not.toBeCalled();
Expand All @@ -318,7 +318,7 @@ describe("inferLanguageArg", () => {
ts: true,
lang: "ts",
};
inferLanguageArg(args);
deriveCorrelatedArgs(args);

expect(crash).toBeCalledWith(
"The `--ts` argument cannot be specified in conjunction with the `--lang` argument",
Expand Down
Loading

0 comments on commit 37dc86f

Please sign in to comment.