runner.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import Inliner from '@tauri-apps/tauri-inliner'
  2. import toml, { JsonMap } from '@tauri-apps/toml'
  3. import chokidar, { FSWatcher } from 'chokidar'
  4. import { existsSync, readFileSync, writeFileSync } from 'fs-extra'
  5. import { JSDOM } from 'jsdom'
  6. import { debounce, template } from 'lodash'
  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 (runner)', '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. const tomlContents = this.__getManifest()
  41. this.__whitelistApi(cfg, tomlContents)
  42. this.__rewriteManifest(tomlContents)
  43. entry.generate(tauriDir, cfg)
  44. const runningDevServer = devPath.startsWith('http')
  45. let inlinedAssets: string[] = []
  46. if (!runningDevServer) {
  47. inlinedAssets = await this.__parseHtml(cfg, devPath)
  48. }
  49. process.env.TAURI_INLINED_ASSSTS = inlinedAssets.join('|')
  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. exitOnPanic: cfg.ctx.exitOnPanic
  59. })
  60. }
  61. // Start watching for tauri app changes
  62. // eslint-disable-next-line security/detect-non-literal-fs-filename
  63. let tauriPaths: string[] = []
  64. // @ts-ignore
  65. if (tomlContents.dependencies.tauri.path) {
  66. // @ts-ignore
  67. const tauriPath = path.resolve(tauriDir, tomlContents.dependencies.tauri.path)
  68. tauriPaths = [
  69. tauriPath,
  70. `${tauriPath}-api`,
  71. `${tauriPath}-updater`,
  72. `${tauriPath}-utils`
  73. ]
  74. }
  75. this.tauriWatcher = chokidar
  76. .watch(
  77. [
  78. path.join(tauriDir, 'src'),
  79. path.join(tauriDir, 'Cargo.toml'),
  80. path.join(tauriDir, 'build.rs'),
  81. path.join(tauriDir, 'tauri.conf.json'),
  82. ...tauriPaths
  83. ],
  84. {
  85. ignoreInitial: true
  86. }
  87. )
  88. .on(
  89. 'change',
  90. debounce((path: string) => {
  91. (this.pid ? this.__stopCargo() : Promise.resolve())
  92. .then(() => {
  93. if (path.includes('tauri.conf.json')) {
  94. this.run(getTauriConfig({ ctx: cfg.ctx })).catch(e => {
  95. throw e
  96. })
  97. } else {
  98. startDevTauri().catch(e => {
  99. throw e
  100. })
  101. }
  102. })
  103. .catch(err => {
  104. warn(err)
  105. process.exit(1)
  106. })
  107. }, 1000)
  108. )
  109. return startDevTauri()
  110. }
  111. async build(cfg: TauriConfig): Promise<void> {
  112. const tomlContents = this.__getManifest()
  113. this.__whitelistApi(cfg, tomlContents)
  114. this.__rewriteManifest(tomlContents)
  115. entry.generate(tauriDir, cfg)
  116. const inlinedAssets = await this.__parseHtml(cfg, cfg.build.distDir)
  117. process.env.TAURI_INLINED_ASSSTS = inlinedAssets.join('|')
  118. const features = [
  119. cfg.tauri.embeddedServer.active ? 'embedded-server' : 'no-server'
  120. ]
  121. const buildFn = async (target?: string): Promise<void> =>
  122. this.__runCargoCommand({
  123. cargoArgs: [
  124. cfg.tauri.bundle.active ? 'tauri-bundler' : 'build',
  125. '--features',
  126. ...features
  127. ]
  128. .concat(cfg.ctx.debug ? [] : ['--release'])
  129. .concat(target ? ['--target', target] : [])
  130. })
  131. if (!cfg.ctx.target) {
  132. // if no target specified,
  133. // build only for the current platform
  134. await buildFn()
  135. } else {
  136. const targets = cfg.ctx.target.split(',')
  137. for (const target of targets) {
  138. await buildFn(target)
  139. }
  140. }
  141. }
  142. async __parseHtml(cfg: TauriConfig, indexDir: string): Promise<string[]> {
  143. const inlinedAssets: string[] = []
  144. return new Promise((resolve, reject) => {
  145. const indexPath = path.join(indexDir, 'index.html')
  146. if (!existsSync(indexPath)) {
  147. warn(
  148. `Error: cannot find index.html in "${indexDir}". Did you forget to build your web code or update the build.distDir in tauri.conf.json?`
  149. )
  150. reject(new Error('Could not find index.html in dist dir.'))
  151. }
  152. const rewriteHtml = (html: string, interceptor?: (dom: JSDOM) => void) => {
  153. const dom = new JSDOM(html)
  154. const document = dom.window.document
  155. if (interceptor !== undefined) {
  156. interceptor(dom)
  157. }
  158. if (!((cfg.ctx.dev && cfg.build.devPath.startsWith('http')) || cfg.tauri.embeddedServer.active)) {
  159. const mutationObserverTemplate = require('!!raw-loader!!../templates/mutation-observer').default
  160. const compiledMutationObserver = template(mutationObserverTemplate)
  161. const bodyMutationObserverScript = document.createElement('script')
  162. bodyMutationObserverScript.text = compiledMutationObserver({
  163. target: 'body',
  164. inlinedAssets: JSON.stringify(inlinedAssets)
  165. })
  166. document.body.insertBefore(bodyMutationObserverScript, document.body.firstChild)
  167. const headMutationObserverScript = document.createElement('script')
  168. headMutationObserverScript.text = compiledMutationObserver({
  169. target: 'head',
  170. inlinedAssets: JSON.stringify(inlinedAssets)
  171. })
  172. document.head.insertBefore(headMutationObserverScript, document.head.firstChild)
  173. }
  174. const tauriScript = document.createElement('script')
  175. // @ts-ignore
  176. tauriScript.text = readFileSync(path.join(tauriDir, 'tauri.js'))
  177. document.body.insertBefore(tauriScript, document.body.firstChild)
  178. const csp = cfg.tauri.security.csp
  179. if (csp) {
  180. const cspTag = document.createElement('meta')
  181. cspTag.setAttribute('http-equiv', 'Content-Security-Policy')
  182. cspTag.setAttribute('content', csp)
  183. document.head.appendChild(cspTag)
  184. }
  185. writeFileSync(
  186. path.join(indexDir, 'index.tauri.html'),
  187. dom.serialize()
  188. )
  189. }
  190. const domInterceptor = cfg.tauri.embeddedServer.active ? undefined : (dom: JSDOM) => {
  191. const document = dom.window.document
  192. document.querySelectorAll('link').forEach((link: HTMLLinkElement) => {
  193. link.removeAttribute('rel')
  194. link.removeAttribute('as')
  195. })
  196. }
  197. if (cfg.tauri.embeddedServer.active || !cfg.tauri.inliner.active) {
  198. rewriteHtml(readFileSync(indexPath).toString(), domInterceptor)
  199. resolve(inlinedAssets)
  200. } else {
  201. new Inliner(indexPath, (err: Error, html: string) => {
  202. if (err) {
  203. reject(err)
  204. } else {
  205. rewriteHtml(html, domInterceptor)
  206. resolve(inlinedAssets)
  207. }
  208. }).on('progress', (event: string) => {
  209. const match = event.match(/([\S\d]+)\.([\S\d]+)/g)
  210. match && inlinedAssets.push(match[0])
  211. })
  212. }
  213. })
  214. }
  215. async stop(): Promise<void> {
  216. return new Promise((resolve, reject) => {
  217. this.tauriWatcher && this.tauriWatcher.close()
  218. this.__stopCargo()
  219. .then(resolve)
  220. .catch(reject)
  221. })
  222. }
  223. async __runCargoCommand({
  224. cargoArgs,
  225. extraArgs,
  226. dev = false,
  227. exitOnPanic = true
  228. }: {
  229. cargoArgs: string[]
  230. extraArgs?: string[]
  231. dev?: boolean,
  232. exitOnPanic?: boolean
  233. }): Promise<void> {
  234. return new Promise((resolve, reject) => {
  235. this.pid = spawn(
  236. 'cargo',
  237. extraArgs ? cargoArgs.concat(['--']).concat(extraArgs) : cargoArgs,
  238. tauriDir,
  239. code => {
  240. if (dev && !exitOnPanic && code === 101) {
  241. this.pid = 0
  242. resolve()
  243. return
  244. }
  245. if (code) {
  246. warn()
  247. warn('⚠️ [FAIL] Cargo CLI has failed')
  248. warn()
  249. reject()
  250. process.exit(1)
  251. } else if (!dev) {
  252. resolve()
  253. }
  254. if (this.killPromise) {
  255. this.killPromise()
  256. this.killPromise = undefined
  257. } else if (dev) {
  258. warn()
  259. warn('Cargo process was killed. Exiting...')
  260. warn()
  261. process.exit(0)
  262. }
  263. resolve()
  264. }
  265. )
  266. if (dev) {
  267. resolve()
  268. }
  269. })
  270. }
  271. async __stopCargo(): Promise<void> {
  272. const pid = this.pid
  273. if (!pid) {
  274. return Promise.resolve()
  275. }
  276. log('Shutting down tauri process...')
  277. this.pid = 0
  278. return new Promise((resolve, reject) => {
  279. this.killPromise = resolve
  280. try {
  281. process.kill(pid)
  282. } catch (e) {
  283. reject(e)
  284. }
  285. })
  286. }
  287. __getManifestPath(): string {
  288. return path.join(tauriDir, 'Cargo.toml')
  289. }
  290. __getManifest(): JsonMap {
  291. const tomlPath = this.__getManifestPath()
  292. const tomlFile = readFileSync(tomlPath).toString()
  293. const tomlContents = toml.parse(tomlFile)
  294. return tomlContents
  295. }
  296. __rewriteManifest(tomlContents: JsonMap) {
  297. const tomlPath = this.__getManifestPath()
  298. const output = toml.stringify(tomlContents)
  299. writeFileSync(tomlPath, output)
  300. }
  301. __whitelistApi(
  302. cfg: TauriConfig,
  303. tomlContents: { [index: string]: any }
  304. ): void {
  305. const tomlFeatures = []
  306. if (cfg.tauri.whitelist.all) {
  307. tomlFeatures.push('all-api')
  308. } else {
  309. const whitelist = Object.keys(cfg.tauri.whitelist).filter(
  310. w => cfg.tauri.whitelist[String(w)] === true
  311. )
  312. tomlFeatures.push(...whitelist)
  313. }
  314. if (cfg.tauri.edge.active) {
  315. tomlFeatures.push('edge')
  316. }
  317. tomlContents.dependencies.tauri.features = tomlFeatures
  318. }
  319. }
  320. export default Runner