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", "@types/semver": "7.3.5",
"@typescript-eslint/eslint-plugin": "4.22.0", "@typescript-eslint/eslint-plugin": "4.22.0",
"@typescript-eslint/parser": "4.22.0", "@typescript-eslint/parser": "4.22.0",
"chalk": "4.1.1",
"eslint": "7.25.0", "eslint": "7.25.0",
"eslint-config-prettier": "8.3.0", "eslint-config-prettier": "8.3.0",
"eslint-config-standard-with-typescript": "20.0.0", "eslint-config-standard-with-typescript": "20.0.0",

View File

@ -4,21 +4,16 @@
import minimist from 'minimist' import minimist from 'minimist'
import inquirer from 'inquirer' import inquirer from 'inquirer'
import { bold, cyan, green, reset, yellow } from 'chalk'
import { resolve, join } from 'path' import { resolve, join } from 'path'
import { TauriBuildConfig } from './types/config'
import { reactjs, reactts } from './recipes/react' import { reactjs, reactts } from './recipes/react'
import { vuecli } from './recipes/vue-cli' import { vuecli } from './recipes/vue-cli'
import { vanillajs } from './recipes/vanilla' import { vanillajs } from './recipes/vanilla'
import { vite } from './recipes/vite' import { vite } from './recipes/vite'
import { import { install, checkPackageManager } from './dependency-manager'
install,
checkPackageManager,
PackageManager
} from './dependency-manager'
import { shell } from './shell' import { shell } from './shell'
import { addTauriScript } from './helpers/add-tauri-script' import { addTauriScript } from './helpers/add-tauri-script'
import { Recipe } from './types/recipe'
interface Argv { interface Argv {
h: boolean h: boolean
@ -90,8 +85,7 @@ export const createTauriApp = async (cliArgs: string[]): Promise<any> => {
P: 'dev-path', P: 'dev-path',
r: 'recipe' r: 'recipe'
}, },
boolean: ['h', 'l', 'ci', 'dev'], boolean: ['h', 'l', 'ci', 'dev']
default: { A: 'tauri-app', r: 'vanillajs' }
}) as unknown) as Argv }) as unknown) as Argv
if (argv.help) { if (argv.help) {
@ -108,9 +102,7 @@ export const createTauriApp = async (cliArgs: string[]): Promise<any> => {
/* eslint-enable @typescript-eslint/no-unsafe-member-access */ /* eslint-enable @typescript-eslint/no-unsafe-member-access */
} }
return await getOptionsInteractive(argv, !argv.ci).then( return await runInit(argv)
async (responses) => await runInit(argv, responses)
)
} }
interface Responses { interface Responses {
@ -119,91 +111,6 @@ interface Responses {
recipeName: string 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 allRecipes: Recipe[] = [vanillajs, reactjs, reactts, vite, vuecli]
const recipeByShortName = (name: string): Recipe | undefined => 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 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 { const {
appName, appName,
recipeName, recipeName,
tauri: { tauri: {
window: { title } window: { title }
} }
} = config } = { ...defaults, ...answers }
// 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'
let recipe: Recipe | undefined let recipe: Recipe | undefined
if (argv.r) { if (argv.r) {
recipe = recipeByShortName(argv.r) recipe = recipeByShortName(argv.r)
} else if (recipeName !== undefined) { } 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.') 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 = { const buildConfig = {
distDir: argv.D, distDir: argv.D,
devPath: argv.P, devPath: argv.P,
@ -248,11 +202,40 @@ const runInit = async (argv: Argv, config: Responses): Promise<void> => {
} }
const directory = argv.d || process.cwd() 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 let updatedConfig
if (recipe.configUpdate) { if (recipe.configUpdate) {
updatedConfig = recipe.configUpdate({ updatedConfig = recipe.configUpdate({
cfg: buildConfig, cfg: buildConfig,
packageManager packageManager,
ci: argv.ci,
cwd: directory,
answers: recipeAnswers
}) })
} }
const cfg = { const cfg = {
@ -269,8 +252,14 @@ const runInit = async (argv: Argv, config: Responses): Promise<void> => {
await checkPackageManager({ cwd: directory, packageManager }) await checkPackageManager({ cwd: directory, packageManager })
if (recipe.preInit) { if (recipe.preInit) {
console.log('===== running initial command(s) =====') logStep('Running initial command(s)')
await recipe.preInit({ cwd: directory, cfg, packageManager }) await recipe.preInit({
cwd: directory,
cfg,
packageManager,
ci: argv.ci,
answers: recipeAnswers
})
} }
const initArgs = [ const initArgs = [
@ -288,7 +277,7 @@ const runInit = async (argv: Argv, config: Responses): Promise<void> => {
// Vue CLI plugin automatically runs these // Vue CLI plugin automatically runs these
if (recipe.shortName !== 'vuecli') { if (recipe.shortName !== 'vuecli') {
console.log('===== installing any additional needed deps =====') logStep('Installing any additional needed dependencies')
if (argv.dev) { if (argv.dev) {
await shell('yarn', ['link', '@tauri-apps/cli'], { await shell('yarn', ['link', '@tauri-apps/cli'], {
cwd: appDirectory cwd: appDirectory
@ -307,7 +296,7 @@ const runInit = async (argv: Argv, config: Responses): Promise<void> => {
packageManager packageManager
}) })
console.log('===== running tauri init =====') logStep(`Running: ${reset(yellow('tauri init'))}`)
addTauriScript(appDirectory) addTauriScript(appDirectory)
const binary = !argv.b ? packageManager : resolve(appDirectory, argv.b) 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) { if (recipe.postInit) {
console.log('===== running final command(s) =====') logStep('Running final command(s)')
await recipe.postInit({ await recipe.postInit({
cwd: appDirectory, cwd: appDirectory,
cfg, 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: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { Recipe } from '..'
import { join } from 'path' import { join } from 'path'
// @ts-expect-error // @ts-expect-error
import scaffe from 'scaffe' import scaffe from 'scaffe'
import { shell } from '../shell' import { shell } from '../shell'
import { Recipe } from '../types/recipe'
const afterCra = async ( const afterCra = async (
cwd: string, cwd: string,

View File

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

View File

@ -2,13 +2,12 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { Recipe } from '..'
import { join } from 'path' import { join } from 'path'
import { readdirSync } from 'fs' import { readdirSync } from 'fs'
// @ts-expect-error // @ts-expect-error
import scaffe from 'scaffe' import scaffe from 'scaffe'
import { shell } from '../shell' import { shell } from '../shell'
import inquirer from 'inquirer' import { Recipe } from '../types/recipe'
const afterViteCA = async ( const afterViteCA = async (
cwd: string, cwd: string,
@ -41,56 +40,50 @@ const vite: Recipe = {
}), }),
extraNpmDevDependencies: [], extraNpmDevDependencies: [],
extraNpmDependencies: [], extraNpmDependencies: [],
preInit: async ({ cwd, cfg, packageManager }) => { extraQuestions: ({ ci }) => {
try { return [
const { template } = (await inquirer.prompt([ {
{ type: 'list',
type: 'list', name: 'template',
name: 'template', message: 'Which vite template would you like to use?',
message: 'Which vite template would you like to use?', choices: readdirSync(join(__dirname, '../src/templates/vite')),
choices: readdirSync(join(__dirname, '../src/templates/vite')), default: 'vue',
default: 'vue' when: !ci
}
])) 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)
} }
]
},
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 }) => { postInit: async ({ packageManager }) => {
console.log(` console.log(`

View File

@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { Recipe } from '..'
import { join } from 'path' import { join } from 'path'
import { shell } from '../shell' import { shell } from '../shell'
import { Recipe } from '../types/recipe'
const completeLogMsg = ` const completeLogMsg = `
Your installation completed. Your installation completed.
@ -17,9 +17,20 @@ const vuecli: Recipe = {
extraNpmDevDependencies: [], extraNpmDevDependencies: [],
extraNpmDependencies: [], extraNpmDependencies: [],
configUpdate: ({ cfg }) => cfg, configUpdate: ({ cfg }) => cfg,
preInit: async ({ cwd, cfg }) => { preInit: async ({ cwd, cfg, ci, packageManager }) => {
// Vue CLI creates the folder for you // 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( await shell(
'npx', '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 // and then run that test suite instead
let opts: string[] = [] let opts: string[] = []
if (manager === 'npm') { if (manager === 'npm') {
opts = ['run', 'tauri', '--', 'build'] opts =
recipe == 'vuecli'
? ['run', 'tauri:build']
: ['run', 'tauri', '--', 'build']
} else if (manager === 'yarn') { } else if (manager === 'yarn') {
opts = ['tauri', 'build'] opts = recipe == 'vuecli' ? ['tauri:build'] : ['tauri', 'build']
} }
const tauriBuild = await execa(manager, opts, { const tauriBuild = await execa(manager, opts, {
all: true, all: true,
@ -148,6 +151,21 @@ describe('CTA', () => {
tauri: 'tauri' 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()
})
)
} }
} }