diff --git a/.changes/cta-shift-and-type.md b/.changes/cta-shift-and-type.md new file mode 100644 index 000000000..c9ca82e8d --- /dev/null +++ b/.changes/cta-shift-and-type.md @@ -0,0 +1,5 @@ +--- +"create-tauri-app": patch +--- + +Shift everything out of the `bin` and into `.ts` so we can apply Typescript types. diff --git a/tooling/create-tauri-app/.eslintrc.js b/tooling/create-tauri-app/.eslintrc.js index 0eb2bd097..3e72aec2f 100644 --- a/tooling/create-tauri-app/.eslintrc.js +++ b/tooling/create-tauri-app/.eslintrc.js @@ -7,7 +7,7 @@ module.exports = { }, parser: '@typescript-eslint/parser', - ignorePatterns: ['.eslintrc.js', 'jest.config.js', 'test/**/*'], + ignorePatterns: ['.eslintrc.js', '*.config.js', 'test', 'bin'], extends: [ 'standard-with-typescript', 'plugin:@typescript-eslint/recommended-requiring-type-checking', diff --git a/tooling/create-tauri-app/bin/create-tauri-app.js b/tooling/create-tauri-app/bin/create-tauri-app.js index 0def3fafa..e9b6cf624 100755 --- a/tooling/create-tauri-app/bin/create-tauri-app.js +++ b/tooling/create-tauri-app/bin/create-tauri-app.js @@ -3,249 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -const parseArgs = require('minimist') -const inquirer = require('inquirer') -const { resolve, join } = require('path') -const { - recipeShortNames, - recipeDescriptiveNames, - recipeByDescriptiveName, - recipeByShortName, - install, - checkPackageManager, - shell, - addTauriScript -} = require('../dist/') - -/** - * @type {object} - * @property {boolean} h - * @property {boolean} help - * @property {boolean} v - * @property {boolean} version - * @property {string|boolean} f - * @property {string|boolean} force - * @property {boolean} l - * @property {boolean} log - * @property {boolean} d - * @property {boolean} directory - * @property {boolean} dev - * @property {string} r - * @property {string} recipe - */ -const createTauriApp = async (cliArgs) => { - const argv = parseArgs(cliArgs, { - alias: { - h: 'help', - v: 'version', - f: 'force', - l: 'log', - m: 'manager', - d: 'directory', - dev: 'dev', - t: 'tauri-path', - A: 'app-name', - W: 'window-title', - D: 'dist-dir', - P: 'dev-path', - r: 'recipe' - }, - boolean: ['h', 'l', 'ci', 'dev'] - }) - - if (argv.help) { - printUsage() - return 0 - } - - if (argv.v) { - console.log(require('../package.json').version) - return false // do this for node consumers and tests - } - - return getOptionsInteractive(argv, !argv.ci).then((responses) => - runInit(argv, responses) - ) -} - -function printUsage() { - console.log(` - Description - Starts a new tauri app from a "recipe" or pre-built template. - Usage - $ yarn create tauri-app # npm create-tauri-app - Options - --help, -h Displays this message - -v, --version Displays the Tauri CLI version - --ci Skip prompts - --force, -f Force init to overwrite [conf|template|all] - --log, -l Logging [boolean] - --manager, -d Set package manager to use [npm|yarn] - --directory, -d Set target directory for init - --binary, -b Optional path to a tauri binary from which to run init - --app-name, -A Name of your Tauri application - --window-title, -W Window title of your Tauri application - --dist-dir, -D Web assets location, relative to /src-tauri - --dev-path, -P Url of your dev server - --recipe, -r Add UI framework recipe. None by default. - Supported recipes: [${recipeShortNames.join('|')}] - `) -} - -const getOptionsInteractive = (argv, ask) => { - const defaults = { - appName: argv.A || 'tauri-app', - tauri: { window: { title: 'Tauri App' } }, - recipeName: argv.r || 'vanillajs' - } - - return 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) => ({ - ...defaults, - ...answers - })) - .catch((error) => { - 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.' - ) - runInit() - } else { - // Something else went wrong - console.error('An unknown error occurred:', error) - } - }) -} - -async function runInit(argv, config = {}) { - const { - appName, - recipeName, - tauri: { - window: { title } - } - } = config - // this little fun snippet pulled from vite determines the package manager the script was run from - const packageManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm' - - let recipe - - if (argv.r) { - recipe = recipeByShortName(argv.r) - } else if (recipeName !== undefined) { - recipe = recipeByDescriptiveName(recipeName) - } - - let buildConfig = { - distDir: argv.D, - devPath: argv.P - } - - if (recipe !== undefined) { - buildConfig = recipe.configUpdate({ buildConfig, packageManager }) - } - - const directory = argv.d || process.cwd() - const cfg = { - ...buildConfig, - appName: appName || argv.A, - windowTitle: title || argv.w - } - - // note that our app directory is reliant on the appName and - // generally there are issues if the path has spaces (see Windows) - // future TODO prevent app names with spaces or escape here? - const appDirectory = join(directory, cfg.appName) - - // this throws an error if we can't run the package manager they requested - await checkPackageManager({ cwd: directory, packageManager }) - - if (recipe.preInit) { - console.log('===== running initial command(s) =====') - await recipe.preInit({ cwd: directory, cfg, packageManager }) - } - - const initArgs = [ - ['--app-name', cfg.appName], - ['--window-title', cfg.windowTitle], - ['--dist-dir', cfg.distDir], - ['--dev-path', cfg.devPath] - ].reduce((final, argSet) => { - if (argSet[1]) { - return final.concat(argSet) - } else { - return final - } - }, []) - - // Vue CLI plugin automatically runs these - if (recipe.shortName !== 'vuecli') { - console.log('===== installing any additional needed deps =====') - if (argv.dev) { - await shell('yarn', ['link', '@tauri-apps/cli'], { - cwd: appDirectory - }) - await shell('yarn', ['link', '@tauri-apps/api'], { - cwd: appDirectory - }) - } - - await install({ - appDir: appDirectory, - dependencies: recipe.extraNpmDependencies, - devDependencies: argv.dev - ? [...recipe.extraNpmDevDependencies] - : ['@tauri-apps/cli'].concat(recipe.extraNpmDevDependencies), - packageManager - }) - - console.log('===== running tauri init =====') - addTauriScript(appDirectory) - - const binary = !argv.b ? packageManager : resolve(appDirectory, argv.b) - const runTauriArgs = - packageManager === 'npm' && !argv.b - ? ['run', 'tauri', '--', 'init'] - : ['tauri', 'init'] - await shell(binary, [...runTauriArgs, ...initArgs, '--ci'], { - cwd: appDirectory - }) - } - - if (recipe.postInit) { - console.log('===== running final command(s) =====') - await recipe.postInit({ - cwd: appDirectory, - cfg, - packageManager - }) - } -} +const { createTauriApp } = require('../dist/') createTauriApp(process.argv.slice(2)).catch((err) => { console.error(err) diff --git a/tooling/create-tauri-app/jest.config.js b/tooling/create-tauri-app/jest.config.js index 895b7e211..c435bfffc 100644 --- a/tooling/create-tauri-app/jest.config.js +++ b/tooling/create-tauri-app/jest.config.js @@ -6,8 +6,6 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', modulePathIgnorePatterns: ['__fixtures__'], - testMatch: ['/test/**/*.spec.ts'], - moduleFileExtensions: ['ts', 'js', 'json'], globals: { 'ts-jest': { tsconfig: 'tsconfig.json' diff --git a/tooling/create-tauri-app/package.json b/tooling/create-tauri-app/package.json index 2c0525c11..f3a4389fd 100644 --- a/tooling/create-tauri-app/package.json +++ b/tooling/create-tauri-app/package.json @@ -45,6 +45,7 @@ "@types/cross-spawn": "6.0.2", "@types/inquirer": "7.3.1", "@types/jest": "26.0.23", + "@types/minimist": "1.2.1", "@types/semver": "7.3.5", "@typescript-eslint/eslint-plugin": "4.22.0", "@typescript-eslint/parser": "4.22.0", diff --git a/tooling/create-tauri-app/src/index.ts b/tooling/create-tauri-app/src/index.ts index 2e0537926..b8da562ea 100644 --- a/tooling/create-tauri-app/src/index.ts +++ b/tooling/create-tauri-app/src/index.ts @@ -2,16 +2,175 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +import minimist from 'minimist' +import inquirer from 'inquirer' +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 { PackageManager } from './dependency-manager' +import { + install, + checkPackageManager, + PackageManager +} from './dependency-manager' -export { shell } from './shell' -export { install, checkPackageManager } from './dependency-manager' -export { addTauriScript } from './helpers/add-tauri-script' +import { shell } from './shell' +import { addTauriScript } from './helpers/add-tauri-script' + +interface Argv { + h: boolean + help: boolean + v: string + version: string + ci: boolean + dev: boolean + b: string + binary: string + f: string + force: string + l: boolean + log: boolean + m: string + manager: string + d: string + directory: string + t: string + tauriPath: string + A: string + appName: string + W: string + windowTitle: string + D: string + distDir: string + P: string + devPath: string + r: string + recipe: string +} + +const printUsage = (): void => { + console.log(` + Description + Starts a new tauri app from a "recipe" or pre-built template. + Usage + $ yarn create tauri-app # npm create-tauri-app + Options + --help, -h Displays this message + -v, --version Displays the Tauri CLI version + --ci Skip prompts + --force, -f Force init to overwrite [conf|template|all] + --log, -l Logging [boolean] + --manager, -d Set package manager to use [npm|yarn] + --directory, -d Set target directory for init + --app-name, -A Name of your Tauri application + --window-title, -W Window title of your Tauri application + --dist-dir, -D Web assets location, relative to /src-tauri + --dev-path, -P Url of your dev server + --recipe, -r Add UI framework recipe. None by default. + Supported recipes: [${recipeShortNames.join('|')}] + `) +} + +export const createTauriApp = async (cliArgs: string[]): Promise => { + const argv = (minimist(cliArgs, { + alias: { + h: 'help', + v: 'version', + f: 'force', + l: 'log', + m: 'manager', + d: 'directory', + t: 'tauri-path', + A: 'app-name', + W: 'window-title', + D: 'dist-dir', + P: 'dev-path', + r: 'recipe' + }, + boolean: ['h', 'l', 'ci', 'dev'], + default: { A: 'tauri-app', r: 'vanillajs' } + }) as unknown) as Argv + + if (argv.help) { + printUsage() + return 0 + } + + if (argv.v) { + /* eslint-disable @typescript-eslint/no-var-requires */ + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + console.log(require('../package.json').version) + return false // do this for node consumers and tests + /* eslint-enable @typescript-eslint/no-var-requires */ + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ + } + + return await getOptionsInteractive(argv, !argv.ci).then( + async (responses) => await runInit(argv, responses) + ) +} + +interface Responses { + appName: string + tauri: { window: { title: string } } + 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 @@ -45,19 +204,128 @@ export interface Recipe { }) => Promise } -export const allRecipes: Recipe[] = [vanillajs, reactjs, reactts, vite, vuecli] +const allRecipes: Recipe[] = [vanillajs, reactjs, reactts, vite, vuecli] -export const recipeNames: Array<[string, string]> = allRecipes.map((r) => [ - r.shortName, - r.descriptiveName -]) - -export const recipeByShortName = (name: string): Recipe | undefined => +const recipeByShortName = (name: string): Recipe | undefined => allRecipes.find((r) => r.shortName === name) -export const recipeByDescriptiveName = (name: string): Recipe | undefined => +const recipeByDescriptiveName = (name: string): Recipe | undefined => allRecipes.find((r) => r.descriptiveName === name) -export const recipeShortNames = allRecipes.map((r) => r.shortName) +const recipeShortNames = allRecipes.map((r) => r.shortName) -export const recipeDescriptiveNames = allRecipes.map((r) => r.descriptiveName) +const recipeDescriptiveNames = allRecipes.map((r) => r.descriptiveName) + +const runInit = async (argv: Argv, config: Responses): Promise => { + 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' + + let recipe: Recipe | undefined + + if (argv.r) { + recipe = recipeByShortName(argv.r) + } else if (recipeName !== undefined) { + recipe = recipeByDescriptiveName(recipeName) + } + + if (!recipe) throw new Error('Could not find the recipe specified.') + + const buildConfig = { + distDir: argv.D, + devPath: argv.P, + appName: appName, + windowTitle: title + } + + const directory = argv.d || process.cwd() + let updatedConfig + if (recipe.configUpdate) { + updatedConfig = recipe.configUpdate({ + cfg: buildConfig, + packageManager + }) + } + const cfg = { + ...buildConfig, + ...(updatedConfig ?? {}) + } + + // note that our app directory is reliant on the appName and + // generally there are issues if the path has spaces (see Windows) + // future TODO prevent app names with spaces or escape here? + const appDirectory = join(directory, cfg.appName) + + // this throws an error if we can't run the package manager they requested + await checkPackageManager({ cwd: directory, packageManager }) + + if (recipe.preInit) { + console.log('===== running initial command(s) =====') + await recipe.preInit({ cwd: directory, cfg, packageManager }) + } + + const initArgs = [ + ['--app-name', cfg.appName], + ['--window-title', cfg.windowTitle], + ['--dist-dir', cfg.distDir], + ['--dev-path', cfg.devPath] + ].reduce((final: string[], argSet) => { + if (argSet[1]) { + return final.concat(argSet) + } else { + return final + } + }, []) + + // Vue CLI plugin automatically runs these + if (recipe.shortName !== 'vuecli') { + console.log('===== installing any additional needed deps =====') + if (argv.dev) { + await shell('yarn', ['link', '@tauri-apps/cli'], { + cwd: appDirectory + }) + await shell('yarn', ['link', '@tauri-apps/api'], { + cwd: appDirectory + }) + } + + await install({ + appDir: appDirectory, + dependencies: recipe.extraNpmDependencies, + devDependencies: argv.dev + ? [...recipe.extraNpmDevDependencies] + : ['@tauri-apps/cli'].concat(recipe.extraNpmDevDependencies), + packageManager + }) + + console.log('===== running tauri init =====') + addTauriScript(appDirectory) + + const binary = !argv.b ? packageManager : resolve(appDirectory, argv.b) + const runTauriArgs = + packageManager === 'npm' && !argv.b + ? ['run', 'tauri', '--', 'init'] + : ['tauri', 'init'] + await shell(binary, [...runTauriArgs, ...initArgs, '--ci'], { + cwd: appDirectory + }) + } + + if (recipe.postInit) { + console.log('===== running final command(s) =====') + await recipe.postInit({ + cwd: appDirectory, + cfg, + packageManager + }) + } +} diff --git a/tooling/create-tauri-app/tsconfig.json b/tooling/create-tauri-app/tsconfig.json index de5e3dd92..f0b018b0d 100644 --- a/tooling/create-tauri-app/tsconfig.json +++ b/tooling/create-tauri-app/tsconfig.json @@ -8,7 +8,10 @@ "esModuleInterop": true, "resolveJsonModule": true, "moduleResolution": "node", - "typeRoots": ["./types", "node_modules/@types"] + "checkJs": false, + "preserveSymlinks": true, + "skipLibCheck": true, + "typeRoots": ["types", "node_modules/@types"] }, "include": ["src"], "exclude": ["src/templates", "types", "test", "__fixtures__"]