Views.mjs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import logger from '@overleaf/logger'
  2. import pug from 'pug'
  3. import globby from 'globby'
  4. import Settings from '@overleaf/settings'
  5. import fs from 'node:fs'
  6. import Path from 'node:path'
  7. // Generate list of view names from app/views
  8. function buildViewList() {
  9. return globby
  10. .sync('app/views/**/*.pug', {
  11. onlyFiles: true,
  12. concurrency: 1,
  13. ignore: [
  14. // Ignore includes
  15. '**/_*.pug',
  16. '**/_*/**',
  17. // Ignore shared layout files
  18. 'app/views/layout*',
  19. 'app/views/layout/*',
  20. ],
  21. })
  22. .concat(
  23. globby.sync('modules/*/app/views/**/*.pug', {
  24. onlyFiles: true,
  25. concurrency: 1,
  26. // Ignore includes
  27. ignore: ['**/_*.pug', '**/_*/**'],
  28. })
  29. )
  30. .concat(Object.values(Settings.viewIncludes).flat())
  31. .map(x => Path.resolve(x))
  32. }
  33. const PUG_COMPILE_ARGUMENTS = {
  34. doctype: 'html',
  35. cache: true,
  36. compileDebug: Settings.debugPugTemplates,
  37. inlineRuntimeFunctions: false,
  38. module: true,
  39. }
  40. /**
  41. * @param {string} compiled
  42. * @return {{duplicates: Array<string>, found: Array<string>}}
  43. * @private
  44. */
  45. function _findAllMetaTags(compiled) {
  46. const inString = /name=(\\?["'`])(ol-.+?)\1/g
  47. const asExpression = /pug\.attr\("name",\s*(["'`])(ol-.+?)\1/g
  48. const found = new Set()
  49. const duplicates = new Set()
  50. for (const regex of [inString, asExpression]) {
  51. for (const [, , name] of compiled.matchAll(regex)) {
  52. if (found.has(name)) duplicates.add(name)
  53. found.add(name)
  54. }
  55. }
  56. // Special case: Ignore the loop for adding permissions meta tags.
  57. duplicates.delete('ol-cannot-')
  58. return { found: Array.from(found), duplicates: Array.from(duplicates) }
  59. }
  60. /**
  61. * @param {string} filePath
  62. * @param {string} firstLine
  63. * @return {boolean}
  64. * @private
  65. */
  66. function _expectMetaFor(filePath, firstLine) {
  67. // no-js pages have no use for ol-meta tags
  68. if (firstLine.match(/extends .*layout\/layout-no-js/)) return false
  69. // plain html pages have no use for ol-meta tags
  70. if (firstLine === 'doctype html') return false
  71. // xml pages do not use meta tags
  72. if (firstLine === 'doctype xml') return false
  73. // view includes should not add meta tags as we cannot trace these easily.
  74. if (Object.values(Settings.viewIncludes).flat().includes(filePath)) {
  75. if (
  76. filePath === Path.resolve('modules/writefull/app/views/_editor_meta.pug')
  77. ) {
  78. // Special case: The Writefull module adds meta tags to editor, see inline comment there
  79. return true
  80. }
  81. // default case: no meta tags
  82. return false
  83. }
  84. // default to expect meta tags in top-level templates
  85. return true
  86. }
  87. /**
  88. * @param {string} filePath
  89. * @param {string} compiled
  90. */
  91. function checkForDuplicateMeta(filePath, compiled) {
  92. const { found, duplicates } = _findAllMetaTags(compiled)
  93. if (duplicates.length !== 0) {
  94. throw new Error(
  95. `Found duplicate meta tags in ${filePath} (or it's imports): ${Array.from(duplicates)}`
  96. )
  97. }
  98. const firstLine = fs.readFileSync(filePath, 'utf-8').split('\n', 1)[0]
  99. const expectNoMeta = _expectMetaFor(filePath, firstLine)
  100. if (found.length === 0 && expectNoMeta) {
  101. throw new Error(
  102. `Expected to find meta entries in ${filePath} (or it's imports)`
  103. )
  104. }
  105. if (!expectNoMeta && found.length !== 0) {
  106. throw new Error(
  107. `Expected to find no meta entries in plain html or 'viewIncludes'. Found ${Array.from(found)} in ${filePath} (or it's imports)`
  108. )
  109. }
  110. }
  111. function precompileViewsAndCacheToDisk() {
  112. const startTime = Date.now()
  113. let success = 0
  114. let precompiled = 0
  115. for (const filePath of buildViewList()) {
  116. const precompiledFilename = filePath.replace(/\.pug$/, '.js')
  117. try {
  118. const compiled = pug.compileFileClient(filePath, PUG_COMPILE_ARGUMENTS)
  119. try {
  120. if (fs.readFileSync(precompiledFilename, 'utf-8') === compiled) {
  121. precompiled++
  122. continue
  123. }
  124. } catch {}
  125. checkForDuplicateMeta(filePath, compiled)
  126. fs.writeFileSync(precompiledFilename, compiled, {
  127. encoding: 'utf-8',
  128. mode: 0o644,
  129. })
  130. success++
  131. } catch (err) {
  132. logger.err({ err, filePath }, 'failed to precompile pug template')
  133. throw err
  134. }
  135. }
  136. logger.info(
  137. { timeTaken: Date.now() - startTime, success, precompiled },
  138. 'compiled pug templates'
  139. )
  140. }
  141. export default {
  142. // for tests
  143. PUG_COMPILE_ARGUMENTS,
  144. _expectMetaFor,
  145. _findAllMetaTags,
  146. compileViewIncludes(app) {
  147. const viewIncludes = {}
  148. for (const [view, paths] of Object.entries(Settings.viewIncludes)) {
  149. viewIncludes[view] = []
  150. for (const filePath of paths) {
  151. viewIncludes[view].push(
  152. pug.compileFile(filePath, {
  153. ...PUG_COMPILE_ARGUMENTS,
  154. cache: app.enabled('view cache'),
  155. })
  156. )
  157. }
  158. }
  159. return viewIncludes
  160. },
  161. async precompileViews(app) {
  162. const startTime = Date.now()
  163. let success = 0
  164. let precompiled = 0
  165. let failures = 0
  166. for (const filePath of buildViewList()) {
  167. const precompiledFilename = filePath.replace(/\.pug$/, '.js')
  168. if (fs.existsSync(precompiledFilename)) {
  169. logger.debug({ filePath }, 'loading precompiled pug template')
  170. try {
  171. // We need to retrieve the `default` property as a result of using dynamic imports
  172. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import#module_namespace_object
  173. pug.cache[filePath] = (await import(precompiledFilename)).default
  174. precompiled++
  175. continue
  176. } catch (err) {
  177. logger.error(
  178. { filePath, err },
  179. 'error loading precompiled pug template'
  180. )
  181. failures++
  182. }
  183. }
  184. try {
  185. logger.warn({ filePath }, 'compiling pug template at boot time')
  186. pug.compileFile(filePath, PUG_COMPILE_ARGUMENTS)
  187. success++
  188. } catch (err) {
  189. logger.error({ filePath, err }, 'error compiling pug template')
  190. failures++
  191. }
  192. }
  193. logger.debug(
  194. { timeTaken: Date.now() - startTime, failures, success, precompiled },
  195. 'compiled pug templates'
  196. )
  197. },
  198. }
  199. if (import.meta.main) {
  200. precompileViewsAndCacheToDisk()
  201. process.exit(0)
  202. }