Browse Source

refactor(cta): add questions hook for recipes (#1658)

* refactor(cta): add questions hook for recipes

* fix lint

* fix tests

* fix vue-cli

* one more time

* adjust vuecli to packagemanager

* revert bundle.js changes

* refacotr: extract `Recipe` type to its own file

* colorful step logging

* pin chalk dependency

* Restore bundle.js

* fix typo

* more styling
Amr Bashir 4 years ago
parent
commit
9fadbf3350

+ 1 - 0
tooling/create-tauri-app/package.json

@@ -49,6 +49,7 @@
     "@types/semver": "7.3.5",
     "@typescript-eslint/eslint-plugin": "4.22.0",
     "@typescript-eslint/parser": "4.22.0",
+    "chalk": "4.1.1",
     "eslint": "7.25.0",
     "eslint-config-prettier": "8.3.0",
     "eslint-config-standard-with-typescript": "20.0.0",

+ 85 - 89
tooling/create-tauri-app/src/index.ts

@@ -4,21 +4,16 @@
 
 import minimist from 'minimist'
 import inquirer from 'inquirer'
+import { bold, cyan, green, reset, yellow } from 'chalk'
 import { resolve, join } from 'path'
-
-import { TauriBuildConfig } from './types/config'
 import { reactjs, reactts } from './recipes/react'
 import { vuecli } from './recipes/vue-cli'
 import { vanillajs } from './recipes/vanilla'
 import { vite } from './recipes/vite'
-import {
-  install,
-  checkPackageManager,
-  PackageManager
-} from './dependency-manager'
-
+import { install, checkPackageManager } from './dependency-manager'
 import { shell } from './shell'
 import { addTauriScript } from './helpers/add-tauri-script'
+import { Recipe } from './types/recipe'
 
 interface Argv {
   h: boolean
@@ -90,8 +85,7 @@ export const createTauriApp = async (cliArgs: string[]): Promise<any> => {
       P: 'dev-path',
       r: 'recipe'
     },
-    boolean: ['h', 'l', 'ci', 'dev'],
-    default: { A: 'tauri-app', r: 'vanillajs' }
+    boolean: ['h', 'l', 'ci', 'dev']
   }) as unknown) as Argv
 
   if (argv.help) {
@@ -108,9 +102,7 @@ export const createTauriApp = async (cliArgs: string[]): Promise<any> => {
     /* eslint-enable @typescript-eslint/no-unsafe-member-access */
   }
 
-  return await getOptionsInteractive(argv, !argv.ci).then(
-    async (responses) => await runInit(argv, responses)
-  )
+  return await runInit(argv)
 }
 
 interface Responses {
@@ -119,31 +111,41 @@ interface Responses {
   recipeName: string
 }
 
-const getOptionsInteractive = async (
-  argv: Argv,
-  ask: boolean
-): Promise<Responses> => {
+const allRecipes: Recipe[] = [vanillajs, reactjs, reactts, vite, vuecli]
+
+const recipeByShortName = (name: string): Recipe | undefined =>
+  allRecipes.find((r) => r.shortName === name)
+
+const recipeByDescriptiveName = (name: string): Recipe | undefined =>
+  allRecipes.find((r) => r.descriptiveName === name)
+
+const recipeShortNames = allRecipes.map((r) => r.shortName)
+
+const recipeDescriptiveNames = allRecipes.map((r) => r.descriptiveName)
+
+const runInit = async (argv: Argv): Promise<void> => {
   const defaults = {
-    appName: argv.A,
+    appName: 'tauri-app',
     tauri: { window: { title: 'Tauri App' } },
-    recipeName: argv.r
+    recipeName: 'vanillajs'
   }
 
-  return (await inquirer
+  // prompt initial questions
+  const answers = (await inquirer
     .prompt([
       {
         type: 'input',
         name: 'appName',
         message: 'What is your app name?',
         default: defaults.appName,
-        when: ask && !argv.A
+        when: !argv.ci && !argv.A
       },
       {
         type: 'input',
         name: 'tauri.window.title',
         message: 'What should the window title be?',
         default: defaults.tauri.window.title,
-        when: ask && !argv.W
+        when: !argv.ci && !argv.W
       },
       {
         type: 'list',
@@ -151,14 +153,10 @@ const getOptionsInteractive = async (
         message: 'Would you like to add a UI recipe?',
         choices: recipeDescriptiveNames,
         default: defaults.recipeName,
-        when: ask && !argv.r
+        when: !argv.ci && !argv.r
       }
     ])
-    .then((answers: Argv) => ({
-      ...defaults,
-      ...answers
-    }))
-    .catch(async (error: { isTtyError: boolean }) => {
+    .catch((error: { isTtyError: boolean }) => {
       if (error.isTtyError) {
         // Prompt couldn't be rendered in the current environment
         console.warn(
@@ -168,70 +166,17 @@ const getOptionsInteractive = async (
         // Something else went wrong
         console.error('An unknown error occurred:', error)
       }
-      return await runInit(argv, defaults)
     })) as Responses
-}
-
-export interface Recipe {
-  descriptiveName: string
-  shortName: string
-  configUpdate?: ({
-    cfg,
-    packageManager
-  }: {
-    cfg: TauriBuildConfig
-    packageManager: PackageManager
-  }) => TauriBuildConfig
-  extraNpmDependencies: string[]
-  extraNpmDevDependencies: string[]
-  preInit?: ({
-    cwd,
-    cfg,
-    packageManager
-  }: {
-    cwd: string
-    cfg: TauriBuildConfig
-    packageManager: PackageManager
-  }) => Promise<void>
-  postInit?: ({
-    cwd,
-    cfg,
-    packageManager
-  }: {
-    cwd: string
-    cfg: TauriBuildConfig
-    packageManager: PackageManager
-  }) => Promise<void>
-}
-
-const allRecipes: Recipe[] = [vanillajs, reactjs, reactts, vite, vuecli]
 
-const recipeByShortName = (name: string): Recipe | undefined =>
-  allRecipes.find((r) => r.shortName === name)
-
-const recipeByDescriptiveName = (name: string): Recipe | undefined =>
-  allRecipes.find((r) => r.descriptiveName === name)
-
-const recipeShortNames = allRecipes.map((r) => r.shortName)
-
-const recipeDescriptiveNames = allRecipes.map((r) => r.descriptiveName)
-
-const runInit = async (argv: Argv, config: Responses): Promise<void> => {
   const {
     appName,
     recipeName,
     tauri: {
       window: { title }
     }
-  } = config
-  // this little fun snippet pulled from vite determines the package manager the script was run from
-  // @ts-expect-error
-  const packageManager = /yarn/.test(process?.env?.npm_execpath)
-    ? 'yarn'
-    : 'npm'
+  } = { ...defaults, ...answers }
 
   let recipe: Recipe | undefined
-
   if (argv.r) {
     recipe = recipeByShortName(argv.r)
   } else if (recipeName !== undefined) {
@@ -240,6 +185,15 @@ const runInit = async (argv: Argv, config: Responses): Promise<void> => {
 
   if (!recipe) throw new Error('Could not find the recipe specified.')
 
+  const packageManager =
+    argv.m === 'yarn' || argv.m === 'npm'
+      ? argv.m
+      : // @ts-expect-error
+      // this little fun snippet pulled from vite determines the package manager the script was run from
+      /yarn/.test(process?.env?.npm_execpath)
+      ? 'yarn'
+      : 'npm'
+
   const buildConfig = {
     distDir: argv.D,
     devPath: argv.P,
@@ -248,11 +202,40 @@ const runInit = async (argv: Argv, config: Responses): Promise<void> => {
   }
 
   const directory = argv.d || process.cwd()
+
+  // prompt additional recipe questions
+  let recipeAnswers
+  if (recipe.extraQuestions) {
+    recipeAnswers = await inquirer
+      .prompt(
+        recipe.extraQuestions({
+          cfg: buildConfig,
+          packageManager,
+          ci: argv.ci,
+          cwd: directory
+        })
+      )
+      .catch((error: { isTtyError: boolean }) => {
+        if (error.isTtyError) {
+          // Prompt couldn't be rendered in the current environment
+          console.warn(
+            'It appears your terminal does not support interactive prompts. Using default values.'
+          )
+        } else {
+          // Something else went wrong
+          console.error('An unknown error occurred:', error)
+        }
+      })
+  }
+
   let updatedConfig
   if (recipe.configUpdate) {
     updatedConfig = recipe.configUpdate({
       cfg: buildConfig,
-      packageManager
+      packageManager,
+      ci: argv.ci,
+      cwd: directory,
+      answers: recipeAnswers
     })
   }
   const cfg = {
@@ -269,8 +252,14 @@ const runInit = async (argv: Argv, config: Responses): Promise<void> => {
   await checkPackageManager({ cwd: directory, packageManager })
 
   if (recipe.preInit) {
-    console.log('===== running initial command(s) =====')
-    await recipe.preInit({ cwd: directory, cfg, packageManager })
+    logStep('Running initial command(s)')
+    await recipe.preInit({
+      cwd: directory,
+      cfg,
+      packageManager,
+      ci: argv.ci,
+      answers: recipeAnswers
+    })
   }
 
   const initArgs = [
@@ -288,7 +277,7 @@ const runInit = async (argv: Argv, config: Responses): Promise<void> => {
 
   // Vue CLI plugin automatically runs these
   if (recipe.shortName !== 'vuecli') {
-    console.log('===== installing any additional needed deps =====')
+    logStep('Installing any additional needed dependencies')
     if (argv.dev) {
       await shell('yarn', ['link', '@tauri-apps/cli'], {
         cwd: appDirectory
@@ -307,7 +296,7 @@ const runInit = async (argv: Argv, config: Responses): Promise<void> => {
       packageManager
     })
 
-    console.log('===== running tauri init =====')
+    logStep(`Running: ${reset(yellow('tauri init'))}`)
     addTauriScript(appDirectory)
 
     const binary = !argv.b ? packageManager : resolve(appDirectory, argv.b)
@@ -321,11 +310,18 @@ const runInit = async (argv: Argv, config: Responses): Promise<void> => {
   }
 
   if (recipe.postInit) {
-    console.log('===== running final command(s) =====')
+    logStep('Running final command(s)')
     await recipe.postInit({
       cwd: appDirectory,
       cfg,
-      packageManager
+      packageManager,
+      ci: argv.ci,
+      answers: recipeAnswers
     })
   }
 }
+
+function logStep(msg: string): void {
+  const out = `${green('>>')} ${bold(cyan(msg))}`
+  console.log(out)
+}

+ 1 - 1
tooling/create-tauri-app/src/recipes/react.ts

@@ -2,11 +2,11 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-import { Recipe } from '..'
 import { join } from 'path'
 // @ts-expect-error
 import scaffe from 'scaffe'
 import { shell } from '../shell'
+import { Recipe } from '../types/recipe'
 
 const afterCra = async (
   cwd: string,

+ 1 - 1
tooling/create-tauri-app/src/recipes/vanilla.ts

@@ -2,10 +2,10 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-import { Recipe } from '..'
 import { join } from 'path'
 // @ts-expect-error
 import scaffe from 'scaffe'
+import { Recipe } from '../types/recipe'
 
 export const vanillajs: Recipe = {
   descriptiveName: 'Vanilla.js',

+ 42 - 49
tooling/create-tauri-app/src/recipes/vite.ts

@@ -2,13 +2,12 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-import { Recipe } from '..'
 import { join } from 'path'
 import { readdirSync } from 'fs'
 // @ts-expect-error
 import scaffe from 'scaffe'
 import { shell } from '../shell'
-import inquirer from 'inquirer'
+import { Recipe } from '../types/recipe'
 
 const afterViteCA = async (
   cwd: string,
@@ -41,56 +40,50 @@ const vite: Recipe = {
   }),
   extraNpmDevDependencies: [],
   extraNpmDependencies: [],
-  preInit: async ({ cwd, cfg, packageManager }) => {
-    try {
-      const { template } = (await inquirer.prompt([
-        {
-          type: 'list',
-          name: 'template',
-          message: 'Which vite template would you like to use?',
-          choices: readdirSync(join(__dirname, '../src/templates/vite')),
-          default: 'vue'
-        }
-      ])) as { template: string }
-
-      // Vite creates the folder for you
-      if (packageManager === 'yarn') {
-        await shell(
-          'yarn',
-          [
-            'create',
-            '@vitejs/app',
-            `${cfg.appName}`,
-            '--template',
-            `${template}`
-          ],
-          {
-            cwd
-          }
-        )
-      } else {
-        await shell(
-          'npx',
-          ['@vitejs/create-app', `${cfg.appName}`, '--template', `${template}`],
-          {
-            cwd
-          }
-        )
+  extraQuestions: ({ ci }) => {
+    return [
+      {
+        type: 'list',
+        name: 'template',
+        message: 'Which vite template would you like to use?',
+        choices: readdirSync(join(__dirname, '../src/templates/vite')),
+        default: 'vue',
+        when: !ci
       }
+    ]
+  },
+  preInit: async ({ cwd, cfg, packageManager, answers }) => {
+    let template = 'vue'
+    if (answers) {
+      template = answers.template ? (answers.template as string) : 'vue'
+    }
 
-      await afterViteCA(cwd, cfg.appName, template)
-    } catch (error) {
-      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
-      if (error?.isTtyError) {
-        // Prompt couldn't be rendered in the current environment
-        console.log(
-          'It appears your terminal does not support interactive prompts. Using default values.'
-        )
-      } else {
-        // Something else went wrong
-        console.error('An unknown error occurred:', error)
-      }
+    // Vite creates the folder for you
+    if (packageManager === 'yarn') {
+      await shell(
+        'yarn',
+        [
+          'create',
+          '@vitejs/app',
+          `${cfg.appName}`,
+          '--template',
+          `${template}`
+        ],
+        {
+          cwd
+        }
+      )
+    } else {
+      await shell(
+        'npx',
+        ['@vitejs/create-app', `${cfg.appName}`, '--template', `${template}`],
+        {
+          cwd
+        }
+      )
     }
+
+    await afterViteCA(cwd, cfg.appName, template)
   },
   postInit: async ({ packageManager }) => {
     console.log(`

+ 14 - 3
tooling/create-tauri-app/src/recipes/vue-cli.ts

@@ -2,9 +2,9 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-import { Recipe } from '..'
 import { join } from 'path'
 import { shell } from '../shell'
+import { Recipe } from '../types/recipe'
 
 const completeLogMsg = `
   Your installation completed.
@@ -17,9 +17,20 @@ const vuecli: Recipe = {
   extraNpmDevDependencies: [],
   extraNpmDependencies: [],
   configUpdate: ({ cfg }) => cfg,
-  preInit: async ({ cwd, cfg }) => {
+  preInit: async ({ cwd, cfg, ci, packageManager }) => {
     // Vue CLI creates the folder for you
-    await shell('npx', ['@vue/cli', 'create', `${cfg.appName}`], { cwd })
+    await shell(
+      'npx',
+      [
+        '@vue/cli',
+        'create',
+        `${cfg.appName}`,
+        '--packageManager',
+        packageManager,
+        ci ? '--default' : ''
+      ],
+      { cwd }
+    )
     await shell(
       'npx',
       [

+ 23 - 0
tooling/create-tauri-app/src/types/recipe.ts

@@ -0,0 +1,23 @@
+import { Answers, QuestionCollection } from 'inquirer'
+import { PackageManager } from '../dependency-manager'
+import { TauriBuildConfig } from './config'
+
+export interface RecipeArgs {
+  cwd: string
+  cfg: TauriBuildConfig
+  packageManager: PackageManager
+  ci: boolean
+  // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
+  answers?: undefined | void | Answers
+}
+
+export interface Recipe {
+  descriptiveName: string
+  shortName: string
+  configUpdate?: (args: RecipeArgs) => TauriBuildConfig
+  extraNpmDependencies: string[]
+  extraNpmDevDependencies: string[]
+  extraQuestions?: (args: RecipeArgs) => QuestionCollection[]
+  preInit?: (args: RecipeArgs) => Promise<void>
+  postInit?: (args: RecipeArgs) => Promise<void>
+}

+ 20 - 2
tooling/create-tauri-app/test/index.spec.ts

@@ -102,9 +102,12 @@ describe('CTA', () => {
           //  and then run that test suite instead
           let opts: string[] = []
           if (manager === 'npm') {
-            opts = ['run', 'tauri', '--', 'build']
+            opts =
+              recipe == 'vuecli'
+                ? ['run', 'tauri:build']
+                : ['run', 'tauri', '--', 'build']
           } else if (manager === 'yarn') {
-            opts = ['tauri', 'build']
+            opts = recipe == 'vuecli' ? ['tauri:build'] : ['tauri', 'build']
           }
           const tauriBuild = await execa(manager, opts, {
             all: true,
@@ -148,6 +151,21 @@ describe('CTA', () => {
                   tauri: 'tauri'
                 })
               )
+            },
+            vite: () => {
+              expect(packageFileOutput['scripts']).toEqual(
+                expect.objectContaining({
+                  tauri: 'tauri'
+                })
+              )
+            },
+            vuecli: () => {
+              expect(packageFileOutput['scripts']).toEqual(
+                expect.objectContaining({
+                  'tauri:build': expect.anything(),
+                  'tauri:serve': expect.anything()
+                })
+              )
             }
           }