diff --git a/.changeset/gold-books-cheat.md b/.changeset/gold-books-cheat.md new file mode 100644 index 000000000000..d9e95b89c8d5 --- /dev/null +++ b/.changeset/gold-books-cheat.md @@ -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. diff --git a/packages/cli/args.ts b/packages/cli/args.ts index 93f7a5841abd..2e1cf3a4be79 100644 --- a/packages/cli/args.ts +++ b/packages/cli/args.ts @@ -1,5 +1,4 @@ -import { getRenderers, inputPrompt } from "./interactive"; -import { crash, logRaw } from "."; +import { inputPrompt } from "./interactive"; import type { Arg, PromptConfig } from "./interactive"; export const processArgument = async ( @@ -7,23 +6,16 @@ export const processArgument = async ( name: string, promptConfig: PromptConfig ) => { - let value = args[name]; - const renderSubmitted = getRenderers(promptConfig).submit; + const value = args[name]; + const result = await inputPrompt({ + ...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; }; diff --git a/packages/cli/index.ts b/packages/cli/index.ts index 2f21de93ba03..ad0490b9345d 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -18,6 +18,9 @@ export const shapes = { radioInactive: "○", radioActive: "●", + backActive: "◀", + backInactive: "◁", + bar: "│", leftT: "├", rigthT: "┤", diff --git a/packages/cli/interactive.ts b/packages/cli/interactive.ts index 7495b5b56eb0..461664e2f780 100644 --- a/packages/cli/interactive.ts +++ b/packages/cli/interactive.ts @@ -35,6 +35,8 @@ export type Option = { description?: string; value: string; // underlying key hidden?: boolean; + activeIcon?: string; + inactiveIcon?: string; }; export type BasePromptConfig = { @@ -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}`; }; diff --git a/packages/create-cloudflare/e2e-tests/cli.test.ts b/packages/create-cloudflare/e2e-tests/cli.test.ts index 4c2b3c217052..59c88a7dfff8 100644 --- a/packages/create-cloudflare/e2e-tests/cli.test.ts +++ b/packages/create-cloudflare/e2e-tests/cli.test.ts @@ -236,8 +236,8 @@ 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", }, }, @@ -245,8 +245,9 @@ describe.skipIf(frameworkToTest || isQuarantineMode())( 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", }, }, ], @@ -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`); + }, + ); }, ); diff --git a/packages/create-cloudflare/e2e-tests/helpers.ts b/packages/create-cloudflare/e2e-tests/helpers.ts index 4b2a2936c3e7..4e594de94f49 100644 --- a/packages/create-cloudflare/e2e-tests/helpers.ts +++ b/packages/create-cloudflare/e2e-tests/helpers.ts @@ -53,7 +53,8 @@ export type PromptHandler = { | { type: "select"; target: RegExp | string; - searchBy?: "label" | "description"; + assertDefaultSelection?: string; + assertDescriptionText?: string; }; }; @@ -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) { @@ -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 @@ -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; } }); diff --git a/packages/create-cloudflare/src/__tests__/templates.test.ts b/packages/create-cloudflare/src/__tests__/templates.test.ts index 9c489737821e..ca7a0a56d092 100644 --- a/packages/create-cloudflare/src/__tests__/templates.test.ts +++ b/packages/create-cloudflare/src/__tests__/templates.test.ts @@ -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"; @@ -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 = { 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 = { 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 = { lang: "ts", }; - inferLanguageArg(args); + deriveCorrelatedArgs(args); expect(args.lang).toBe("ts"); expect(crash).not.toBeCalled(); @@ -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", diff --git a/packages/create-cloudflare/src/cli.ts b/packages/create-cloudflare/src/cli.ts index a2f7bdefe073..a05c821e3e42 100644 --- a/packages/create-cloudflare/src/cli.ts +++ b/packages/create-cloudflare/src/cli.ts @@ -1,12 +1,11 @@ #!/usr/bin/env node import { mkdirSync } from "fs"; -import { basename, dirname, resolve } from "path"; +import { dirname } from "path"; import { chdir } from "process"; import { crash, endSection, logRaw, startSection } from "@cloudflare/cli"; -import { processArgument } from "@cloudflare/cli/args"; import { isInteractive } from "@cloudflare/cli/interactive"; import { parseArgs } from "helpers/args"; -import { C3_DEFAULTS, isUpdateAvailable } from "helpers/cli"; +import { isUpdateAvailable } from "helpers/cli"; import { runCommand } from "helpers/command"; import { detectPackageManager, @@ -16,12 +15,13 @@ import { installWrangler, npmInstall } from "helpers/packages"; import { version } from "../package.json"; import { maybeOpenBrowser, offerToDeploy, runDeploy } from "./deploy"; import { printSummary, printWelcomeMessage } from "./dialog"; -import { gitCommit, isInsideGitRepo, offerGit } from "./git"; +import { gitCommit, offerGit } from "./git"; import { createProject } from "./pages"; import { addWranglerToGitIgnore, copyTemplateFiles, - selectTemplate, + createContext, + deriveCorrelatedArgs, updatePackageName, updatePackageScripts, } from "./templates"; @@ -68,72 +68,40 @@ export const runLatest = async () => { export const runCli = async (args: Partial) => { printBanner(); - const defaultName = args.existingScript || C3_DEFAULTS.projectName; - - const projectName = await processArgument(args, "projectName", { - type: "text", - question: `In which directory do you want to create your application?`, - helpText: "also used as application name", - defaultValue: defaultName, - label: "dir", - validate: (value) => - validateProjectDirectory(String(value) || C3_DEFAULTS.projectName, args), - format: (val) => `./${val}`, - }); - - const validatedArgs: C3Args = { - ...args, - projectName, - }; - - const originalCWD = process.cwd(); - const { name, path } = setupProjectDirectory(validatedArgs); - - const template = await selectTemplate(validatedArgs); - const ctx: C3Context = { - project: { name, path }, - args: validatedArgs, - template, - originalCWD, - gitRepoAlreadyExisted: await isInsideGitRepo(dirname(path)), - deployment: {}, - }; - - await runTemplate(ctx); + deriveCorrelatedArgs(args); + + const ctx = await createContext(args); + + await create(ctx); + await configure(ctx); + await deploy(ctx); + + printSummary(ctx); + logRaw(""); }; -export const setupProjectDirectory = (args: C3Args) => { +export const setupProjectDirectory = (ctx: C3Context) => { // Crash if the directory already exists - const path = resolve(args.projectName); - const err = validateProjectDirectory(path, args); + const path = ctx.project.path; + const err = validateProjectDirectory(path, ctx.args); if (err) { crash(err); } const directory = dirname(path); - const pathBasename = basename(path); // If the target is a nested directory, create the parent mkdirSync(directory, { recursive: true }); // Change to the parent directory chdir(directory); - - return { name: pathBasename, path }; -}; - -const runTemplate = async (ctx: C3Context) => { - await create(ctx); - await configure(ctx); - await deploy(ctx); - await printSummary(ctx); - - logRaw(""); }; const create = async (ctx: C3Context) => { const { template } = ctx; + setupProjectDirectory(ctx); + if (template.generate) { await template.generate(ctx); } diff --git a/packages/create-cloudflare/src/dialog.ts b/packages/create-cloudflare/src/dialog.ts index 43e746e1e6cf..e030be7dee63 100644 --- a/packages/create-cloudflare/src/dialog.ts +++ b/packages/create-cloudflare/src/dialog.ts @@ -104,7 +104,7 @@ export function printWelcomeMessage(version: string) { logRaw(dialog); } -export const printSummary = async (ctx: C3Context) => { +export const printSummary = (ctx: C3Context) => { // Prepare relevant information const dashboardUrl = ctx.account ? `https://dash.cloudflare.com/?to=/:account/workers/services/view/${ctx.project.name}` diff --git a/packages/create-cloudflare/src/helpers/files.ts b/packages/create-cloudflare/src/helpers/files.ts index 5ed8d791dec6..408e17ca60ae 100644 --- a/packages/create-cloudflare/src/helpers/files.ts +++ b/packages/create-cloudflare/src/helpers/files.ts @@ -74,7 +74,11 @@ export const probePaths = (paths: string[]) => { }; export const usesTypescript = (ctx: C3Context) => { - return existsSync(join(`${ctx.project.path}`, `tsconfig.json`)); + return hasTsConfig(ctx.project.path); +}; + +export const hasTsConfig = (path: string) => { + return existsSync(join(`${path}`, `tsconfig.json`)); }; const eslintRcExts = ["js", "cjs", "yaml", "yml", "json"] as const; diff --git a/packages/create-cloudflare/src/templates.ts b/packages/create-cloudflare/src/templates.ts index c11d17eb448b..4a046298634a 100644 --- a/packages/create-cloudflare/src/templates.ts +++ b/packages/create-cloudflare/src/templates.ts @@ -1,8 +1,8 @@ import { existsSync } from "fs"; import { cp, mkdtemp, rename } from "fs/promises"; import { tmpdir } from "os"; -import { join, resolve } from "path"; -import { crash, updateStatus, warn } from "@cloudflare/cli"; +import { basename, dirname, join, resolve } from "path"; +import { crash, shapes, updateStatus, warn } from "@cloudflare/cli"; import { processArgument } from "@cloudflare/cli/args"; import { blue, brandColor, dim } from "@cloudflare/cli/colors"; import { spinner } from "@cloudflare/cli/interactive"; @@ -12,13 +12,15 @@ import { C3_DEFAULTS } from "helpers/cli"; import { appendFile, directoryExists, + hasTsConfig, readFile, readJSON, - usesTypescript, writeFile, writeJSON, } from "helpers/files"; -import { validateTemplateUrl } from "./validators"; +import { isInsideGitRepo } from "./git"; +import { validateProjectDirectory, validateTemplateUrl } from "./validators"; +import type { Option } from "@cloudflare/cli/interactive"; import type { C3Args, C3Context, PackageJson } from "types"; export type TemplateConfig = { @@ -116,7 +118,7 @@ type StaticFileMap = { }; const defaultSelectVariant = async (ctx: C3Context) => { - return await selectLanguage(ctx); + return ctx.args.lang; }; export type FrameworkMap = Awaited>; @@ -153,8 +155,8 @@ export const getTemplateMap = async () => { } as Record; }; -export const selectTemplate = async (args: Partial) => { - // Infering the type based on the additional arguments provided +export const deriveCorrelatedArgs = (args: Partial) => { + // Derive 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) { @@ -165,7 +167,7 @@ export const selectTemplate = async (args: Partial) => { args.type ??= "pre-existing"; } - // Infering the category based on the type + // Derive the category based on the type switch (args.type) { case "hello-world": case "hello-world-durable-object": @@ -200,115 +202,250 @@ export const selectTemplate = async (args: Partial) => { break; } + if (args.ts !== undefined) { + 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; + } +}; + +/** + * Collecting all information about the template here + * This includes the project name, the type fo template and the language to use (if applicable) + * There should be no side effects in these prompts so that we can always go back to the previous step + */ +export const createContext = async ( + args: Partial, + prevArgs?: Partial, +): Promise => { + // Allows the users to go back to the previous step + // By moving the cursor up to a certain line and clearing the screen + const goBack = async (from: "type" | "framework" | "lang") => { + const newArgs = { ...args }; + let linesPrinted = 0; + + switch (from) { + case "type": + linesPrinted = 9; + newArgs.category = undefined; + break; + case "framework": + linesPrinted = 9; + newArgs.category = undefined; + break; + case "lang": + linesPrinted = 12; + newArgs.type = undefined; + break; + } + + newArgs[from] = undefined; + args[from] = undefined; + + if (process.stdout.isTTY) { + process.stdout.moveCursor(0, -linesPrinted); + process.stdout.clearScreenDown(); + } + + return await createContext(newArgs, args); + }; + + // The option to go back to the previous step + const BACK_VALUE = "__BACK__"; + const backOption: Option = { + label: "Go back", + value: BACK_VALUE, + activeIcon: shapes.backActive, + inactiveIcon: shapes.backInactive, + }; + + const defaultName = args.existingScript || C3_DEFAULTS.projectName; + const projectName = await processArgument(args, "projectName", { + type: "text", + question: `In which directory do you want to create your application?`, + helpText: "also used as application name", + defaultValue: defaultName, + label: "dir", + validate: (value) => + validateProjectDirectory(String(value) || C3_DEFAULTS.projectName, args), + format: (val) => `./${val}`, + }); + + const categoryOptions = [ + { + 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 }, + ]; + const category = await processArgument(args, "category", { type: "select", question: "What would you like to start with?", label: "category", - options: [ - { - 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 }, - ], - defaultValue: C3_DEFAULTS.category, + options: categoryOptions, + defaultValue: prevArgs?.category ?? C3_DEFAULTS.category, }); + let template: TemplateConfig; + if (category === "web-framework") { - return selectFramework(args); - } + const frameworkMap = await getFrameworkMap(); + const frameworkOptions = Object.entries(frameworkMap).map( + ([key, config]) => ({ + label: config.displayName, + value: key, + }), + ); - if (category === "remote-template") { - return processRemoteTemplate(args); - } + const framework = await processArgument( + args, + "framework", + { + type: "select", + label: "framework", + question: "Which development framework do you want to use?", + options: frameworkOptions.concat(backOption), + defaultValue: prevArgs?.framework ?? C3_DEFAULTS.framework, + }, + ); - const templateMap = await getTemplateMap(); - const templateOptions = Object.entries(templateMap).map( - ([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, - }; - }, - ); + if (framework === BACK_VALUE) { + return goBack("framework"); + } - const type = await processArgument(args, "type", { - type: "select", - question: "Which template would you like to use?", - label: "type", - options: templateOptions, - defaultValue: C3_DEFAULTS.type, - }); + const frameworkConfig = frameworkMap[framework]; - if (!type) { - return crash("An application type must be specified to continue."); - } + if (!frameworkConfig) { + crash(`Unsupported framework: ${framework}`); + } - if (!Object.keys(templateMap).includes(type)) { - return crash(`Unknown application type provided: ${type}.`); - } + template = { + deployScript: "pages:deploy", + devScript: "pages:dev", + ...frameworkConfig, + }; + } else if (category === "remote-template") { + template = await processRemoteTemplate(args); + } else { + const templateMap = await getTemplateMap(); + const templateOptions: Option[] = Object.entries(templateMap).map( + ([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, + }; + }, + ); - return templateMap[type]; -}; + const type = await processArgument(args, "type", { + type: "select", + question: "Which template would you like to use?", + label: "type", + options: templateOptions.concat(backOption), + defaultValue: prevArgs?.type ?? C3_DEFAULTS.type, + }); -export const selectFramework = async (args: Partial) => { - const frameworkMap = await getFrameworkMap(); - const frameworkOptions = Object.entries(frameworkMap).map( - ([key, config]) => ({ - label: config.displayName, - value: key, - }), - ); + if (type === BACK_VALUE) { + return goBack("type"); + } - const framework = await processArgument(args, "framework", { - type: "select", - label: "framework", - question: "Which development framework do you want to use?", - options: frameworkOptions, - defaultValue: C3_DEFAULTS.framework, - }); + template = templateMap[type]; - if (!framework) { - crash("A framework must be selected to continue."); + if (!template) { + return crash(`Unknown application type provided: ${type}.`); + } } - if (!Object.keys(frameworkMap).includes(framework)) { - crash(`Unsupported framework: ${framework}`); + const path = resolve(projectName); + const languageVariants = + template.copyFiles && + !isVariantInfo(template.copyFiles) && + !template.copyFiles.selectVariant + ? Object.keys(template.copyFiles.variants) + : []; + + // Prompt for language preference only if selectVariant is not defined + // If it is defined, copyTemplateFiles will handle the selection + if (languageVariants.length > 0) { + if (hasTsConfig(path)) { + // If we can infer from the directory that it uses typescript, use that + args.lang = "ts"; + } else if (template.generate) { + // 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 + args.lang = "js"; + } else { + // Otherwise, prompt the user for their language preference + const languageOptions = [ + { label: "TypeScript", value: "ts" }, + { label: "JavaScript", value: "js" }, + { label: "Python (beta)", value: "python" }, + ]; + + const lang = await processArgument(args, "lang", { + type: "select", + question: "Which language do you want to use?", + label: "lang", + options: languageOptions + .filter((option) => languageVariants.includes(option.value)) + // Allow going back only if the user is not selecting a remote template + .concat(args.template ? [] : backOption), + defaultValue: C3_DEFAULTS.lang, + }); + + if (lang === BACK_VALUE) { + return goBack("lang"); + } + } } - const defaultFrameworkConfig = { - deployScript: "pages:deploy", - devScript: "pages:dev", - }; + const name = basename(path); + const directory = dirname(path); + const originalCWD = process.cwd(); return { - ...defaultFrameworkConfig, - ...frameworkMap[framework as FrameworkName], + project: { name, path }, + args: { + ...args, + projectName, + }, + template, + originalCWD, + gitRepoAlreadyExisted: await isInsideGitRepo(directory), + deployment: {}, }; }; @@ -329,8 +466,13 @@ export async function copyTemplateFiles(ctx: C3Context) { const variant = await selectVariant(ctx); - const variantPath = copyFiles.variants[variant].path; - srcdir = join(getTemplatePath(ctx), variantPath); + const variantInfo = variant ? copyFiles.variants[variant] : null; + + if (!variantInfo) { + crash(`Unknown variant provided: ${JSON.stringify(variant ?? "")}`); + } + + srcdir = join(getTemplatePath(ctx), variantInfo.path); } const copyDestDir = await getCopyFilesDestinationDir(ctx); @@ -351,56 +493,6 @@ export async function copyTemplateFiles(ctx: C3Context) { s.stop(`${brandColor("files")} ${dim("copied to project directory")}`); } -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 "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 "js"; - } - - 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 (beta)", 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, - }); -}; - export const processRemoteTemplate = async (args: Partial) => { const templateUrl = await processArgument(args, "template", { type: "text",