index.ts 11 KB

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