index.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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 { resolve, join } from 'path'
  7. import { TauriBuildConfig } from './types/config'
  8. import { reactjs, reactts } from './recipes/react'
  9. import { vuecli } from './recipes/vue-cli'
  10. import { vanillajs } from './recipes/vanilla'
  11. import { vite } from './recipes/vite'
  12. import {
  13. install,
  14. checkPackageManager,
  15. PackageManager
  16. } from './dependency-manager'
  17. import { shell } from './shell'
  18. import { addTauriScript } from './helpers/add-tauri-script'
  19. interface Argv {
  20. h: boolean
  21. help: boolean
  22. v: string
  23. version: string
  24. ci: boolean
  25. dev: boolean
  26. b: string
  27. binary: string
  28. f: string
  29. force: string
  30. l: boolean
  31. log: boolean
  32. m: string
  33. manager: string
  34. d: string
  35. directory: string
  36. t: string
  37. tauriPath: string
  38. A: string
  39. appName: string
  40. W: string
  41. windowTitle: string
  42. D: string
  43. distDir: string
  44. P: string
  45. devPath: string
  46. r: string
  47. recipe: string
  48. }
  49. const printUsage = (): void => {
  50. console.log(`
  51. Description
  52. Starts a new tauri app from a "recipe" or pre-built template.
  53. Usage
  54. $ yarn create tauri-app <app-name> # npm create-tauri-app <app-name>
  55. Options
  56. --help, -h Displays this message
  57. -v, --version Displays the Tauri CLI version
  58. --ci Skip prompts
  59. --force, -f Force init to overwrite [conf|template|all]
  60. --log, -l Logging [boolean]
  61. --manager, -d Set package manager to use [npm|yarn]
  62. --directory, -d Set target directory for init
  63. --app-name, -A Name of your Tauri application
  64. --window-title, -W Window title of your Tauri application
  65. --dist-dir, -D Web assets location, relative to <project-dir>/src-tauri
  66. --dev-path, -P Url of your dev server
  67. --recipe, -r Add UI framework recipe. None by default.
  68. Supported recipes: [${recipeShortNames.join('|')}]
  69. `)
  70. }
  71. export const createTauriApp = async (cliArgs: string[]): Promise<any> => {
  72. const argv = (minimist(cliArgs, {
  73. alias: {
  74. h: 'help',
  75. v: 'version',
  76. f: 'force',
  77. l: 'log',
  78. m: 'manager',
  79. d: 'directory',
  80. t: 'tauri-path',
  81. A: 'app-name',
  82. W: 'window-title',
  83. D: 'dist-dir',
  84. P: 'dev-path',
  85. r: 'recipe'
  86. },
  87. boolean: ['h', 'l', 'ci', 'dev'],
  88. default: { A: 'tauri-app', r: 'vanillajs' }
  89. }) as unknown) as Argv
  90. if (argv.help) {
  91. printUsage()
  92. return 0
  93. }
  94. if (argv.v) {
  95. /* eslint-disable @typescript-eslint/no-var-requires */
  96. /* eslint-disable @typescript-eslint/no-unsafe-member-access */
  97. console.log(require('../package.json').version)
  98. return false // do this for node consumers and tests
  99. /* eslint-enable @typescript-eslint/no-var-requires */
  100. /* eslint-enable @typescript-eslint/no-unsafe-member-access */
  101. }
  102. return await getOptionsInteractive(argv, !argv.ci).then(
  103. async (responses) => await runInit(argv, responses)
  104. )
  105. }
  106. interface Responses {
  107. appName: string
  108. tauri: { window: { title: string } }
  109. recipeName: string
  110. }
  111. const getOptionsInteractive = async (
  112. argv: Argv,
  113. ask: boolean
  114. ): Promise<Responses> => {
  115. const defaults = {
  116. appName: argv.A,
  117. tauri: { window: { title: 'Tauri App' } },
  118. recipeName: argv.r
  119. }
  120. return (await inquirer
  121. .prompt([
  122. {
  123. type: 'input',
  124. name: 'appName',
  125. message: 'What is your app name?',
  126. default: defaults.appName,
  127. when: ask && !argv.A
  128. },
  129. {
  130. type: 'input',
  131. name: 'tauri.window.title',
  132. message: 'What should the window title be?',
  133. default: defaults.tauri.window.title,
  134. when: ask && !argv.W
  135. },
  136. {
  137. type: 'list',
  138. name: 'recipeName',
  139. message: 'Would you like to add a UI recipe?',
  140. choices: recipeDescriptiveNames,
  141. default: defaults.recipeName,
  142. when: ask && !argv.r
  143. }
  144. ])
  145. .then((answers: Argv) => ({
  146. ...defaults,
  147. ...answers
  148. }))
  149. .catch(async (error: { isTtyError: boolean }) => {
  150. if (error.isTtyError) {
  151. // Prompt couldn't be rendered in the current environment
  152. console.warn(
  153. 'It appears your terminal does not support interactive prompts. Using default values.'
  154. )
  155. } else {
  156. // Something else went wrong
  157. console.error('An unknown error occurred:', error)
  158. }
  159. return await runInit(argv, defaults)
  160. })) as Responses
  161. }
  162. export interface Recipe {
  163. descriptiveName: string
  164. shortName: string
  165. configUpdate?: ({
  166. cfg,
  167. packageManager
  168. }: {
  169. cfg: TauriBuildConfig
  170. packageManager: PackageManager
  171. }) => TauriBuildConfig
  172. extraNpmDependencies: string[]
  173. extraNpmDevDependencies: string[]
  174. preInit?: ({
  175. cwd,
  176. cfg,
  177. packageManager
  178. }: {
  179. cwd: string
  180. cfg: TauriBuildConfig
  181. packageManager: PackageManager
  182. }) => Promise<void>
  183. postInit?: ({
  184. cwd,
  185. cfg,
  186. packageManager
  187. }: {
  188. cwd: string
  189. cfg: TauriBuildConfig
  190. packageManager: PackageManager
  191. }) => Promise<void>
  192. }
  193. const allRecipes: Recipe[] = [vanillajs, reactjs, reactts, vite, vuecli]
  194. const recipeByShortName = (name: string): Recipe | undefined =>
  195. allRecipes.find((r) => r.shortName === name)
  196. const recipeByDescriptiveName = (name: string): Recipe | undefined =>
  197. allRecipes.find((r) => r.descriptiveName === name)
  198. const recipeShortNames = allRecipes.map((r) => r.shortName)
  199. const recipeDescriptiveNames = allRecipes.map((r) => r.descriptiveName)
  200. const runInit = async (argv: Argv, config: Responses): Promise<void> => {
  201. const {
  202. appName,
  203. recipeName,
  204. tauri: {
  205. window: { title }
  206. }
  207. } = config
  208. // this little fun snippet pulled from vite determines the package manager the script was run from
  209. // @ts-expect-error
  210. const packageManager = /yarn/.test(process?.env?.npm_execpath)
  211. ? 'yarn'
  212. : 'npm'
  213. let recipe: Recipe | undefined
  214. if (argv.r) {
  215. recipe = recipeByShortName(argv.r)
  216. } else if (recipeName !== undefined) {
  217. recipe = recipeByDescriptiveName(recipeName)
  218. }
  219. if (!recipe) throw new Error('Could not find the recipe specified.')
  220. const buildConfig = {
  221. distDir: argv.D,
  222. devPath: argv.P,
  223. appName: appName,
  224. windowTitle: title
  225. }
  226. const directory = argv.d || process.cwd()
  227. let updatedConfig
  228. if (recipe.configUpdate) {
  229. updatedConfig = recipe.configUpdate({
  230. cfg: buildConfig,
  231. packageManager
  232. })
  233. }
  234. const cfg = {
  235. ...buildConfig,
  236. ...(updatedConfig ?? {})
  237. }
  238. // note that our app directory is reliant on the appName and
  239. // generally there are issues if the path has spaces (see Windows)
  240. // future TODO prevent app names with spaces or escape here?
  241. const appDirectory = join(directory, cfg.appName)
  242. // this throws an error if we can't run the package manager they requested
  243. await checkPackageManager({ cwd: directory, packageManager })
  244. if (recipe.preInit) {
  245. console.log('===== running initial command(s) =====')
  246. await recipe.preInit({ cwd: directory, cfg, packageManager })
  247. }
  248. const initArgs = [
  249. ['--app-name', cfg.appName],
  250. ['--window-title', cfg.windowTitle],
  251. ['--dist-dir', cfg.distDir],
  252. ['--dev-path', cfg.devPath]
  253. ].reduce((final: string[], argSet) => {
  254. if (argSet[1]) {
  255. return final.concat(argSet)
  256. } else {
  257. return final
  258. }
  259. }, [])
  260. // Vue CLI plugin automatically runs these
  261. if (recipe.shortName !== 'vuecli') {
  262. console.log('===== installing any additional needed deps =====')
  263. if (argv.dev) {
  264. await shell('yarn', ['link', '@tauri-apps/cli'], {
  265. cwd: appDirectory
  266. })
  267. await shell('yarn', ['link', '@tauri-apps/api'], {
  268. cwd: appDirectory
  269. })
  270. }
  271. await install({
  272. appDir: appDirectory,
  273. dependencies: recipe.extraNpmDependencies,
  274. devDependencies: argv.dev
  275. ? [...recipe.extraNpmDevDependencies]
  276. : ['@tauri-apps/cli'].concat(recipe.extraNpmDevDependencies),
  277. packageManager
  278. })
  279. console.log('===== running tauri init =====')
  280. addTauriScript(appDirectory)
  281. const binary = !argv.b ? packageManager : resolve(appDirectory, argv.b)
  282. const runTauriArgs =
  283. packageManager === 'npm' && !argv.b
  284. ? ['run', 'tauri', '--', 'init']
  285. : ['tauri', 'init']
  286. await shell(binary, [...runTauriArgs, ...initArgs, '--ci'], {
  287. cwd: appDirectory
  288. })
  289. }
  290. if (recipe.postInit) {
  291. console.log('===== running final command(s) =====')
  292. await recipe.postInit({
  293. cwd: appDirectory,
  294. cfg,
  295. packageManager
  296. })
  297. }
  298. }