chore: shift CTA from bin to .ts (#1651)

* chore: shift CTA from bin to .ts

* add change file

* fix rollup build
This commit is contained in:
Jacob Bolda 2021-04-29 08:55:09 -05:00 committed by GitHub
parent 252014cae2
commit c3acbd68ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 294 additions and 261 deletions

View File

@ -0,0 +1,5 @@
---
"create-tauri-app": patch
---
Shift everything out of the `bin` and into `.ts` so we can apply Typescript types.

View File

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

View File

@ -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 <app-name> # npm create-tauri-app <app-name>
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 <project-dir>/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)

View File

@ -6,8 +6,6 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
modulePathIgnorePatterns: ['__fixtures__'],
testMatch: ['<rootDir>/test/**/*.spec.ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json'

View File

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

View File

@ -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 <app-name> # npm create-tauri-app <app-name>
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 <project-dir>/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<any> => {
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<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
@ -45,19 +204,128 @@ export interface Recipe {
}) => Promise<void>
}
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<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'
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
})
}
}

View File

@ -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__"]