Skip to content

Commit

Permalink
style(create-cloudflare): add description support to select prompt (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung authored Aug 12, 2024
1 parent 71882ee commit 5d771c2
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-deers-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-cloudflare": patch
---

style(create-cloudflare): guiding user template selection with description for each options
65 changes: 58 additions & 7 deletions packages/cli/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ import { createLogUpdate } from "log-update";
import { blue, bold, brandColor, dim, gray, white } from "./colors";
import SelectRefreshablePrompt from "./select-list";
import { stdout } from "./streams";
import { cancel, crash, logRaw, newline, shapes, space, status } from "./index";
import {
cancel,
crash,
logRaw,
newline,
shapes,
space,
status,
stripAnsi,
} from "./index";
import type { OptionWithDetails } from "./select-list";
import type { Prompt } from "@clack/core";

Expand All @@ -23,6 +32,7 @@ export const leftT = gray(shapes.leftT);
export type Option = {
label: string; // user-visible string
sublabel?: string; // user-visible string
description?: string;
value: string; // underlying key
hidden?: boolean;
};
Expand Down Expand Up @@ -303,8 +313,7 @@ const getSelectRenderers = (
const helpText = _helpText ?? "";
const maxItemsPerPage = config.maxItemsPerPage ?? 32;

const defaultRenderer: Renderer = ({ cursor, value }) => {
cursor = cursor ?? 0;
const defaultRenderer: Renderer = ({ cursor = 0, value }) => {
const renderOption = (opt: Option, i: number) => {
const { label: optionLabel, value: optionValue } = opt;
const active = i === cursor;
Expand All @@ -327,22 +336,22 @@ const getSelectRenderers = (
return true;
}

cursor = cursor ?? 0;
if (i < cursor) {
return options.length - i <= maxItemsPerPage;
}

return cursor + maxItemsPerPage > i;
};

return [
const visibleOptions = options.filter((o) => !o.hidden);
const activeOption = visibleOptions.at(cursor);
const lines = [
`${blCorner} ${bold(question)} ${dim(helpText)}`,
`${
cursor > 0 && options.length > maxItemsPerPage
? `${space(2)}${dim("...")}\n`
: ""
}${options
.filter((o) => !o.hidden)
}${visibleOptions
.map(renderOption)
.filter(renderOptionCondition)
.join(`\n`)}${
Expand All @@ -353,6 +362,48 @@ const getSelectRenderers = (
}`,
``, // extra line for readability
];

if (activeOption?.description) {
// To wrap the text by words instead of characters
const wordSegmenter = new Intl.Segmenter("en", { granularity: "word" });
const padding = space(2);
const availableWidth =
process.stdout.columns - stripAnsi(padding).length * 2;

// The description cannot have any ANSI code
// As the segmenter will split the code to several segments
const description = stripAnsi(activeOption.description);
const descriptionLines: string[] = [];
let descriptionLineNumber = 0;

for (const data of wordSegmenter.segment(description)) {
let line = descriptionLines[descriptionLineNumber] ?? "";

const currentLineWidth = line.length;
const segmentSize = data.segment.length;

if (currentLineWidth + segmentSize > availableWidth) {
descriptionLineNumber++;
line = "";

// To avoid starting a new line with a space
if (data.segment.match(/^\s+$/)) {
continue;
}
}

descriptionLines[descriptionLineNumber] = line + data.segment;
}

lines.push(
dim(
descriptionLines.map((line) => padding + line + padding).join("\n")
),
``
);
}

return lines;
};

return {
Expand Down
35 changes: 34 additions & 1 deletion packages/create-cloudflare/e2e-tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe.skipIf(frameworkToTest || isQuarantineMode())(
);

expect(projectPath).toExist();
expect(output).toContain(`type Example router & proxy Worker`);
expect(output).toContain(`type Scheduled Worker (Cron Trigger)`);
expect(output).toContain(`lang JavaScript`);
expect(output).toContain(`no git`);
expect(output).toContain(`no deploy`);
Expand Down Expand Up @@ -225,5 +225,38 @@ describe.skipIf(frameworkToTest || isQuarantineMode())(
expect(output).toContain(`lang Python`);
},
);

test.skipIf(process.platform === "win32")(
"Selecting template by description",
async () => {
const { output } = await runC3(
[projectPath, "--no-deploy", "--git=false"],
[
{
matcher: /What would you like to start with\?/,
input: {
type: "select",
searchBy: "description",
target:
"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",
},
},
],
logStream,
);

expect(projectPath).toExist();
expect(output).toContain(`category Demo application`);
expect(output).toContain(`type API starter (OpenAPI compliant)`);
},
);
},
);
21 changes: 17 additions & 4 deletions packages/create-cloudflare/e2e-tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ const testEnv = {

export type PromptHandler = {
matcher: RegExp;
input: string[] | { type: "select"; target: RegExp | string };
input:
| string[]
| {
type: "select";
target: RegExp | string;
searchBy?: "label" | "description";
};
};

export type RunnerConfig = {
Expand Down Expand Up @@ -113,10 +119,17 @@ export const runC3 = async (
return;
}

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

if (matchesSelectionTarget) {
// matches selection, so hit enter
Expand Down
31 changes: 26 additions & 5 deletions packages/create-cloudflare/src/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export type TemplateConfig = {
id: string;
/** A string that controls how the template is presented to the user in the selection menu*/
displayName: string;
/** A string that explains what is inside the template, including any resources and how those will be used*/
description?: string;
/** The deployment platform for this template */
platform: "workers" | "pages";
/** When set to true, hides this template from the selection menu */
Expand Down Expand Up @@ -203,10 +205,28 @@ export const selectTemplate = async (args: Partial<C3Args>) => {
question: "What would you like to start with?",
label: "category",
options: [
{ label: "Hello World example", value: "hello-world" },
{ label: "Framework Starter", value: "web-framework" },
{ label: "Demo application", value: "demo" },
{ label: "Template from a Github repo", value: "remote-template" },
{
label: "Hello World example",
value: "hello-world",
description:
"Select from barebones examples to get started with Workers",
},
{
label: "Framework Starter",
value: "web-framework",
description: "Select from the most popular full-stack web frameworks",
},
{
label: "Demo application",
value: "demo",
description:
"Select from a range of starter applications using various Cloudflare products",
},
{
label: "Template from a Github repo",
value: "remote-template",
description: "Start from an existing GitHub repo link",
},
// This is used only if the type is `pre-existing`
{ label: "Others", value: "others", hidden: true },
],
Expand All @@ -223,14 +243,15 @@ export const selectTemplate = async (args: Partial<C3Args>) => {

const templateMap = await getTemplateMap();
const templateOptions = Object.entries(templateMap).map(
([value, { displayName, hidden }]) => {
([value, { displayName, description, hidden }]) => {
const isHelloWorldExample = value.startsWith("hello-world");
const isCategoryMatched =
category === "hello-world" ? isHelloWorldExample : !isHelloWorldExample;

return {
value,
label: displayName,
description,
hidden: hidden || !isCategoryMatched,
};
},
Expand Down
3 changes: 3 additions & 0 deletions packages/create-cloudflare/templates/common/c3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ export default {
configVersion: 1,
id: "common",
displayName: "Example router & proxy Worker",
description:
"Create a Worker to route and forward requests to other services",
platform: "workers",
hidden: true,
copyFiles: {
variants: {
js: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export default {
configVersion: 1,
id: "hello-world-durable-object",
displayName: "Hello World Worker Using Durable Objects",
description:
"Get started with a basic stateful app to build projects like real-time chats, collaborative apps, and multiplayer games",
platform: "workers",
copyFiles: {
variants: {
Expand Down
1 change: 1 addition & 0 deletions packages/create-cloudflare/templates/hello-world/c3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
configVersion: 1,
id: "hello-world",
displayName: "Hello World Worker",
description: "Get started with a basic Worker in the language of your choice",
platform: "workers",
copyFiles: {
variants: {
Expand Down
1 change: 1 addition & 0 deletions packages/create-cloudflare/templates/openapi/c3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
configVersion: 1,
id: "openapi",
displayName: "API starter (OpenAPI compliant)",
description: "Get started building a basic API on Workers",
platform: "workers",
copyFiles: {
path: "./ts",
Expand Down
2 changes: 2 additions & 0 deletions packages/create-cloudflare/templates/queues/c3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export default {
configVersion: 1,
id: "queues",
displayName: "Queue consumer & producer Worker",
description:
"Get started with a Worker that processes background tasks and message batches with Cloudflare Queues",
platform: "workers",
copyFiles: {
variants: {
Expand Down
2 changes: 2 additions & 0 deletions packages/create-cloudflare/templates/scheduled/c3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export default {
configVersion: 1,
id: "scheduled",
displayName: "Scheduled Worker (Cron Trigger)",
description:
"Create a Worker to be executed on a schedule for periodic (cron) jobs",
platform: "workers",
copyFiles: {
variants: {
Expand Down

0 comments on commit 5d771c2

Please sign in to comment.