index.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. import minimist from 'minimist'
  5. import inquirer from 'inquirer'
  6. import { bold, cyan, green, reset, yellow } from 'chalk'
  7. import { platform } from 'os'
  8. import { resolve, join, relative } from 'path'
  9. import { cra } from './recipes/react'
  10. import { vuecli } from './recipes/vue-cli'
  11. import { vanillajs } from './recipes/vanilla'
  12. import { vite } from './recipes/vite'
  13. import { dominator } from './recipes/dominator'
  14. import { ngcli } from './recipes/ng-cli'
  15. import { svelte } from './recipes/svelte'
  16. import { install, checkPackageManager } from './dependency-manager'
  17. import { shell } from './shell'
  18. import { updatePackageJson } from './helpers/update-package-json'
  19. import { Recipe } from './types/recipe'
  20. import { updateTauriConf } from './helpers/update-tauri-conf'
  21. interface Argv {
  22. h: boolean
  23. help: boolean
  24. v: string
  25. version: string
  26. ci: boolean
  27. dev: boolean
  28. b: string
  29. binary: string
  30. f: string
  31. force: string
  32. l: boolean
  33. log: boolean
  34. m: string
  35. manager: string
  36. d: string
  37. directory: string
  38. t: string
  39. tauriPath: string
  40. A: string
  41. appName: string
  42. W: string
  43. windowTitle: string
  44. D: string
  45. distDir: string
  46. P: string
  47. devPath: string
  48. r: string
  49. recipe: string
  50. }
  51. const printUsage = (): void => {
  52. console.log(`
  53. Description
  54. Starts a new tauri app from a "recipe" or pre-built template.
  55. Usage
  56. $ yarn create tauri-app <app-name> # npm create-tauri-app <app-name>
  57. Options
  58. --help, -h Displays this message
  59. -v, --version Displays the Tauri CLI version
  60. --ci Skip prompts
  61. --force, -f Force init to overwrite [conf|template|all]
  62. --log, -l Logging [boolean]
  63. --manager, -m Set package manager to use [npm|yarn|pnpm]
  64. --directory, -d Set target directory for init
  65. --app-name, -A Name of your Tauri application
  66. --window-title, -W Window title of your Tauri application
  67. --dist-dir, -D Web assets location, relative to <project-dir>/src-tauri
  68. --dev-path, -P Url of your dev server
  69. --recipe, -r Add UI framework recipe. None by default.
  70. Supported recipes: [${recipeShortNames.join('|')}]
  71. `)
  72. }
  73. export const createTauriApp = async (cliArgs: string[]): Promise<any> => {
  74. const argv = minimist(cliArgs, {
  75. alias: {
  76. h: 'help',
  77. v: 'version',
  78. f: 'force',
  79. l: 'log',
  80. m: 'manager',
  81. d: 'directory',
  82. t: 'tauri-path',
  83. A: 'app-name',
  84. W: 'window-title',
  85. D: 'dist-dir',
  86. P: 'dev-path',
  87. r: 'recipe'
  88. },
  89. boolean: ['h', 'l', 'ci', 'dev']
  90. }) as unknown as Argv
  91. if (argv.help) {
  92. printUsage()
  93. return 0
  94. }
  95. if (argv.v) {
  96. /* eslint-disable @typescript-eslint/no-var-requires */
  97. /* eslint-disable @typescript-eslint/no-unsafe-member-access */
  98. console.log(require('../package.json').version)
  99. return false // do this for node consumers and tests
  100. /* eslint-enable @typescript-eslint/no-var-requires */
  101. /* eslint-enable @typescript-eslint/no-unsafe-member-access */
  102. }
  103. return await runInit(argv)
  104. }
  105. interface Responses {
  106. appName: string
  107. tauri: { window: { title: string } }
  108. recipeName: string
  109. installApi: boolean
  110. }
  111. const allRecipes: Recipe[] = [
  112. vanillajs,
  113. cra,
  114. vite,
  115. vuecli,
  116. ngcli,
  117. svelte,
  118. dominator
  119. ]
  120. const recipeByShortName = (name: string): Recipe | undefined =>
  121. allRecipes.find((r) => r.shortName === name)
  122. const recipeByDescriptiveName = (name: string): Recipe | undefined =>
  123. allRecipes.find((r) => r.descriptiveName.value === name)
  124. const recipeShortNames = allRecipes.map((r) => r.shortName)
  125. const recipeDescriptiveNames = allRecipes.map((r) => r.descriptiveName)
  126. const keypress = async (skip: boolean): Promise<void> => {
  127. if (skip) return
  128. process.stdin.setRawMode(true)
  129. return await new Promise((resolve, reject) => {
  130. console.log('Press any key to continue...')
  131. process.stdin.once('data', (data) => {
  132. const byteArray = [...data]
  133. if (byteArray.length > 0 && byteArray[0] === 3) {
  134. console.log('^C')
  135. process.exit(1)
  136. }
  137. process.stdin.setRawMode(false)
  138. resolve()
  139. })
  140. })
  141. }
  142. const runInit = async (argv: Argv): Promise<void> => {
  143. console.log(
  144. `We hope to help you create something special with ${bold(
  145. yellow('Tauri')
  146. )}!`
  147. )
  148. console.log(
  149. 'You will have a choice of one of the UI frameworks supported by the greater web tech community.'
  150. )
  151. console.log(
  152. `This should get you started. See our docs at https://tauri.studio/`
  153. )
  154. const setupLink =
  155. platform() === 'win32'
  156. ? 'https://tauri.studio/en/docs/getting-started/setup-windows/'
  157. : platform() === 'darwin'
  158. ? 'https://tauri.studio/en/docs/getting-started/setup-macos/'
  159. : 'https://tauri.studio/en/docs/getting-started/setup-linux/'
  160. console.log(
  161. `If you haven't already, please take a moment to setup your system.`
  162. )
  163. console.log(`You may find the requirements here: ${setupLink}`)
  164. await keypress(argv.ci)
  165. const defaults = {
  166. appName: 'tauri-app',
  167. tauri: { window: { title: 'Tauri App' } },
  168. recipeName: 'vanillajs',
  169. installApi: true
  170. }
  171. // prompt initial questions
  172. const answers = (await inquirer
  173. .prompt([
  174. {
  175. type: 'input',
  176. name: 'appName',
  177. message: 'What is your app name?',
  178. default: defaults.appName,
  179. when: !argv.ci && !argv.A
  180. },
  181. {
  182. type: 'input',
  183. name: 'tauri.window.title',
  184. message: 'What should the window title be?',
  185. default: defaults.tauri.window.title,
  186. when: !argv.ci && !argv.W
  187. },
  188. {
  189. type: 'list',
  190. name: 'recipeName',
  191. message: 'What UI recipe would you like to add?',
  192. choices: recipeDescriptiveNames,
  193. default: defaults.recipeName,
  194. when: !argv.ci && !argv.r
  195. },
  196. {
  197. type: 'confirm',
  198. name: 'installApi',
  199. message: 'Add "@tauri-apps/api" npm package?',
  200. default: true,
  201. when: !argv.ci
  202. }
  203. ])
  204. .catch((error: { isTtyError: boolean }) => {
  205. if (error.isTtyError) {
  206. // Prompt couldn't be rendered in the current environment
  207. console.warn(
  208. 'It appears your terminal does not support interactive prompts. Using default values.'
  209. )
  210. } else {
  211. // Something else went wrong
  212. console.error('An unknown error occurred:', error)
  213. }
  214. })) as Responses
  215. const {
  216. appName,
  217. recipeName,
  218. installApi,
  219. tauri: {
  220. window: { title }
  221. }
  222. } = { ...defaults, ...answers }
  223. let recipe: Recipe | undefined
  224. if (argv.r) {
  225. recipe = recipeByShortName(argv.r)
  226. } else if (recipeName !== undefined) {
  227. recipe = recipeByDescriptiveName(recipeName)
  228. }
  229. // throw if recipe is not set
  230. if (!recipe) {
  231. throw new Error('Could not find the recipe specified.')
  232. }
  233. const packageManager =
  234. argv.m === 'yarn' || argv.m === 'npm' || argv.m === 'pnpm'
  235. ? argv.m
  236. : // @ts-expect-error
  237. // this little fun snippet pulled from vite determines the package manager the script was run from
  238. /yarn/.test(process?.env?.npm_execpath)
  239. ? 'yarn'
  240. : // @ts-expect-error
  241. /pnpm/.test(process?.env?.npm_execpath)
  242. ? 'pnpm'
  243. : 'npm'
  244. const buildConfig = {
  245. distDir: argv.D,
  246. devPath: argv.P,
  247. appName: argv.A || appName,
  248. windowTitle: argv.W || title
  249. }
  250. const directory = argv.d || process.cwd()
  251. // prompt additional recipe questions
  252. let recipeAnswers
  253. if (recipe.extraQuestions) {
  254. recipeAnswers = await inquirer
  255. .prompt(
  256. recipe.extraQuestions({
  257. cfg: buildConfig,
  258. packageManager,
  259. ci: argv.ci,
  260. cwd: directory
  261. })
  262. )
  263. .catch((error: { isTtyError: boolean }) => {
  264. if (error.isTtyError) {
  265. // Prompt couldn't be rendered in the current environment
  266. console.warn(
  267. 'It appears your terminal does not support interactive prompts. Using default values.'
  268. )
  269. } else {
  270. // Something else went wrong
  271. console.error('An unknown error occurred:', error)
  272. }
  273. })
  274. }
  275. let updatedConfig
  276. if (recipe.configUpdate) {
  277. updatedConfig = recipe.configUpdate({
  278. cfg: buildConfig,
  279. packageManager,
  280. ci: argv.ci,
  281. cwd: directory,
  282. answers: recipeAnswers
  283. })
  284. }
  285. const cfg = {
  286. ...buildConfig,
  287. ...(updatedConfig ?? {})
  288. }
  289. // note that our app directory is reliant on the appName and
  290. // generally there are issues if the path has spaces (see Windows)
  291. // future TODO prevent app names with spaces or escape here?
  292. const appDirectory = join(directory, cfg.appName)
  293. // this throws an error if we can't run the package manager they requested
  294. await checkPackageManager({ cwd: directory, packageManager })
  295. if (recipe.preInit) {
  296. logStep('Running initial command(s)')
  297. await recipe.preInit({
  298. cwd: directory,
  299. cfg,
  300. packageManager,
  301. ci: argv.ci,
  302. answers: recipeAnswers
  303. })
  304. }
  305. const initArgs = [
  306. ['--app-name', cfg.appName],
  307. ['--window-title', cfg.windowTitle],
  308. ['--dist-dir', cfg.distDir],
  309. ['--dev-path', cfg.devPath]
  310. ].reduce((final: string[], argSet) => {
  311. if (argSet[1]) {
  312. return final.concat(argSet)
  313. } else {
  314. return final
  315. }
  316. }, [])
  317. const tauriCLIVersion = !argv.dev
  318. ? 'latest'
  319. : `file:${relative(appDirectory, join(__dirname, '../../cli.js'))}`
  320. // Vue CLI plugin automatically runs these
  321. if (recipe.shortName !== 'vuecli') {
  322. logStep('Installing any additional needed dependencies')
  323. await install({
  324. appDir: appDirectory,
  325. dependencies: [installApi ? '@tauri-apps/api@latest' : ''].concat(
  326. recipe.extraNpmDependencies
  327. ),
  328. devDependencies: [`@tauri-apps/cli@${tauriCLIVersion}`].concat(
  329. recipe.extraNpmDevDependencies
  330. ),
  331. packageManager
  332. })
  333. logStep(`Updating ${reset(yellow('"package.json"'))}`)
  334. updatePackageJson(appDirectory, appName)
  335. logStep(`Running ${reset(yellow('"tauri init"'))}`)
  336. const binary = !argv.b ? packageManager : resolve(appDirectory, argv.b)
  337. // pnpm is equivalent to yarn and can run srcipts without using "run" but due to this bug https://github.com/pnpm/pnpm/issues/2764
  338. // we need to pass "--" to pnpm or arguments won't be parsed correctly so for this command only we are gonna treat pnpm as npm equivalent/
  339. const runTauriArgs =
  340. packageManager === 'yarn' || argv.b
  341. ? ['tauri', 'init']
  342. : ['run', 'tauri', '--', 'init']
  343. await shell(binary, [...runTauriArgs, ...initArgs, '--ci'], {
  344. cwd: appDirectory
  345. })
  346. logStep(`Updating ${reset(yellow('"tauri.conf.json"'))}`)
  347. updateTauriConf(appDirectory, cfg)
  348. }
  349. if (recipe.postInit) {
  350. logStep('Running final command(s)')
  351. await recipe.postInit({
  352. cwd: appDirectory,
  353. cfg,
  354. packageManager,
  355. ci: argv.ci,
  356. answers: recipeAnswers
  357. })
  358. }
  359. }
  360. function logStep(msg: string): void {
  361. const out = `${green('>>')} ${bold(cyan(msg))}`
  362. console.log(out)
  363. }