From 5aa4753a12dde28e91e85a2f9e4eae17ead62fb0 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Mon, 29 Jul 2024 11:11:38 +0100 Subject: [PATCH] feat(create-cloudflare): update submenu userflow (#6320) --- .changeset/rich-nails-attend.md | 9 + .../create-cloudflare/e2e-tests/cli.test.ts | 59 +++++-- .../e2e-tests/workers.test.ts | 46 ++--- .../src/__tests__/templates.test.ts | 59 ++++++- packages/create-cloudflare/src/cli.ts | 2 +- .../create-cloudflare/src/helpers/args.ts | 26 ++- packages/create-cloudflare/src/helpers/cli.ts | 5 +- packages/create-cloudflare/src/templates.ts | 163 ++++++++++++------ packages/create-cloudflare/src/types.ts | 2 + .../hello-world-durable-object/c3.ts | 2 +- .../templates/hello-world-python/c3.ts | 9 - .../templates/hello-world/c3.ts | 5 +- .../py/.gitignore | 0 .../py/package.json | 0 .../py/src/entry.py | 0 .../py/wrangler.toml | 0 16 files changed, 272 insertions(+), 115 deletions(-) create mode 100644 .changeset/rich-nails-attend.md delete mode 100644 packages/create-cloudflare/templates/hello-world-python/c3.ts rename packages/create-cloudflare/templates/{hello-world-python => hello-world}/py/.gitignore (100%) rename packages/create-cloudflare/templates/{hello-world-python => hello-world}/py/package.json (100%) rename packages/create-cloudflare/templates/{hello-world-python => hello-world}/py/src/entry.py (100%) rename packages/create-cloudflare/templates/{hello-world-python => hello-world}/py/wrangler.toml (100%) diff --git a/.changeset/rich-nails-attend.md b/.changeset/rich-nails-attend.md new file mode 100644 index 000000000000..6cbdd4a7ec9c --- /dev/null +++ b/.changeset/rich-nails-attend.md @@ -0,0 +1,9 @@ +--- +"create-cloudflare": minor +--- + +feat: update submenu userflow + +Now, we will first prompt for the kind of templates that should be created and show a different set of templates depending on the category. If the template selected supports different languages, we will also ask for the language user preferred. + +Two new arguments are also added to support the new structure: `--category` and `--lang`. For more details, please refer to our [docs](https://developers.cloudflare.com/pages/get-started/c3#cli-arguments). diff --git a/packages/create-cloudflare/e2e-tests/cli.test.ts b/packages/create-cloudflare/e2e-tests/cli.test.ts index 8d872c817794..6cca9f6f8246 100644 --- a/packages/create-cloudflare/e2e-tests/cli.test.ts +++ b/packages/create-cloudflare/e2e-tests/cli.test.ts @@ -75,11 +75,15 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( [projectPath], [ { - matcher: /What type of application do you want to create/, + matcher: /What do you want to start with\?/, input: [keys.enter], }, { - matcher: /Do you want to use TypeScript/, + matcher: /Which template would you like to use\?/, + input: [keys.enter], + }, + { + matcher: /Which language do you want to use\?/, input: [keys.enter], }, { @@ -95,8 +99,9 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( ); expect(projectPath).toExist(); - expect(output).toContain(`type "Hello World" Worker`); - expect(output).toContain(`yes typescript`); + expect(output).toContain(`category Hello World example`); + expect(output).toContain(`type Hello World Worker`); + expect(output).toContain(`lang TypeScript`); expect(output).toContain(`no git`); expect(output).toContain(`no deploy`); }, @@ -114,12 +119,16 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( input: [projectPath, keys.enter], }, { - matcher: /What type of application do you want to create/, - input: [keys.down, keys.down, keys.down, keys.enter], + matcher: /What do you want to start with\?/, + input: [keys.down, keys.down, keys.enter], }, { - matcher: /Do you want to use TypeScript/, - input: ["n"], + matcher: /Which template would you like to use\?/, + input: [keys.enter], + }, + { + matcher: /Which language do you want to use\?/, + input: [keys.down, keys.enter], }, { matcher: /Do you want to use git for version control/, @@ -135,7 +144,7 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( expect(projectPath).toExist(); expect(output).toContain(`type Example router & proxy Worker`); - expect(output).toContain(`no typescript`); + expect(output).toContain(`lang JavaScript`); expect(output).toContain(`no git`); expect(output).toContain(`no deploy`); }, @@ -148,7 +157,11 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( [projectPath, "--ts", "--no-deploy"], [ { - matcher: /What type of application do you want to create/, + matcher: /What do you want to start with\?/, + input: [keys.enter], + }, + { + matcher: /Which template would you like to use\?/, input: [keys.enter], }, { @@ -160,8 +173,8 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( ); expect(projectPath).toExist(); - expect(output).toContain(`type "Hello World" Worker`); - expect(output).toContain(`yes typescript`); + expect(output).toContain(`type Hello World Worker`); + expect(output).toContain(`lang TypeScript`); expect(output).toContain(`no git`); expect(output).toContain(`no deploy`); }, @@ -190,5 +203,27 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( expect(output).toContain(`template cloned and validated`); }, ); + + test.skipIf(process.platform === "win32")( + "Inferring the category, type and language if the type is `hello-world-python`", + async () => { + // The `hello-world-python` template is now the python variant of the `hello-world` template + const { output } = await runC3( + [ + projectPath, + "--type=hello-world-python", + "--no-deploy", + "--git=false", + ], + [], + logStream, + ); + + expect(projectPath).toExist(); + expect(output).toContain(`category Hello World example`); + expect(output).toContain(`type Hello World Worker`); + expect(output).toContain(`lang Python`); + }, + ); }, ); diff --git a/packages/create-cloudflare/e2e-tests/workers.test.ts b/packages/create-cloudflare/e2e-tests/workers.test.ts index 4346f08aaa0d..689c2a90543c 100644 --- a/packages/create-cloudflare/e2e-tests/workers.test.ts +++ b/packages/create-cloudflare/e2e-tests/workers.test.ts @@ -9,6 +9,7 @@ import { frameworkToTest } from "./frameworkToTest"; import { createTestLogStream, isQuarantineMode, + keys, recreateLogFolder, runC3, testProjectDir, @@ -22,19 +23,13 @@ const TEST_TIMEOUT = 1000 * 60 * 5; type WorkerTestConfig = RunnerConfig & { name?: string; template: string; + variants: string[]; }; const workerTemplates: WorkerTestConfig[] = [ { template: "hello-world", - verifyDeploy: { - route: "/", - expectedText: "Hello World!", - }, - }, - { - template: "hello-world-python", - promptHandlers: [], + variants: ["ts", "js", "python"], verifyDeploy: { route: "/", expectedText: "Hello World!", @@ -42,6 +37,7 @@ const workerTemplates: WorkerTestConfig[] = [ }, { template: "common", + variants: ["ts", "js"], verifyDeploy: { route: "/", expectedText: "Try making requests to:", @@ -49,15 +45,17 @@ const workerTemplates: WorkerTestConfig[] = [ }, { template: "queues", + variants: ["ts", "js"], // Skipped for now, since C3 does not yet support resource creation }, { template: "scheduled", + variants: ["ts", "js"], // Skipped for now, since it's not possible to test scheduled events on deployed Workers }, { template: "openapi", - promptHandlers: [], + variants: [], verifyDeploy: { route: "/", expectedText: "SwaggerUI", @@ -80,31 +78,21 @@ describe workerTemplates .flatMap((template) => - template.promptHandlers - ? [template] - : [ - { - ...template, - name: `${template.name ?? template.template}-ts`, - promptHandlers: [ - { - matcher: /Do you want to use TypeScript\?/, - input: ["y"], - }, - ], - }, - - { + template.variants.length > 0 + ? template.variants.map((variant, index) => { + return { ...template, - name: `${template.name ?? template.template}-js`, + name: `${template.name ?? template.template}-${variant}`, promptHandlers: [ { - matcher: /Do you want to use TypeScript\?/, - input: ["n"], + matcher: /Which language do you want to use\?/, + // Assuming the variants are defined in the same order it is displayed in the prompt + input: Array(index).fill(keys.down).concat(keys.enter), }, ], - }, - ], + }; + }) + : [template], ) .forEach((template) => { const name = template.name ?? template.template; diff --git a/packages/create-cloudflare/src/__tests__/templates.test.ts b/packages/create-cloudflare/src/__tests__/templates.test.ts index ed283908f107..9c489737821e 100644 --- a/packages/create-cloudflare/src/__tests__/templates.test.ts +++ b/packages/create-cloudflare/src/__tests__/templates.test.ts @@ -1,4 +1,5 @@ import { existsSync, statSync } from "fs"; +import { crash } from "@cloudflare/cli"; import { spinner } from "@cloudflare/cli/interactive"; import degit from "degit"; import { mockSpinner } from "helpers/__tests__/mocks"; @@ -8,14 +9,19 @@ import { readFile, writeFile, } from "helpers/files"; -import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { addWranglerToGitIgnore, downloadRemoteTemplate } from "../templates"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { + addWranglerToGitIgnore, + downloadRemoteTemplate, + inferLanguageArg, +} from "../templates"; import type { PathLike } from "fs"; -import type { C3Context } from "types"; +import type { C3Args, C3Context } from "types"; vi.mock("degit"); vi.mock("fs"); vi.mock("helpers/files"); +vi.mock("@cloudflare/cli"); vi.mock("@cloudflare/cli/interactive"); beforeEach(() => { @@ -258,10 +264,6 @@ describe("downloadRemoteTemplate", () => { }); } - afterEach(() => { - vi.resetAllMocks(); - }); - test("should download template using degit", async () => { const mock = mockDegit(); @@ -280,3 +282,46 @@ describe("downloadRemoteTemplate", () => { expect(spinner).not.toBeCalled(); }); }); + +describe("inferLanguageArg", () => { + test("should infer as TypeScript if `--ts` is specified", async () => { + const args: Partial = { + ts: true, + }; + + inferLanguageArg(args); + + expect(args.lang).toBe("ts"); + }); + + test("should infer as JavaScript if `--ts=false` is specified", async () => { + const args: Partial = { + ts: false, + }; + + inferLanguageArg(args); + + expect(args.lang).toBe("js"); + }); + + test("should crash only if both the lang and ts arguments are specified", async () => { + let args: Partial = { + lang: "ts", + }; + + inferLanguageArg(args); + + expect(args.lang).toBe("ts"); + expect(crash).not.toBeCalled(); + + args = { + ts: true, + lang: "ts", + }; + inferLanguageArg(args); + + expect(crash).toBeCalledWith( + "The `--ts` argument cannot be specified in conjunction with the `--lang` argument", + ); + }); +}); diff --git a/packages/create-cloudflare/src/cli.ts b/packages/create-cloudflare/src/cli.ts index 37c56f30bb0b..23694f53ad86 100644 --- a/packages/create-cloudflare/src/cli.ts +++ b/packages/create-cloudflare/src/cli.ts @@ -90,7 +90,7 @@ export const runCli = async (args: Partial) => { const originalCWD = process.cwd(); const { name, path } = setupProjectDirectory(validatedArgs); - const template = await selectTemplate(args); + const template = await selectTemplate(validatedArgs); const ctx: C3Context = { project: { name, path }, args: validatedArgs, diff --git a/packages/create-cloudflare/src/helpers/args.ts b/packages/create-cloudflare/src/helpers/args.ts index 01d46fe69eea..1d148ecd36a2 100644 --- a/packages/create-cloudflare/src/helpers/args.ts +++ b/packages/create-cloudflare/src/helpers/args.ts @@ -46,6 +46,17 @@ const cliDefinition: ArgumentsDefinition = { }, ], options: [ + { + name: "category", + type: "string", + description: `Specifies the kind of templates that should be created`, + values: [ + { name: "hello-world", description: "Hello World example" }, + { name: "web-framework", description: "Framework Starter" }, + { name: "demo", description: "Demo application" }, + { name: "remote-template", description: "Template from a Github repo" }, + ], + }, { name: "type", alias: "t", @@ -54,13 +65,9 @@ const cliDefinition: ArgumentsDefinition = { description: ` When using a built-in template, specifies the type of application that should be created. - Note that "--type" and "--template" are mutually exclusive options. If both are provided, "--type" will be used. + Note that "--category" and "--template" are mutually exclusive options. If both are provided, "--category" will be used. `, values: [ - { - name: "web-framework", - description: "A website or web application.", - }, { name: "hello-world", description: "A basic “Hello World” Cloudflare Worker.", @@ -101,7 +108,7 @@ const cliDefinition: ArgumentsDefinition = { alias: "f", type: "string", requiresArg: true, - description: `The type of framework to use to create a web application (when using this option "--type" is coerced to "web-framework") + description: `The type of framework to use to create a web application (when using this option "--category" is coerced to "web-framework") When using the --framework option, C3 will dispatch to the official creation tool used by the framework (ex. "create-remix" is used for Remix). @@ -127,6 +134,12 @@ const cliDefinition: ArgumentsDefinition = { { name: "vue" }, ], }, + { + name: "lang", + type: "string", + description: `The programming language of the template`, + values: [{ name: "ts" }, { name: "js" }, { name: "python" }], + }, { name: "deploy", type: "boolean", @@ -136,6 +149,7 @@ const cliDefinition: ArgumentsDefinition = { name: "ts", type: "boolean", description: "Use TypeScript in your application", + hidden: true, }, { name: "git", diff --git a/packages/create-cloudflare/src/helpers/cli.ts b/packages/create-cloudflare/src/helpers/cli.ts index 2b2b2452c33d..dfad6d9d4a27 100644 --- a/packages/create-cloudflare/src/helpers/cli.ts +++ b/packages/create-cloudflare/src/helpers/cli.ts @@ -6,6 +6,7 @@ import { getLatestPackageVersion } from "helpers/packages"; import open from "open"; import semver from "semver"; import { version } from "../../package.json"; +import type { C3Args } from "types"; /** * An extremely simple wrapper around the open command. @@ -44,8 +45,9 @@ export const isUpdateAvailable = async () => { } }; -export const C3_DEFAULTS = { +export const C3_DEFAULTS: C3Args = { projectName: new Haikunator().haikunate({ tokenHex: true }), + category: "hello-world", type: "hello-world", framework: "analog", autoUpdate: true, @@ -53,6 +55,7 @@ export const C3_DEFAULTS = { git: true, open: true, ts: true, + lang: "ts", template: "cloudflare/workers-sdk/packages/create-cloudflare/templates/hello-world", }; diff --git a/packages/create-cloudflare/src/templates.ts b/packages/create-cloudflare/src/templates.ts index 93a69e2445f8..b98de0cb875d 100644 --- a/packages/create-cloudflare/src/templates.ts +++ b/packages/create-cloudflare/src/templates.ts @@ -114,8 +114,7 @@ type StaticFileMap = { }; const defaultSelectVariant = async (ctx: C3Context) => { - const typescript = await shouldUseTs(ctx); - return typescript ? "ts" : "js"; + return await selectLanguage(ctx); }; export type FrameworkMap = Awaited>; @@ -141,10 +140,6 @@ export const getFrameworkMap = async () => ({ export const getTemplateMap = async () => { return { "hello-world": (await import("../templates/hello-world/c3")).default, - "hello-world-python": (await import("../templates/hello-world-python/c3")) - .default, - // Dummy record -- actual template config resolved in `selectFramework` - "web-framework": { displayName: "Website or web app" } as TemplateConfig, common: (await import("../templates/common/c3")).default, scheduled: (await import("../templates/scheduled/c3")).default, queues: (await import("../templates/queues/c3")).default, @@ -152,47 +147,98 @@ export const getTemplateMap = async () => { await import("../templates/hello-world-durable-object/c3") ).default, openapi: (await import("../templates/openapi/c3")).default, - // Dummy record -- actual template config resolved in `processRemoteTemplate` - "remote-template": { - displayName: "Worker built from a template hosted in a git repository", - } as TemplateConfig, "pre-existing": (await import("../templates/pre-existing/c3")).default, } as Record; }; export const selectTemplate = async (args: Partial) => { - // If not specified, attempt to infer the `type` argument from other flags - if (!args.type) { - if (args.framework) { + // Infering the type based on the additional arguments provided + // Both `web-framework` and `remote-template` types are no longer used + // They are set only for backwards compatibility + if (args.framework) { + args.type ??= "web-framework"; + } else if (args.template) { + args.type ??= "remote-template"; + } else if (args.existingScript) { + args.type ??= "pre-existing"; + } + + // Infering the category based on the type + switch (args.type) { + case "hello-world": + case "hello-world-durable-object": + args.category ??= "hello-world"; + break; + case "hello-world-python": + args.category ??= "hello-world"; + // The hello-world-python template is merged into the `hello-world` template + args.type = "hello-world"; + args.lang = "python"; + break; + case "webFramework": + // Add backwards compatibility for the older argument (webFramework) + warn( + "The `webFramework` type is deprecated and will be removed in a future version. Please use `web-framework` instead.", + ); + args.category ??= "web-framework"; args.type = "web-framework"; - } else if (args.existingScript) { - args.type = "pre-existing"; - } else if (args.template) { - args.type = "remote-template"; - } + break; + case "web-framework": + case "remote-template": + args.category ??= args.type; + break; + case "common": + case "scheduled": + case "queues": + case "openapi": + args.category ??= "demo"; + break; + case "pre-existing": + args.category ??= "others"; + break; } - // Add backwards compatibility for the older argument (webFramework) - if (args.type && args.type === "webFramework") { - warn( - "The `webFramework` type is deprecated and will be removed in a future version. Please use `web-framework` instead.", - ); - args.type = "web-framework"; + const category = await processArgument(args, "category", { + type: "select", + question: "What do you want 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" }, + // This is used only if the type is `pre-existing` + { label: "Others", value: "others", hidden: true }, + ], + defaultValue: C3_DEFAULTS.category, + }); + + if (category === "web-framework") { + return selectFramework(args); } - const templateMap = await getTemplateMap(); + if (category === "remote-template") { + return processRemoteTemplate(args); + } + const templateMap = await getTemplateMap(); const templateOptions = Object.entries(templateMap).map( - ([value, { displayName, hidden }]) => ({ - value, - label: displayName, - hidden, - }), + ([value, { displayName, hidden }]) => { + const isHelloWorldExample = value.startsWith("hello-world"); + const isCategoryMatched = + category === "hello-world" ? isHelloWorldExample : !isHelloWorldExample; + + return { + value, + label: displayName, + hidden: hidden || !isCategoryMatched, + }; + }, ); const type = await processArgument(args, "type", { type: "select", - question: "What type of application do you want to create?", + question: "Which template would you like to use?", label: "type", options: templateOptions, defaultValue: C3_DEFAULTS.type, @@ -206,14 +252,6 @@ export const selectTemplate = async (args: Partial) => { return crash(`Unknown application type provided: ${type}.`); } - if (type === "web-framework") { - return selectFramework(args); - } - - if (type === "remote-template") { - return processRemoteTemplate(args); - } - return templateMap[type]; }; @@ -292,24 +330,53 @@ export async function copyTemplateFiles(ctx: C3Context) { s.stop(`${brandColor("files")} ${dim("copied to project directory")}`); } -const shouldUseTs = async (ctx: C3Context) => { +export function inferLanguageArg(args: Partial) { + if (args.ts === undefined) { + return; + } + + const language = args.ts ? "ts" : "js"; + + if (args.lang !== undefined) { + crash( + "The `--ts` argument cannot be specified in conjunction with the `--lang` argument", + ); + } + + args.lang = language; +} + +const selectLanguage = async (ctx: C3Context) => { // If we can infer from the directory that it uses typescript, use that if (usesTypescript(ctx)) { - return true; + return "ts"; } // If there is a generate process then we assume that a potential typescript // setup must have been part of it, so we should not offer it here if (ctx.template.generate) { - return false; + return "js"; } - // Otherwise, prompt the user for their TS preference - return processArgument(ctx.args, "ts", { - type: "confirm", - question: "Do you want to use TypeScript?", - label: "typescript", - defaultValue: C3_DEFAULTS.ts, + inferLanguageArg(ctx.args); + + const variants = + ctx.template.copyFiles && !isVariantInfo(ctx.template.copyFiles) + ? Object.keys(ctx.template.copyFiles.variants) + : []; + const languageOptions = [ + { label: "TypeScript", value: "ts" }, + { label: "JavaScript", value: "js" }, + { label: "Python", value: "python" }, + ].filter((option) => variants.includes(option.value)); + + // Otherwise, prompt the user for their language preference + return processArgument(ctx.args, "lang", { + type: "select", + question: "Which language do you want to use?", + label: "lang", + options: languageOptions, + defaultValue: C3_DEFAULTS.lang, }); }; diff --git a/packages/create-cloudflare/src/types.ts b/packages/create-cloudflare/src/types.ts index fd35500bdb61..0660b5c5a0d8 100644 --- a/packages/create-cloudflare/src/types.ts +++ b/packages/create-cloudflare/src/types.ts @@ -7,10 +7,12 @@ export type C3Args = { open?: boolean; git?: boolean; autoUpdate?: boolean; + category?: string; // pages specific framework?: string; // workers specific ts?: boolean; + lang?: string; existingScript?: string; template?: string; acceptDefaults?: boolean; diff --git a/packages/create-cloudflare/templates/hello-world-durable-object/c3.ts b/packages/create-cloudflare/templates/hello-world-durable-object/c3.ts index 145753ba3cd1..7b91e707aa27 100644 --- a/packages/create-cloudflare/templates/hello-world-durable-object/c3.ts +++ b/packages/create-cloudflare/templates/hello-world-durable-object/c3.ts @@ -1,7 +1,7 @@ export default { configVersion: 1, id: "hello-world-durable-object", - displayName: "Co-ordination / multiplayer API (using Durable Objects)", + displayName: "Hello World Worker Using Durable Objects", platform: "workers", copyFiles: { variants: { diff --git a/packages/create-cloudflare/templates/hello-world-python/c3.ts b/packages/create-cloudflare/templates/hello-world-python/c3.ts deleted file mode 100644 index 93718955d7cd..000000000000 --- a/packages/create-cloudflare/templates/hello-world-python/c3.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default { - configVersion: 1, - id: "hello-world-python", - displayName: '"Hello World" Worker (Python)', - platform: "workers", - copyFiles: { - path: "./py", - }, -}; diff --git a/packages/create-cloudflare/templates/hello-world/c3.ts b/packages/create-cloudflare/templates/hello-world/c3.ts index 239eb6bdee1a..1352c15cd41b 100644 --- a/packages/create-cloudflare/templates/hello-world/c3.ts +++ b/packages/create-cloudflare/templates/hello-world/c3.ts @@ -1,7 +1,7 @@ export default { configVersion: 1, id: "hello-world", - displayName: '"Hello World" Worker', + displayName: "Hello World Worker", platform: "workers", copyFiles: { variants: { @@ -11,6 +11,9 @@ export default { ts: { path: "./ts", }, + python: { + path: "./py", + }, }, }, }; diff --git a/packages/create-cloudflare/templates/hello-world-python/py/.gitignore b/packages/create-cloudflare/templates/hello-world/py/.gitignore similarity index 100% rename from packages/create-cloudflare/templates/hello-world-python/py/.gitignore rename to packages/create-cloudflare/templates/hello-world/py/.gitignore diff --git a/packages/create-cloudflare/templates/hello-world-python/py/package.json b/packages/create-cloudflare/templates/hello-world/py/package.json similarity index 100% rename from packages/create-cloudflare/templates/hello-world-python/py/package.json rename to packages/create-cloudflare/templates/hello-world/py/package.json diff --git a/packages/create-cloudflare/templates/hello-world-python/py/src/entry.py b/packages/create-cloudflare/templates/hello-world/py/src/entry.py similarity index 100% rename from packages/create-cloudflare/templates/hello-world-python/py/src/entry.py rename to packages/create-cloudflare/templates/hello-world/py/src/entry.py diff --git a/packages/create-cloudflare/templates/hello-world-python/py/wrangler.toml b/packages/create-cloudflare/templates/hello-world/py/wrangler.toml similarity index 100% rename from packages/create-cloudflare/templates/hello-world-python/py/wrangler.toml rename to packages/create-cloudflare/templates/hello-world/py/wrangler.toml