runner.js 6.9 KB

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