plugins.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import * as fs from 'mz/fs'
  2. import * as path from 'path'
  3. import * as remote from '@electron/remote'
  4. const nodeModule = require('module') // eslint-disable-line @typescript-eslint/no-var-requires
  5. const nodeRequire = (global as any).require
  6. function normalizePath (p: string): string {
  7. const cygwinPrefix = '/cygdrive/'
  8. if (p.startsWith(cygwinPrefix)) {
  9. p = p.substring(cygwinPrefix.length).replace('/', '\\')
  10. p = p[0] + ':' + p.substring(1)
  11. }
  12. return p
  13. }
  14. global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
  15. if (process.env.TERMINUS_DEV) {
  16. nodeModule.globalPaths.unshift(path.dirname(remote.app.getAppPath()))
  17. }
  18. const builtinPluginsPath = process.env.TERMINUS_DEV ? path.dirname(remote.app.getAppPath()) : path.join((process as any).resourcesPath, 'builtin-plugins')
  19. const userPluginsPath = path.join(
  20. remote.app.getPath('userData'),
  21. 'plugins',
  22. )
  23. if (!fs.existsSync(userPluginsPath)) {
  24. fs.mkdir(userPluginsPath)
  25. }
  26. Object.assign(window, { builtinPluginsPath, userPluginsPath })
  27. nodeModule.globalPaths.unshift(builtinPluginsPath)
  28. nodeModule.globalPaths.unshift(path.join(userPluginsPath, 'node_modules'))
  29. // nodeModule.globalPaths.unshift(path.join((process as any).resourcesPath, 'app.asar', 'node_modules'))
  30. if (process.env.TERMINUS_PLUGINS) {
  31. process.env.TERMINUS_PLUGINS.split(':').map(x => nodeModule.globalPaths.push(normalizePath(x)))
  32. }
  33. export type ProgressCallback = (current: number, total: number) => void // eslint-disable-line @typescript-eslint/no-type-alias
  34. export interface PluginInfo {
  35. name: string
  36. description: string
  37. packageName: string
  38. isBuiltin: boolean
  39. version: string
  40. author: string
  41. homepage?: string
  42. path?: string
  43. info?: any
  44. }
  45. const builtinModules = [
  46. '@angular/animations',
  47. '@angular/common',
  48. '@angular/compiler',
  49. '@angular/core',
  50. '@angular/forms',
  51. '@angular/platform-browser',
  52. '@angular/platform-browser-dynamic',
  53. '@ng-bootstrap/ng-bootstrap',
  54. 'ngx-toastr',
  55. 'rxjs',
  56. 'rxjs/operators',
  57. 'terminus-core',
  58. 'terminus-local',
  59. 'terminus-settings',
  60. 'terminus-terminal',
  61. 'zone.js/dist/zone.js',
  62. ]
  63. const cachedBuiltinModules = {}
  64. builtinModules.forEach(m => {
  65. const label = 'Caching ' + m
  66. console.time(label)
  67. cachedBuiltinModules[m] = nodeRequire(m)
  68. console.timeEnd(label)
  69. })
  70. const originalRequire = (global as any).require
  71. ;(global as any).require = function (query: string) {
  72. if (cachedBuiltinModules[query]) {
  73. return cachedBuiltinModules[query]
  74. }
  75. return originalRequire.apply(this, [query])
  76. }
  77. const originalModuleRequire = nodeModule.prototype.require
  78. nodeModule.prototype.require = function (query: string) {
  79. if (cachedBuiltinModules[query]) {
  80. return cachedBuiltinModules[query]
  81. }
  82. return originalModuleRequire.call(this, query)
  83. }
  84. export async function findPlugins (): Promise<PluginInfo[]> {
  85. const paths = nodeModule.globalPaths
  86. let foundPlugins: PluginInfo[] = []
  87. const candidateLocations: { pluginDir: string, packageName: string }[] = []
  88. const PREFIX = 'terminus-'
  89. for (let pluginDir of paths) {
  90. pluginDir = normalizePath(pluginDir)
  91. if (!await fs.exists(pluginDir)) {
  92. continue
  93. }
  94. const pluginNames = await fs.readdir(pluginDir)
  95. if (await fs.exists(path.join(pluginDir, 'package.json'))) {
  96. candidateLocations.push({
  97. pluginDir: path.dirname(pluginDir),
  98. packageName: path.basename(pluginDir),
  99. })
  100. }
  101. for (const packageName of pluginNames) {
  102. if (packageName.startsWith(PREFIX)) {
  103. candidateLocations.push({ pluginDir, packageName })
  104. }
  105. }
  106. }
  107. for (const { pluginDir, packageName } of candidateLocations) {
  108. const pluginPath = path.join(pluginDir, packageName)
  109. const infoPath = path.join(pluginPath, 'package.json')
  110. if (!await fs.exists(infoPath)) {
  111. continue
  112. }
  113. const name = packageName.substring(PREFIX.length)
  114. if (builtinModules.includes(packageName) && pluginDir !== builtinPluginsPath) {
  115. continue
  116. }
  117. if (foundPlugins.some(x => x.name === name)) {
  118. console.info(`Plugin ${packageName} already exists, overriding`)
  119. foundPlugins = foundPlugins.filter(x => x.name !== name)
  120. }
  121. try {
  122. const info = JSON.parse(await fs.readFile(infoPath, { encoding: 'utf-8' }))
  123. if (!info.keywords || !(info.keywords.includes('terminus-plugin') || info.keywords.includes('terminus-builtin-plugin'))) {
  124. continue
  125. }
  126. let author = info.author
  127. author = author.name || author
  128. foundPlugins.push({
  129. name: name,
  130. packageName: packageName,
  131. isBuiltin: pluginDir === builtinPluginsPath,
  132. version: info.version,
  133. description: info.description,
  134. author,
  135. path: pluginPath,
  136. info,
  137. })
  138. } catch (error) {
  139. console.error('Cannot load package info for', packageName)
  140. }
  141. }
  142. foundPlugins.sort((a, b) => a.name > b.name ? 1 : -1)
  143. ;(window as any).installedPlugins = foundPlugins
  144. return foundPlugins
  145. }
  146. export async function loadPlugins (foundPlugins: PluginInfo[], progress: ProgressCallback): Promise<any[]> {
  147. const plugins: any[] = []
  148. progress(0, 1)
  149. let index = 0
  150. for (const foundPlugin of foundPlugins) {
  151. console.info(`Loading ${foundPlugin.name}: ${nodeRequire.resolve(foundPlugin.path)}`)
  152. progress(index, foundPlugins.length)
  153. try {
  154. const label = 'Loading ' + foundPlugin.name
  155. console.time(label)
  156. const packageModule = nodeRequire(foundPlugin.path)
  157. const pluginModule = packageModule.default.forRoot ? packageModule.default.forRoot() : packageModule.default
  158. pluginModule.pluginName = foundPlugin.name
  159. pluginModule.bootstrap = packageModule.bootstrap
  160. plugins.push(pluginModule)
  161. console.timeEnd(label)
  162. await new Promise(x => setTimeout(x, 50))
  163. } catch (error) {
  164. console.error(`Could not load ${foundPlugin.name}:`, error)
  165. }
  166. index++
  167. }
  168. progress(1, 1)
  169. return plugins
  170. }