runner.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. const
  2. chokidar = require('chokidar')
  3. const debounce = require('lodash.debounce')
  4. const path = require('path')
  5. const { readFileSync, writeFileSync } = 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. new Inliner(path.join(indexDir, 'index.html'), (err, html) => {
  115. if (err) {
  116. reject(err)
  117. } else {
  118. const dom = new JSDOM(html)
  119. const document = dom.window.document
  120. document.querySelectorAll('link').forEach(link => {
  121. link.removeAttribute('rel')
  122. link.removeAttribute('as')
  123. })
  124. const tauriScript = document.createElement('script')
  125. tauriScript.text = readFileSync(path.join(tauriDir, 'tauri.js'))
  126. document.body.insertBefore(tauriScript, document.body.firstChild)
  127. const csp = cfg.tauri.security.csp
  128. if (csp) {
  129. const cspTag = document.createElement('meta')
  130. cspTag.setAttribute('http-equiv', 'Content-Security-Policy')
  131. cspTag.setAttribute('content', csp)
  132. document.head.appendChild(cspTag)
  133. }
  134. writeFileSync(path.join(indexDir, 'index.tauri.html'), dom.serialize())
  135. resolve(inlinedAssets)
  136. }
  137. }).on('progress', event => {
  138. const match = event.match(/([\S\d]+)\.([\S\d]+)/g)
  139. match && inlinedAssets.push(match[0])
  140. })
  141. })
  142. }
  143. stop () {
  144. return new Promise((resolve, reject) => {
  145. this.tauriWatcher && this.tauriWatcher.close()
  146. this.__stopCargo().then(resolve)
  147. })
  148. }
  149. __runCargoCommand ({
  150. cargoArgs,
  151. extraArgs,
  152. dev = false
  153. }) {
  154. return new Promise(resolve => {
  155. this.pid = spawn(
  156. 'cargo',
  157. extraArgs
  158. ? cargoArgs.concat(['--']).concat(extraArgs)
  159. : cargoArgs,
  160. tauriDir,
  161. code => {
  162. if (code) {
  163. warn()
  164. warn('⚠️ [FAIL] Cargo CLI has failed')
  165. warn()
  166. process.exit(1)
  167. }
  168. if (this.killPromise) {
  169. this.killPromise()
  170. this.killPromise = null
  171. } else if (dev) {
  172. warn()
  173. warn('Cargo process was killed. Exiting...')
  174. warn()
  175. process.exit(0)
  176. }
  177. }
  178. )
  179. resolve()
  180. })
  181. }
  182. __stopCargo () {
  183. const pid = this.pid
  184. if (!pid) {
  185. return Promise.resolve()
  186. }
  187. log('Shutting down tauri process...')
  188. this.pid = 0
  189. return new Promise((resolve, reject) => {
  190. this.killPromise = resolve
  191. process.kill(pid)
  192. })
  193. }
  194. __manipulateToml (callback) {
  195. const toml = require('@tauri-apps/toml')
  196. const tomlPath = path.join(tauriDir, 'Cargo.toml')
  197. const tomlFile = readFileSync(tomlPath)
  198. const tomlContents = toml.parse(tomlFile)
  199. callback(tomlContents)
  200. const output = toml.stringify(tomlContents)
  201. writeFileSync(tomlPath, output)
  202. }
  203. __whitelistApi (cfg, tomlContents) {
  204. const tomlFeatures = []
  205. if (cfg.tauri.whitelist.all) {
  206. tomlFeatures.push('all-api')
  207. } else {
  208. const whitelist = Object.keys(cfg.tauri.whitelist).filter(w => cfg.tauri.whitelist[w] === true)
  209. tomlFeatures.push(...whitelist)
  210. }
  211. if (cfg.tauri.edge.active) {
  212. tomlFeatures.push('edge')
  213. }
  214. tomlContents.dependencies.tauri.features = tomlFeatures
  215. }
  216. }
  217. module.exports = Runner