diff --git a/tooling/create-tauri-app/package.json b/tooling/create-tauri-app/package.json index f3a4389fd..6e0f81ecd 100644 --- a/tooling/create-tauri-app/package.json +++ b/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", diff --git a/tooling/create-tauri-app/src/index.ts b/tooling/create-tauri-app/src/index.ts index b8da562ea..d6e473067 100644 --- a/tooling/create-tauri-app/src/index.ts +++ b/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 => { 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 => { /* 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 => { - 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 - postInit?: ({ - cwd, - cfg, - packageManager - }: { - cwd: string - cfg: TauriBuildConfig - packageManager: PackageManager - }) => Promise -} - 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 => { +const runInit = async (argv: Argv): Promise => { + 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 => { 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 => { } 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 => { 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 => { // 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 => { 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 => { } 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) +} diff --git a/tooling/create-tauri-app/src/recipes/react.ts b/tooling/create-tauri-app/src/recipes/react.ts index 6b3cfa4f7..17136f6cf 100644 --- a/tooling/create-tauri-app/src/recipes/react.ts +++ b/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, diff --git a/tooling/create-tauri-app/src/recipes/vanilla.ts b/tooling/create-tauri-app/src/recipes/vanilla.ts index f7cb0d20e..8a69885bf 100644 --- a/tooling/create-tauri-app/src/recipes/vanilla.ts +++ b/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', diff --git a/tooling/create-tauri-app/src/recipes/vite.ts b/tooling/create-tauri-app/src/recipes/vite.ts index 86d977507..b2fdd96f5 100644 --- a/tooling/create-tauri-app/src/recipes/vite.ts +++ b/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 - } - ) - } - - 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(` diff --git a/tooling/create-tauri-app/src/recipes/vue-cli.ts b/tooling/create-tauri-app/src/recipes/vue-cli.ts index 1d69a917d..cc544485c 100644 --- a/tooling/create-tauri-app/src/recipes/vue-cli.ts +++ b/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', [ diff --git a/tooling/create-tauri-app/src/types/recipe.ts b/tooling/create-tauri-app/src/types/recipe.ts new file mode 100644 index 000000000..303ad7a1a --- /dev/null +++ b/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 + postInit?: (args: RecipeArgs) => Promise +} diff --git a/tooling/create-tauri-app/test/index.spec.ts b/tooling/create-tauri-app/test/index.spec.ts index 62b8f997e..d4210b74b 100644 --- a/tooling/create-tauri-app/test/index.spec.ts +++ b/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() + }) + ) } }