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
This commit is contained in:
Amr Bashir 2021-05-01 03:48:21 +02:00 committed by GitHub
parent b0bb796a42
commit 9fadbf3350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 212 additions and 170 deletions

View File

@ -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",

View File

@ -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,91 +111,6 @@ interface Responses {
recipeName: string
}
const getOptionsInteractive = async (
argv: Argv,
ask: boolean
): Promise<Responses> => {
const defaults = {
appName: argv.A,
tauri: { window: { title: 'Tauri App' } },
recipeName: argv.r
}
return (await inquirer
.prompt([
{
type: 'input',
name: 'appName',
message: 'What is your app name?',
default: defaults.appName,
when: ask && !argv.A
},
{
type: 'input',
name: 'tauri.window.title',
message: 'What should the window title be?',
default: defaults.tauri.window.title,
when: ask && !argv.W
},
{
type: 'list',
name: 'recipeName',
message: 'Would you like to add a UI recipe?',
choices: recipeDescriptiveNames,
default: defaults.recipeName,
when: ask && !argv.r
}
])
.then((answers: Argv) => ({
...defaults,
...answers
}))
.catch(async (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)
}
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 =>
@ -216,22 +123,60 @@ const recipeShortNames = allRecipes.map((r) => r.shortName)
const recipeDescriptiveNames = allRecipes.map((r) => r.descriptiveName)
const runInit = async (argv: Argv, config: Responses): Promise<void> => {
const runInit = async (argv: Argv): Promise<void> => {
const defaults = {
appName: 'tauri-app',
tauri: { window: { title: 'Tauri App' } },
recipeName: 'vanillajs'
}
// prompt initial questions
const answers = (await inquirer
.prompt([
{
type: 'input',
name: 'appName',
message: 'What is your app name?',
default: defaults.appName,
when: !argv.ci && !argv.A
},
{
type: 'input',
name: 'tauri.window.title',
message: 'What should the window title be?',
default: defaults.tauri.window.title,
when: !argv.ci && !argv.W
},
{
type: 'list',
name: 'recipeName',
message: 'Would you like to add a UI recipe?',
choices: recipeDescriptiveNames,
default: defaults.recipeName,
when: !argv.ci && !argv.r
}
])
.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)
}
})) as Responses
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)
}

View File

@ -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,

View File

@ -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',

View File

@ -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
}
)
}
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)
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'
}
// 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(`

View File

@ -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',
[

View File

@ -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>
}

View File

@ -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()
})
)
}
}