Modules.mjs 5.7 KB

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