Modules.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. const fs = require('fs')
  2. const Path = require('path')
  3. const { promisify, callbackify } = require('util')
  4. const Settings = require('@overleaf/settings')
  5. const Views = require('./Views')
  6. const _ = require('lodash')
  7. const Metrics = require('@overleaf/metrics')
  8. const MODULE_BASE_PATH = Path.join(__dirname, '/../../../modules')
  9. const _modules = []
  10. let _modulesLoaded = false
  11. const _hooks = {}
  12. const _middleware = {}
  13. let _viewIncludes = {}
  14. async function modules() {
  15. if (!_modulesLoaded) {
  16. const beforeLoadModules = performance.now()
  17. await loadModules()
  18. Metrics.gauge('web_startup', performance.now() - beforeLoadModules, 1, {
  19. path: 'loadModules',
  20. })
  21. }
  22. return _modules
  23. }
  24. async function loadModulesImpl() {
  25. const settingsCheckModule = Path.join(
  26. MODULE_BASE_PATH,
  27. 'settings-check',
  28. 'index.mjs'
  29. )
  30. if (fs.existsSync(settingsCheckModule)) {
  31. await import(settingsCheckModule)
  32. }
  33. for (const moduleName of Settings.moduleImportSequence || []) {
  34. const module = await import(
  35. Path.join(MODULE_BASE_PATH, moduleName, 'index.mjs')
  36. )
  37. const loadedModule = module.default || module
  38. loadedModule.name = moduleName
  39. _modules.push(loadedModule)
  40. if (loadedModule.viewIncludes) {
  41. throw new Error(
  42. `${moduleName}: module.viewIncludes moved into Settings.viewIncludes`
  43. )
  44. }
  45. if (loadedModule.dependencies) {
  46. for (const dependency of loadedModule.dependencies) {
  47. if (!Settings.moduleImportSequence.includes(dependency)) {
  48. throw new Error(
  49. `Module '${dependency}' listed as a dependency of '${moduleName}' is missing in the moduleImportSequence. Please also verify that it is available in the current environment.`
  50. )
  51. }
  52. }
  53. }
  54. }
  55. _modulesLoaded = true
  56. await attachHooks()
  57. await attachMiddleware()
  58. }
  59. const loadModules = _.memoize(loadModulesImpl)
  60. async function applyRouter(webRouter, privateApiRouter, publicApiRouter) {
  61. for (const module of await modules()) {
  62. if (module.router && module.router.apply) {
  63. await module.router.apply(webRouter, privateApiRouter, publicApiRouter)
  64. }
  65. }
  66. }
  67. async function applyNonCsrfRouter(
  68. webRouter,
  69. privateApiRouter,
  70. publicApiRouter
  71. ) {
  72. for (const module of await modules()) {
  73. if (module.nonCsrfRouter != null) {
  74. module.nonCsrfRouter.apply(webRouter, privateApiRouter, publicApiRouter)
  75. }
  76. if (module.router && module.router.applyNonCsrfRouter) {
  77. module.router.applyNonCsrfRouter(
  78. webRouter,
  79. privateApiRouter,
  80. publicApiRouter
  81. )
  82. }
  83. }
  84. }
  85. async function start() {
  86. for (const module of await modules()) {
  87. await module.start?.()
  88. }
  89. }
  90. function loadViewIncludes(app) {
  91. _viewIncludes = Views.compileViewIncludes(app)
  92. }
  93. async function applyMiddleware(appOrRouter, middlewareName, options) {
  94. if (!middlewareName) {
  95. throw new Error(
  96. 'middleware name must be provided to register module middleware'
  97. )
  98. }
  99. for (const module of await modules()) {
  100. if (module[middlewareName]) {
  101. module[middlewareName](appOrRouter, options)
  102. }
  103. }
  104. }
  105. function moduleIncludes(view, locals) {
  106. const compiledPartials = _viewIncludes[view] || []
  107. let html = ''
  108. for (const compiledPartial of compiledPartials) {
  109. html += compiledPartial(locals)
  110. }
  111. return html
  112. }
  113. function moduleIncludesAvailable(view) {
  114. return (_viewIncludes[view] || []).length > 0
  115. }
  116. async function linkedFileAgentsIncludes() {
  117. const agents = {}
  118. for (const module of await modules()) {
  119. for (const name in module.linkedFileAgents) {
  120. const agentFunction = module.linkedFileAgents[name]
  121. agents[name] = agentFunction()
  122. }
  123. }
  124. return agents
  125. }
  126. async function attachHooks() {
  127. for (const module of await modules()) {
  128. const { promises, ...hooks } = module.hooks || {}
  129. for (const hook in promises || {}) {
  130. const method = promises[hook]
  131. attachHook(hook, method)
  132. }
  133. for (const hook in hooks || {}) {
  134. const method = hooks[hook]
  135. attachHook(hook, promisify(method))
  136. }
  137. }
  138. }
  139. function attachHook(name, method) {
  140. if (_hooks[name] == null) {
  141. _hooks[name] = []
  142. }
  143. _hooks[name].push(method)
  144. }
  145. async function attachMiddleware() {
  146. for (const module of await modules()) {
  147. for (const middleware in module.middleware || {}) {
  148. const method = module.middleware[middleware]
  149. if (_middleware[middleware] == null) {
  150. _middleware[middleware] = []
  151. }
  152. _middleware[middleware].push(method)
  153. }
  154. }
  155. }
  156. async function fireHook(name, ...args) {
  157. // ensure that modules are loaded if we need to fire a hook
  158. // this can happen if a script calls a method that fires a hook
  159. if (!_modulesLoaded) {
  160. await loadModules()
  161. }
  162. const methods = _hooks[name] || []
  163. const results = []
  164. for (const method of methods) {
  165. const result = await method(...args)
  166. results.push(result)
  167. }
  168. return results
  169. }
  170. async function getMiddleware(name) {
  171. // ensure that modules are loaded if we need to call a middleware
  172. if (!_modulesLoaded) {
  173. await loadModules()
  174. }
  175. return _middleware[name] || []
  176. }
  177. module.exports = {
  178. applyNonCsrfRouter,
  179. applyRouter,
  180. linkedFileAgentsIncludes,
  181. loadViewIncludes,
  182. moduleIncludes,
  183. moduleIncludesAvailable,
  184. applyMiddleware,
  185. start,
  186. hooks: {
  187. attach: attachHook,
  188. fire: callbackify(fireHook),
  189. },
  190. middleware: getMiddleware,
  191. promises: {
  192. hooks: {
  193. fire: fireHook,
  194. },
  195. },
  196. }