runner.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import Inliner from '@tauri-apps/tauri-inliner'
  2. import toml from '@tauri-apps/toml'
  3. import chokidar, { FSWatcher } from 'chokidar'
  4. import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs-extra'
  5. import { JSDOM } from 'jsdom'
  6. import debounce from 'lodash.debounce'
  7. import path from 'path'
  8. import * as entry from './entry'
  9. import { appDir, tauriDir } from './helpers/app-paths'
  10. import logger from './helpers/logger'
  11. import onShutdown from './helpers/on-shutdown'
  12. import { spawn } from './helpers/spawn'
  13. const getTauriConfig = require('./helpers/tauri-config')
  14. import { TauriConfig } from './types/config'
  15. const log = logger('app:tauri', 'green')
  16. const warn = logger('app:tauri (template)', 'red')
  17. class Runner {
  18. pid: number
  19. tauriWatcher?: FSWatcher
  20. devPath?: string
  21. killPromise?: Function
  22. constructor() {
  23. this.pid = 0
  24. this.tauriWatcher = undefined
  25. onShutdown(() => {
  26. this.stop().catch(e => {
  27. throw e
  28. })
  29. })
  30. }
  31. async run(cfg: TauriConfig): Promise<void> {
  32. const devPath = cfg.build.devPath
  33. if (this.pid) {
  34. if (this.devPath !== devPath) {
  35. await this.stop()
  36. } else {
  37. return
  38. }
  39. }
  40. this.__manipulateToml(toml => {
  41. this.__whitelistApi(cfg, toml)
  42. })
  43. const runningDevServer = devPath.startsWith('http')
  44. let inlinedAssets: string[] = []
  45. if (!runningDevServer) {
  46. inlinedAssets = await this.__parseHtml(cfg, devPath)
  47. }
  48. process.env.TAURI_INLINED_ASSSTS = inlinedAssets.join('|')
  49. entry.generate(tauriDir, cfg)
  50. this.devPath = devPath
  51. const features = runningDevServer ? ['dev-server'] : []
  52. const startDevTauri = async (): Promise<void> => {
  53. return this.__runCargoCommand({
  54. cargoArgs: ['run'].concat(
  55. features.length ? ['--features', ...features] : []
  56. ),
  57. dev: true
  58. })
  59. }
  60. // Start watching for tauri app changes
  61. // eslint-disable-next-line security/detect-non-literal-fs-filename
  62. this.tauriWatcher = chokidar
  63. .watch(
  64. [
  65. path.join(tauriDir, 'src'),
  66. path.join(tauriDir, 'Cargo.toml'),
  67. path.join(tauriDir, 'build.rs'),
  68. path.join(appDir, 'tauri.conf.js')
  69. ],
  70. {
  71. // TODO: incorrect options?
  72. // @ts-ignore
  73. watchers: {
  74. chokidar: {
  75. ignoreInitial: true
  76. }
  77. }
  78. }
  79. )
  80. .on(
  81. 'change',
  82. debounce((path: string) => {
  83. this.__stopCargo()
  84. .then(() => {
  85. if (path.includes('tauri.conf.js')) {
  86. this.run(getTauriConfig({ ctx: cfg.ctx })).catch(e => {
  87. throw e
  88. })
  89. } else {
  90. startDevTauri().catch(e => {
  91. throw e
  92. })
  93. }
  94. })
  95. .catch(err => {
  96. warn(err)
  97. process.exit(1)
  98. })
  99. }, 1000)
  100. )
  101. return startDevTauri()
  102. }
  103. async build(cfg: TauriConfig): Promise<void> {
  104. this.__manipulateToml(toml => {
  105. this.__whitelistApi(cfg, toml)
  106. })
  107. const inlinedAssets = await this.__parseHtml(cfg, cfg.build.distDir)
  108. process.env.TAURI_INLINED_ASSSTS = inlinedAssets.join('|')
  109. entry.generate(tauriDir, cfg)
  110. const features = [
  111. cfg.tauri.embeddedServer.active ? 'embedded-server' : 'no-server'
  112. ]
  113. const buildFn = async (target?: string): Promise<void> =>
  114. this.__runCargoCommand({
  115. cargoArgs: [
  116. cfg.tauri.bundle.active ? 'tauri-cli' : 'build',
  117. '--features',
  118. ...features
  119. ]
  120. .concat(cfg.ctx.debug ? [] : ['--release'])
  121. .concat(target ? ['--target', target] : [])
  122. })
  123. if (cfg.ctx.debug || !cfg.ctx.targetName) {
  124. // on debug mode or if no target specified,
  125. // build only for the current platform
  126. await buildFn()
  127. } else {
  128. const targets = cfg.ctx.target.split(',')
  129. for (const target of targets) {
  130. await buildFn(target)
  131. }
  132. }
  133. }
  134. async __parseHtml(cfg: TauriConfig, indexDir: string): Promise<string[]> {
  135. const inlinedAssets: string[] = []
  136. const distDir = cfg.build.distDir
  137. return new Promise((resolve, reject) => {
  138. const distIndexPath = path.join(indexDir, 'index.html')
  139. if (!existsSync(distIndexPath)) {
  140. warn(
  141. `Error: cannot find index.html in "${indexDir}". Did you forget to build your web code or update the build.distDir in tauri.conf.js?`
  142. )
  143. reject(new Error('Could not find index.html in dist dir.'))
  144. }
  145. new Inliner(distIndexPath, (err: Error, html: string) => {
  146. if (err) {
  147. reject(err)
  148. } else {
  149. const dom = new JSDOM(html)
  150. const document = dom.window.document
  151. document.querySelectorAll('link').forEach(link => {
  152. link.removeAttribute('rel')
  153. link.removeAttribute('as')
  154. })
  155. const tauriScript = document.createElement('script')
  156. // @ts-ignore
  157. tauriScript.text = readFileSync(path.join(tauriDir, 'tauri.js'))
  158. document.body.insertBefore(tauriScript, document.body.firstChild)
  159. const csp = cfg.tauri.security.csp
  160. if (csp) {
  161. const cspTag = document.createElement('meta')
  162. cspTag.setAttribute('http-equiv', 'Content-Security-Policy')
  163. cspTag.setAttribute('content', csp)
  164. document.head.appendChild(cspTag)
  165. }
  166. if (!existsSync(distDir)) {
  167. mkdirSync(distDir, { recursive: true })
  168. }
  169. writeFileSync(
  170. path.join(distDir, 'index.tauri.html'),
  171. dom.serialize()
  172. )
  173. resolve(inlinedAssets)
  174. }
  175. }).on('progress', (event: string) => {
  176. const match = event.match(/([\S\d]+)\.([\S\d]+)/g)
  177. match && inlinedAssets.push(match[0])
  178. })
  179. })
  180. }
  181. async stop(): Promise<void> {
  182. return new Promise((resolve, reject) => {
  183. this.tauriWatcher && this.tauriWatcher.close()
  184. this.__stopCargo()
  185. .then(resolve)
  186. .catch(e => {
  187. console.error(e)
  188. })
  189. })
  190. }
  191. async __runCargoCommand({
  192. cargoArgs,
  193. extraArgs,
  194. dev = false
  195. }: {
  196. cargoArgs: string[]
  197. extraArgs?: string[]
  198. dev?: boolean
  199. }): Promise<void> {
  200. return new Promise(resolve => {
  201. this.pid = spawn(
  202. 'cargo',
  203. extraArgs ? cargoArgs.concat(['--']).concat(extraArgs) : cargoArgs,
  204. tauriDir,
  205. code => {
  206. if (code) {
  207. warn()
  208. warn('⚠️ [FAIL] Cargo CLI has failed')
  209. warn()
  210. process.exit(1)
  211. }
  212. if (this.killPromise) {
  213. this.killPromise()
  214. this.killPromise = undefined
  215. } else if (dev) {
  216. warn()
  217. warn('Cargo process was killed. Exiting...')
  218. warn()
  219. process.exit(0)
  220. }
  221. }
  222. )
  223. resolve()
  224. })
  225. }
  226. async __stopCargo(): Promise<void> {
  227. const pid = this.pid
  228. if (!pid) {
  229. return Promise.resolve()
  230. }
  231. log('Shutting down tauri process...')
  232. this.pid = 0
  233. return new Promise((resolve, reject) => {
  234. this.killPromise = resolve
  235. process.kill(pid)
  236. })
  237. }
  238. __manipulateToml(callback: (tomlContents: object) => void): void {
  239. const tomlPath = path.join(tauriDir, 'Cargo.toml')
  240. // TODO: should this be read as buffer or string?
  241. const tomlFile = readFileSync(tomlPath)
  242. // @ts-ignore
  243. const tomlContents = toml.parse(tomlFile)
  244. callback(tomlContents)
  245. const output = toml.stringify(tomlContents)
  246. writeFileSync(tomlPath, output)
  247. }
  248. __whitelistApi(
  249. cfg: TauriConfig,
  250. tomlContents: { [index: string]: any }
  251. ): void {
  252. const tomlFeatures = []
  253. if (cfg.tauri.whitelist.all) {
  254. tomlFeatures.push('all-api')
  255. } else {
  256. const whitelist = Object.keys(cfg.tauri.whitelist).filter(
  257. w => cfg.tauri.whitelist[String(w)] === true
  258. )
  259. tomlFeatures.push(...whitelist)
  260. }
  261. if (cfg.tauri.edge.active) {
  262. tomlFeatures.push('edge')
  263. }
  264. tomlContents.dependencies.tauri.features = tomlFeatures
  265. }
  266. }
  267. export default Runner