From a34d80fcf76aff1e3797f1ca1fd586fce7900731 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Sun, 8 Dec 2024 22:16:12 +0100 Subject: [PATCH] fix: validate arguments correctly (#724) currently the arguments are not validated correctly (https://github.com/callstack/react-native-builder-bob/issues/722) since they are validated against filtered questions. this fixes it to validate against the full list of questions. this also refactors the prompts module to move argument handling to the prompt function. --- .github/workflows/build-templates.yml | 2 +- .../create-react-native-library/src/index.ts | 23 ++--- .../create-react-native-library/src/input.ts | 67 ++----------- .../src/template.ts | 2 +- .../src/utils/assert.ts | 42 ++++---- .../src/utils/prompt.ts | 98 +++++++++++++++++++ .../src/utils/prompts.ts | 16 --- 7 files changed, 140 insertions(+), 110 deletions(-) create mode 100644 packages/create-react-native-library/src/utils/prompt.ts delete mode 100644 packages/create-react-native-library/src/utils/prompts.ts diff --git a/.github/workflows/build-templates.yml b/.github/workflows/build-templates.yml index 45bda5712..092f5233e 100644 --- a/.github/workflows/build-templates.yml +++ b/.github/workflows/build-templates.yml @@ -45,7 +45,7 @@ jobs: language: kotlin-swift - type: fabric-view language: cpp - - type: legacy-module + - type: legacy-view language: cpp include: - os: ubuntu-latest diff --git a/packages/create-react-native-library/src/index.ts b/packages/create-react-native-library/src/index.ts index dbf87992f..f01eed082 100644 --- a/packages/create-react-native-library/src/index.ts +++ b/packages/create-react-native-library/src/index.ts @@ -3,7 +3,7 @@ import fs from 'fs-extra'; import kleur from 'kleur'; import yargs from 'yargs'; import ora from 'ora'; -import prompts from './utils/prompts'; +import { prompt } from './utils/prompt'; import generateExampleApp from './exampleApp/generateExampleApp'; import { addCodegenBuildScript } from './exampleApp/addCodegenBuildScript'; import { createInitialGitCommit } from './utils/initialCommit'; @@ -56,22 +56,15 @@ async function create(_argv: yargs.Arguments) { const basename = path.basename(folder); - const { questions, singleChoiceAnswers } = await createQuestions({ - basename, - local, - argv, - }); + const questions = await createQuestions({ basename, local }); assertUserInput(questions, argv); - const promptAnswers = await prompts(questions); - - const answers = { - ...argv, - local, - ...singleChoiceAnswers, + const promptAnswers = await prompt(questions, argv); + const answers: Answers = { ...promptAnswers, - } as Required; + local, + }; assertUserInput(questions, answers); @@ -161,7 +154,7 @@ async function promptLocalLibrary(argv: Args) { if (hasPackageJson) { // If we're under a project with package.json, ask the user if they want to create a local library - const answers = await prompts({ + const answers = await prompt({ type: 'confirm', name: 'local', message: `Looks like you're under a project folder. Do you want to create a local library?`, @@ -181,7 +174,7 @@ async function promptPath(argv: Args, local: boolean) { if (argv.name && !local) { folder = path.join(process.cwd(), argv.name); } else { - const answers = await prompts({ + const answers = await prompt({ type: 'text', name: 'folder', message: `Where do you want to create the library?`, diff --git a/packages/create-react-native-library/src/input.ts b/packages/create-react-native-library/src/input.ts index 853388e07..6b02c9a21 100644 --- a/packages/create-react-native-library/src/input.ts +++ b/packages/create-react-native-library/src/input.ts @@ -1,8 +1,8 @@ -import { version } from '../package.json'; -import validateNpmPackage from 'validate-npm-package-name'; import githubUsername from 'github-username'; +import validateNpmPackage from 'validate-npm-package-name'; import type yargs from 'yargs'; -import type { PromptObject } from './utils/prompts'; +import { version } from '../package.json'; +import type { Question } from './utils/prompt'; import { spawn } from './utils/spawn'; export type ArgName = @@ -111,14 +111,6 @@ const TYPE_CHOICES: { }, ]; -export type Question = Omit< - PromptObject, - 'validate' | 'name' -> & { - validate?: (value: string) => boolean | string; - name: keyof Answers; -}; - export const acceptedArgs: Record = { slug: { description: 'Name of the npm package', @@ -180,8 +172,8 @@ export type Answers = { authorUrl: string; repoUrl: string; languages: ProjectLanguages; - type?: ProjectType; - example?: ExampleApp; + type: ProjectType; + example: ExampleApp; reactNativeVersion?: string; local?: boolean; }; @@ -189,11 +181,9 @@ export type Answers = { export async function createQuestions({ basename, local, - argv, }: { basename: string; local: boolean; - argv: Args; }) { let name, email; @@ -204,7 +194,7 @@ export async function createQuestions({ // Ignore error } - const initialQuestions: Question[] = [ + const questions: Question[] = [ { type: 'text', name: 'slug', @@ -295,7 +285,7 @@ export async function createQuestions({ ]; if (!local) { - initialQuestions.push({ + questions.push({ type: 'select', name: 'example', message: 'What type of example app do you want to create?', @@ -313,48 +303,7 @@ export async function createQuestions({ }); } - const singleChoiceAnswers: Partial = {}; - const finalQuestions: Question[] = []; - - for (const question of initialQuestions) { - // Skip questions which are passed as parameter and pass validation - const argValue = argv[question.name]; - if (argValue && question.validate?.(argValue) !== false) { - continue; - } - - // Don't prompt questions with a single choice - if (Array.isArray(question.choices) && question.choices.length === 1) { - const onlyChoice = question.choices[0]!; - singleChoiceAnswers[question.name] = onlyChoice.value; - - continue; - } - - const { type, choices } = question; - - // Don't prompt dynamic questions with a single choice - if (type === 'select' && typeof choices === 'function') { - question.type = (prev, values, prompt) => { - const dynamicChoices = choices(prev, { ...argv, ...values }, prompt); - - if (dynamicChoices && dynamicChoices.length === 1) { - const onlyChoice = dynamicChoices[0]!; - singleChoiceAnswers[question.name] = onlyChoice.value; - return null; - } - - return type; - }; - } - - finalQuestions.push(question); - } - - return { - questions: finalQuestions, - singleChoiceAnswers, - }; + return questions; } export function createMetadata(answers: Answers) { diff --git a/packages/create-react-native-library/src/template.ts b/packages/create-react-native-library/src/template.ts index 1d7c7d7d2..dd7507a12 100644 --- a/packages/create-react-native-library/src/template.ts +++ b/packages/create-react-native-library/src/template.ts @@ -99,7 +99,7 @@ export function generateTemplateConfiguration({ }: { bobVersion: string; basename: string; - answers: Required; + answers: Answers; }): TemplateConfiguration { const { slug, languages, type } = answers; diff --git a/packages/create-react-native-library/src/utils/assert.ts b/packages/create-react-native-library/src/utils/assert.ts index c666f3efa..4e277e294 100644 --- a/packages/create-react-native-library/src/utils/assert.ts +++ b/packages/create-react-native-library/src/utils/assert.ts @@ -1,6 +1,7 @@ import kleur from 'kleur'; import { spawn } from './spawn'; -import type { Answers, Args, Question } from '../input'; +import type { Answers, Args } from '../input'; +import type { Question } from './prompt'; export async function assertNpxExists() { try { @@ -25,8 +26,8 @@ export async function assertNpxExists() { * Makes sure the answers are in expected form and ends the process with error if they are not */ export function assertUserInput( - questions: Question[], - answers: Answers | Args + questions: Question[], + answers: Partial ) { for (const [key, value] of Object.entries(answers)) { if (value == null) { @@ -39,35 +40,40 @@ export function assertUserInput( continue; } - let valid = question.validate ? question.validate(String(value)) : true; + let validation; // We also need to guard against invalid choices // If we don't already have a validation message to provide a better error - if (typeof valid !== 'string' && 'choices' in question) { + if ('choices' in question) { const choices = typeof question.choices === 'function' - ? question.choices( - undefined, - // @ts-expect-error: it complains about optional values, but it should be fine - answers, - question - ) + ? question.choices(undefined, answers) : question.choices; - if (choices && !choices.some((choice) => choice.value === value)) { - valid = `Supported values are - ${choices.map((c) => - kleur.green(c.value) - )}`; + if (choices && choices.every((choice) => choice.value !== value)) { + if (choices.length > 1) { + validation = `Must be one of ${choices + .map((choice) => kleur.green(choice.value)) + .join(', ')}`; + } else if (choices[0]) { + validation = `Must be '${kleur.green(choices[0].value)}'`; + } else { + validation = false; + } } } - if (valid !== true) { + if (validation == null && question.validate) { + validation = question.validate(String(value)); + } + + if (validation != null && validation !== true) { let message = `Invalid value ${kleur.red( String(value) )} passed for ${kleur.blue(key)}`; - if (typeof valid === 'string') { - message += `: ${valid}`; + if (typeof validation === 'string') { + message += `: ${validation}`; } console.log(message); diff --git a/packages/create-react-native-library/src/utils/prompt.ts b/packages/create-react-native-library/src/utils/prompt.ts new file mode 100644 index 000000000..6543acd16 --- /dev/null +++ b/packages/create-react-native-library/src/utils/prompt.ts @@ -0,0 +1,98 @@ +import prompts from 'prompts'; + +type Choice = { + title: string; + value: string; + description?: string; +}; + +export type Question = Omit< + prompts.PromptObject, + 'validate' | 'name' | 'choices' +> & { + name: T; + validate?: (value: string) => boolean | string; + choices?: + | Choice[] + | ((prev: unknown, values: Partial>) => Choice[]); +}; + +/** + * Wrapper around `prompts` with additional features: + * + * - Improved type-safety + * - Read answers from passed arguments + * - Skip questions with a single choice + * - Exit on canceling the prompt + */ +export async function prompt( + questions: Question[] | Question, + argv?: Record, + options?: prompts.Options +) { + const singleChoiceAnswers = {}; + const promptQuestions = []; + + if (Array.isArray(questions)) { + for (const question of questions) { + // Skip questions which are passed as parameter and pass validation + const argValue = argv?.[question.name]; + + if (argValue && question.validate?.(argValue) !== false) { + continue; + } + + // Don't prompt questions with a single choice + if (Array.isArray(question.choices) && question.choices.length === 1) { + const onlyChoice = question.choices[0]; + + if (onlyChoice?.value) { + // @ts-expect-error assume the passed value is correct + singleChoiceAnswers[question.name] = onlyChoice.value; + } + + continue; + } + + const { type, choices } = question; + + // Don't prompt dynamic questions with a single choice + if (type === 'select' && typeof choices === 'function') { + question.type = (prev, values) => { + const dynamicChoices = choices(prev, { ...argv, ...values }); + + if (dynamicChoices && dynamicChoices.length === 1) { + const onlyChoice = dynamicChoices[0]; + + if (onlyChoice?.value) { + // @ts-expect-error assume the passed value is correct + singleChoiceAnswers[question.name] = onlyChoice.value; + } + + return null; + } + + return type; + }; + } + + promptQuestions.push(question); + } + } else { + promptQuestions.push(questions); + } + + const promptAnswers = await prompts(promptQuestions, { + onCancel() { + // Exit the CLI on cancel + process.exit(1); + }, + ...options, + }); + + return { + ...argv, + ...singleChoiceAnswers, + ...promptAnswers, + }; +} diff --git a/packages/create-react-native-library/src/utils/prompts.ts b/packages/create-react-native-library/src/utils/prompts.ts deleted file mode 100644 index 4ed4ae7b4..000000000 --- a/packages/create-react-native-library/src/utils/prompts.ts +++ /dev/null @@ -1,16 +0,0 @@ -import promptsModule from 'prompts'; - -export default function prompts( - args: promptsModule.PromptObject | promptsModule.PromptObject[], - options?: promptsModule.Options -) { - return promptsModule(args, { - onCancel() { - process.exit(1); - }, - ...options, - }); -} - -export type PromptObject = - promptsModule.PromptObject;