You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							766 lines
						
					
					
						
							22 KiB
						
					
					
				
			
		
		
	
	
							766 lines
						
					
					
						
							22 KiB
						
					
					
				| 'use strict'
 | |
| 
 | |
| const fp = require('fastify-plugin')
 | |
| const readFile = require('fs').readFile
 | |
| const accessSync = require('fs').accessSync
 | |
| const existsSync = require('fs').existsSync
 | |
| const mkdirSync = require('fs').mkdirSync
 | |
| const readdirSync = require('fs').readdirSync
 | |
| const resolve = require('path').resolve
 | |
| const join = require('path').join
 | |
| const { basename, dirname, extname } = require('path')
 | |
| const HLRU = require('hashlru')
 | |
| const supportedEngines = ['ejs', 'nunjucks', 'pug', 'handlebars', 'mustache', 'art-template', 'twig', 'liquid', 'dot', 'eta']
 | |
| 
 | |
| function fastifyView (fastify, opts, next) {
 | |
|   if (!opts.engine) {
 | |
|     next(new Error('Missing engine'))
 | |
|     return
 | |
|   }
 | |
|   const type = Object.keys(opts.engine)[0]
 | |
|   if (supportedEngines.indexOf(type) === -1) {
 | |
|     next(new Error(`'${type}' not yet supported, PR? :)`))
 | |
|     return
 | |
|   }
 | |
|   const charset = opts.charset || 'utf-8'
 | |
|   const propertyName = opts.propertyName || 'view'
 | |
|   const engine = opts.engine[type]
 | |
|   const globalOptions = opts.options || {}
 | |
|   const templatesDir = resolveTemplateDir(opts)
 | |
|   const lru = HLRU(opts.maxCache || 100)
 | |
|   const includeViewExtension = opts.includeViewExtension || false
 | |
|   const viewExt = opts.viewExt || ''
 | |
|   const prod = typeof opts.production === 'boolean' ? opts.production : process.env.NODE_ENV === 'production'
 | |
|   const defaultCtx = opts.defaultContext || {}
 | |
|   const globalLayoutFileName = opts.layout
 | |
| 
 | |
|   function templatesDirIsValid (_templatesDir) {
 | |
|     if (Array.isArray(_templatesDir) && type !== 'nunjucks') {
 | |
|       throw new Error('Only Nunjucks supports the "templates" option as an array')
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function layoutIsValid (_layoutFileName) {
 | |
|     if (type !== 'dot' && type !== 'handlebars' && type !== 'ejs' && type !== 'eta') {
 | |
|       throw new Error('Only Dot, Handlebars, EJS, and Eta support the "layout" option')
 | |
|     }
 | |
| 
 | |
|     if (!hasAccessToLayoutFile(_layoutFileName, getDefaultExtension(type))) {
 | |
|       throw new Error(`unable to access template "${_layoutFileName}"`)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   try {
 | |
|     templatesDirIsValid(templatesDir)
 | |
| 
 | |
|     if (globalLayoutFileName) {
 | |
|       layoutIsValid(globalLayoutFileName)
 | |
|     }
 | |
|   } catch (error) {
 | |
|     next(error)
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   const dotRender = type === 'dot' ? viewDot.call(fastify, preProcessDot.call(fastify, templatesDir, globalOptions)) : null
 | |
|   const nunjucksEnv = type === 'nunjucks' ? engine.configure(templatesDir, globalOptions) : null
 | |
| 
 | |
|   const renders = {
 | |
|     ejs: withLayout(viewEjs, globalLayoutFileName),
 | |
|     handlebars: withLayout(viewHandlebars, globalLayoutFileName),
 | |
|     mustache: viewMustache,
 | |
|     nunjucks: viewNunjucks,
 | |
|     'art-template': viewArtTemplate,
 | |
|     twig: viewTwig,
 | |
|     liquid: viewLiquid,
 | |
|     dot: withLayout(dotRender, globalLayoutFileName),
 | |
|     eta: withLayout(viewEta, globalLayoutFileName),
 | |
|     _default: view
 | |
|   }
 | |
| 
 | |
|   const renderer = renders[type] ? renders[type] : renders._default
 | |
| 
 | |
|   function viewDecorator () {
 | |
|     const args = Array.from(arguments)
 | |
| 
 | |
|     let done
 | |
|     if (typeof args[args.length - 1] === 'function') {
 | |
|       done = args.pop()
 | |
|     }
 | |
| 
 | |
|     const promise = new Promise((resolve, reject) => {
 | |
|       renderer.apply({
 | |
|         getHeader: () => { },
 | |
|         header: () => { },
 | |
|         send: result => {
 | |
|           if (result instanceof Error) {
 | |
|             reject(result)
 | |
|             return
 | |
|           }
 | |
| 
 | |
|           resolve(result)
 | |
|         }
 | |
|       }, args)
 | |
|     })
 | |
| 
 | |
|     if (done && typeof done === 'function') {
 | |
|       promise.then(done.bind(null, null), done)
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     return promise
 | |
|   }
 | |
| 
 | |
|   viewDecorator.clearCache = function () {
 | |
|     lru.clear()
 | |
|   }
 | |
| 
 | |
|   fastify.decorate(propertyName, viewDecorator)
 | |
| 
 | |
|   fastify.decorateReply(propertyName, function () {
 | |
|     renderer.apply(this, arguments)
 | |
|     return this
 | |
|   })
 | |
| 
 | |
|   if (!fastify.hasReplyDecorator('locals')) {
 | |
|     fastify.decorateReply('locals', null)
 | |
| 
 | |
|     fastify.addHook('onRequest', (req, reply, done) => {
 | |
|       reply.locals = {}
 | |
|       done()
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   function getPage (page, extension) {
 | |
|     const pageLRU = `getPage-${page}-${extension}`
 | |
|     let result = lru.get(pageLRU)
 | |
| 
 | |
|     if (typeof result === 'string') {
 | |
|       return result
 | |
|     }
 | |
| 
 | |
|     const filename = basename(page, extname(page))
 | |
|     result = join(dirname(page), filename + getExtension(page, extension))
 | |
| 
 | |
|     lru.set(pageLRU, result)
 | |
| 
 | |
|     return result
 | |
|   }
 | |
| 
 | |
|   function getDefaultExtension (type) {
 | |
|     const mappedExtensions = {
 | |
|       'art-template': 'art',
 | |
|       handlebars: 'hbs',
 | |
|       nunjucks: 'njk'
 | |
|     }
 | |
| 
 | |
|     return viewExt || (mappedExtensions[type] || type)
 | |
|   }
 | |
| 
 | |
|   function getExtension (page, extension) {
 | |
|     let filextension = extname(page)
 | |
|     if (!filextension) {
 | |
|       filextension = '.' + getDefaultExtension(type)
 | |
|     }
 | |
| 
 | |
|     return viewExt ? `.${viewExt}` : (includeViewExtension ? `.${extension}` : filextension)
 | |
|   }
 | |
| 
 | |
|   function isPathExcludedMinification (currentPath, pathsToExclude) {
 | |
|     return (pathsToExclude && Array.isArray(pathsToExclude)) ? pathsToExclude.includes(currentPath) : false
 | |
|   }
 | |
| 
 | |
|   function useHtmlMinification (globalOpts, requestedPath) {
 | |
|     return globalOptions.useHtmlMinifier &&
 | |
|       (typeof globalOptions.useHtmlMinifier.minify === 'function') &&
 | |
|       !isPathExcludedMinification(requestedPath, globalOptions.pathsToExcludeHtmlMinifier)
 | |
|   }
 | |
| 
 | |
|   function getRequestedPath (fastify) {
 | |
|     return (fastify && fastify.request) ? fastify.request.routerPath : null
 | |
|   }
 | |
|   // Gets template as string (or precompiled for Handlebars)
 | |
|   // from LRU cache or filesystem.
 | |
|   const getTemplate = function (file, callback, requestedPath) {
 | |
|     const data = lru.get(file)
 | |
|     if (data && prod) {
 | |
|       callback(null, data)
 | |
|     } else {
 | |
|       readFile(join(templatesDir, file), 'utf-8', (err, data) => {
 | |
|         if (err) {
 | |
|           callback(err, null)
 | |
|           return
 | |
|         }
 | |
| 
 | |
|         if (useHtmlMinification(globalOptions, requestedPath)) {
 | |
|           data = globalOptions.useHtmlMinifier.minify(data, globalOptions.htmlMinifierOptions || {})
 | |
|         }
 | |
|         if (type === 'handlebars') {
 | |
|           data = engine.compile(data)
 | |
|         }
 | |
|         lru.set(file, data)
 | |
|         callback(null, data)
 | |
|       })
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Gets partials as collection of strings from LRU cache or filesystem.
 | |
|   const getPartials = function (page, { partials, requestedPath }, callback) {
 | |
|     const cacheKey = getPartialsCacheKey(page, partials, requestedPath)
 | |
|     const partialsObj = lru.get(cacheKey)
 | |
|     if (partialsObj && prod) {
 | |
|       callback(null, partialsObj)
 | |
|     } else {
 | |
|       let filesToLoad = Object.keys(partials).length
 | |
|       if (filesToLoad === 0) {
 | |
|         callback(null, {})
 | |
|         return
 | |
|       }
 | |
|       let error = null
 | |
|       const partialsHtml = {}
 | |
|       Object.keys(partials).forEach((key, index) => {
 | |
|         readFile(join(templatesDir, partials[key]), 'utf-8', (err, data) => {
 | |
|           if (err) {
 | |
|             error = err
 | |
|           }
 | |
|           if (useHtmlMinification(globalOptions, requestedPath)) {
 | |
|             data = globalOptions.useHtmlMinifier.minify(data, globalOptions.htmlMinifierOptions || {})
 | |
|           }
 | |
| 
 | |
|           partialsHtml[key] = data
 | |
|           if (--filesToLoad === 0) {
 | |
|             lru.set(cacheKey, partialsHtml)
 | |
|             callback(error, partialsHtml)
 | |
|           }
 | |
|         })
 | |
|       })
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function getPartialsCacheKey (page, partials, requestedPath) {
 | |
|     let cacheKey = page
 | |
| 
 | |
|     for (const key of Object.keys(partials)) {
 | |
|       cacheKey += `|${key}:${partials[key]}`
 | |
|     }
 | |
| 
 | |
|     cacheKey += `|${requestedPath}-Partials`
 | |
| 
 | |
|     return cacheKey
 | |
|   }
 | |
| 
 | |
|   function readCallback (that, page, data, localOptions) {
 | |
|     return function _readCallback (err, html) {
 | |
|       const requestedPath = getRequestedPath(that)
 | |
| 
 | |
|       if (err) {
 | |
|         that.send(err)
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       let compiledPage
 | |
|       try {
 | |
|         if ((type === 'ejs') && viewExt && !globalOptions.includer) {
 | |
|           globalOptions.includer = (originalPath, parsedPath) => {
 | |
|             return {
 | |
|               filename: parsedPath || join(templatesDir, originalPath + '.' + viewExt)
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         globalOptions.filename = join(templatesDir, page)
 | |
|         if (localOptions) {
 | |
|           for (const key in globalOptions) {
 | |
|             if (!Object.prototype.hasOwnProperty.call(localOptions, key)) localOptions[key] = globalOptions[key]
 | |
|           }
 | |
|         } else localOptions = globalOptions
 | |
|         compiledPage = engine.compile(html, localOptions)
 | |
|       } catch (error) {
 | |
|         that.send(error)
 | |
|         return
 | |
|       }
 | |
|       lru.set(page, compiledPage)
 | |
| 
 | |
|       if (!that.getHeader('content-type')) {
 | |
|         that.header('Content-Type', 'text/html; charset=' + charset)
 | |
|       }
 | |
|       let cachedPage
 | |
|       try {
 | |
|         cachedPage = lru.get(page)(data)
 | |
|       } catch (error) {
 | |
|         cachedPage = error
 | |
|       }
 | |
|       if (type === 'ejs' && (localOptions.async ?? globalOptions.async)) {
 | |
|         cachedPage.then(html => {
 | |
|           if (useHtmlMinification(globalOptions, requestedPath)) {
 | |
|             html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})
 | |
|           }
 | |
|           that.send(html)
 | |
|         }).catch(err => that.send(err))
 | |
|         return
 | |
|       }
 | |
|       if (useHtmlMinification(globalOptions, requestedPath)) {
 | |
|         cachedPage = globalOptions.useHtmlMinifier.minify(cachedPage, globalOptions.htmlMinifierOptions || {})
 | |
|       }
 | |
|       that.send(cachedPage)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function preProcessDot (templatesDir, options) {
 | |
|     // Process all templates to in memory functions
 | |
|     // https://github.com/olado/doT#security-considerations
 | |
|     const destinationDir = options.destination || join(__dirname, 'out')
 | |
|     if (!existsSync(destinationDir)) {
 | |
|       mkdirSync(destinationDir)
 | |
|     }
 | |
| 
 | |
|     const renderer = engine.process(Object.assign(
 | |
|       {},
 | |
|       options,
 | |
|       {
 | |
|         path: templatesDir,
 | |
|         destination: destinationDir
 | |
|       }
 | |
|     ))
 | |
| 
 | |
|     // .jst files are compiled to .js files so we need to require them
 | |
|     for (const file of readdirSync(destinationDir, { withFileTypes: false })) {
 | |
|       renderer[basename(file, '.js')] = require(resolve(join(destinationDir, file)))
 | |
|     }
 | |
|     if (Object.keys(renderer).length === 0) {
 | |
|       this.log.warn(`WARN: no template found in ${templatesDir}`)
 | |
|     }
 | |
| 
 | |
|     return renderer
 | |
|   }
 | |
| 
 | |
|   function view (page, data, opts) {
 | |
|     if (!page) {
 | |
|       this.send(new Error('Missing page'))
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     data = Object.assign({}, defaultCtx, this.locals, data)
 | |
|     // append view extension
 | |
|     page = getPage(page, type)
 | |
| 
 | |
|     const toHtml = lru.get(page)
 | |
| 
 | |
|     if (toHtml && prod) {
 | |
|       if (!this.getHeader('content-type')) {
 | |
|         this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|       }
 | |
|       this.send(toHtml(data))
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     readFile(join(templatesDir, page), 'utf8', readCallback(this, page, data, opts))
 | |
|   }
 | |
| 
 | |
|   function viewEjs (page, data, opts) {
 | |
|     if (opts && opts.layout) {
 | |
|       try {
 | |
|         layoutIsValid(opts.layout)
 | |
|         const that = this
 | |
|         return withLayout(viewEjs, opts.layout).call(that, page, data)
 | |
|       } catch (error) {
 | |
|         this.send(error)
 | |
|         return
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!page) {
 | |
|       this.send(new Error('Missing page'))
 | |
|       return
 | |
|     }
 | |
|     data = Object.assign({}, defaultCtx, this.locals, data)
 | |
|     // append view extension
 | |
|     page = getPage(page, type)
 | |
|     const requestedPath = getRequestedPath(this)
 | |
|     getTemplate(page, (err, template) => {
 | |
|       if (err) {
 | |
|         this.send(err)
 | |
|         return
 | |
|       }
 | |
|       const toHtml = lru.get(page)
 | |
|       if (toHtml && prod && (typeof (toHtml) === 'function')) {
 | |
|         if (!this.getHeader('content-type')) {
 | |
|           this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|         }
 | |
|         if (opts?.async ?? globalOptions.async) {
 | |
|           toHtml(data).then(html => {
 | |
|             if (useHtmlMinification(globalOptions, requestedPath)) {
 | |
|               this.send(globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {}))
 | |
|               return
 | |
|             }
 | |
|             this.send(html)
 | |
|           }).catch(err => this.send(err))
 | |
|           return
 | |
|         }
 | |
|         if (useHtmlMinification(globalOptions, requestedPath)) {
 | |
|           this.send(globalOptions.useHtmlMinifier.minify(toHtml(data), globalOptions.htmlMinifierOptions || {}))
 | |
|           return
 | |
|         }
 | |
|         this.send(toHtml(data))
 | |
|         return
 | |
|       }
 | |
|       readFile(join(templatesDir, page), 'utf8', readCallback(this, page, data, opts))
 | |
|     }, requestedPath)
 | |
|   }
 | |
| 
 | |
|   function viewArtTemplate (page, data) {
 | |
|     if (!page) {
 | |
|       this.send(new Error('Missing page'))
 | |
|       return
 | |
|     }
 | |
|     data = Object.assign({}, defaultCtx, this.locals, data)
 | |
|     // Append view extension.
 | |
|     page = getPage(page, 'art')
 | |
| 
 | |
|     const defaultSetting = {
 | |
|       debug: process.env.NODE_ENV !== 'production',
 | |
|       root: templatesDir
 | |
|     }
 | |
| 
 | |
|     // merge engine options
 | |
|     const confs = Object.assign({}, defaultSetting, globalOptions)
 | |
| 
 | |
|     function render (filename, data) {
 | |
|       confs.filename = join(templatesDir, filename)
 | |
|       const render = engine.compile(confs)
 | |
|       return render(data)
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       const html = render(page, data)
 | |
|       if (!this.getHeader('content-type')) {
 | |
|         this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|       }
 | |
|       this.send(html)
 | |
|     } catch (error) {
 | |
|       this.send(error)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function viewNunjucks (page, data) {
 | |
|     if (!page) {
 | |
|       this.send(new Error('Missing page'))
 | |
|       return
 | |
|     }
 | |
|     if (typeof globalOptions.onConfigure === 'function') {
 | |
|       globalOptions.onConfigure(nunjucksEnv)
 | |
|     }
 | |
|     data = Object.assign({}, defaultCtx, this.locals, data)
 | |
|     // Append view extension.
 | |
|     page = getPage(page, 'njk')
 | |
|     nunjucksEnv.render(page, data, (err, html) => {
 | |
|       const requestedPath = getRequestedPath(this)
 | |
|       if (err) return this.send(err)
 | |
|       if (useHtmlMinification(globalOptions, requestedPath)) {
 | |
|         html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})
 | |
|       }
 | |
|       if (!this.getHeader('content-type')) {
 | |
|         this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|       }
 | |
|       this.send(html)
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   function viewHandlebars (page, data, opts) {
 | |
|     if (opts && opts.layout) {
 | |
|       try {
 | |
|         layoutIsValid(opts.layout)
 | |
|         const that = this
 | |
|         return withLayout(viewHandlebars, opts.layout).call(that, page, data)
 | |
|       } catch (error) {
 | |
|         this.send(error)
 | |
|         return
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!page) {
 | |
|       this.send(new Error('Missing page'))
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     const options = Object.assign({}, globalOptions)
 | |
|     data = Object.assign({}, defaultCtx, this.locals, data)
 | |
|     // append view extension
 | |
|     page = getPage(page, 'hbs')
 | |
|     const requestedPath = getRequestedPath(this)
 | |
|     getTemplate(page, (err, template) => {
 | |
|       if (err) {
 | |
|         this.send(err)
 | |
|         return
 | |
|       }
 | |
| 
 | |
|       if (prod) {
 | |
|         try {
 | |
|           const html = template(data)
 | |
|           if (!this.getHeader('content-type')) {
 | |
|             this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|           }
 | |
|           this.send(html)
 | |
|         } catch (e) {
 | |
|           this.send(e)
 | |
|         }
 | |
|       } else {
 | |
|         getPartials(type, { partials: options.partials || {}, requestedPath }, (err, partialsObject) => {
 | |
|           if (err) {
 | |
|             this.send(err)
 | |
|             return
 | |
|           }
 | |
| 
 | |
|           try {
 | |
|             Object.keys(partialsObject).forEach((name) => {
 | |
|               engine.registerPartial(name, engine.compile(partialsObject[name]))
 | |
|             })
 | |
| 
 | |
|             const html = template(data)
 | |
| 
 | |
|             if (!this.getHeader('content-type')) {
 | |
|               this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|             }
 | |
|             this.send(html)
 | |
|           } catch (e) {
 | |
|             this.send(e)
 | |
|           }
 | |
|         })
 | |
|       }
 | |
|     }, requestedPath)
 | |
|   }
 | |
| 
 | |
|   function viewMustache (page, data, opts) {
 | |
|     if (!page) {
 | |
|       this.send(new Error('Missing page'))
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     const options = Object.assign({}, opts)
 | |
|     data = Object.assign({}, defaultCtx, this.locals, data)
 | |
|     // append view extension
 | |
|     page = getPage(page, 'mustache')
 | |
|     const requestedPath = getRequestedPath(this)
 | |
|     getTemplate(page, (err, templateString) => {
 | |
|       if (err) {
 | |
|         this.send(err)
 | |
|         return
 | |
|       }
 | |
|       getPartials(page, { partials: options.partials || {}, requestedPath }, (err, partialsObject) => {
 | |
|         if (err) {
 | |
|           this.send(err)
 | |
|           return
 | |
|         }
 | |
|         const html = engine.render(templateString, data, partialsObject)
 | |
| 
 | |
|         if (!this.getHeader('content-type')) {
 | |
|           this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|         }
 | |
|         this.send(html)
 | |
|       })
 | |
|     }, requestedPath)
 | |
|   }
 | |
| 
 | |
|   function viewTwig (page, data, opts) {
 | |
|     if (!page) {
 | |
|       this.send(new Error('Missing page'))
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     data = Object.assign({}, defaultCtx, globalOptions, this.locals, data)
 | |
|     // Append view extension.
 | |
|     page = getPage(page, 'twig')
 | |
|     engine.renderFile(join(templatesDir, page), data, (err, html) => {
 | |
|       const requestedPath = getRequestedPath(this)
 | |
|       if (err) {
 | |
|         return this.send(err)
 | |
|       }
 | |
|       if (useHtmlMinification(globalOptions, requestedPath)) {
 | |
|         html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})
 | |
|       }
 | |
|       if (!this.getHeader('content-type')) {
 | |
|         this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|       }
 | |
|       this.send(html)
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   function viewLiquid (page, data, opts) {
 | |
|     if (!page) {
 | |
|       this.send(new Error('Missing page'))
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     data = Object.assign({}, defaultCtx, this.locals, data)
 | |
|     // Append view extension.
 | |
|     page = getPage(page, 'liquid')
 | |
| 
 | |
|     engine.renderFile(join(templatesDir, page), data, opts)
 | |
|       .then((html) => {
 | |
|         const requestedPath = getRequestedPath(this)
 | |
|         if (useHtmlMinification(globalOptions, requestedPath)) {
 | |
|           html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})
 | |
|         }
 | |
|         if (!this.getHeader('content-type')) {
 | |
|           this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|         }
 | |
|         this.send(html)
 | |
|       })
 | |
|       .catch((err) => {
 | |
|         this.send(err)
 | |
|       })
 | |
|   }
 | |
| 
 | |
|   function viewDot (renderModule) {
 | |
|     return function _viewDot (page, data, opts) {
 | |
|       if (opts && opts.layout) {
 | |
|         try {
 | |
|           layoutIsValid(opts.layout)
 | |
|           const that = this
 | |
|           return withLayout(dotRender, opts.layout).call(that, page, data)
 | |
|         } catch (error) {
 | |
|           this.send(error)
 | |
|           return
 | |
|         }
 | |
|       }
 | |
|       if (!page) {
 | |
|         this.send(new Error('Missing page'))
 | |
|         return
 | |
|       }
 | |
|       data = Object.assign({}, defaultCtx, this.locals, data)
 | |
|       let html = renderModule[page](data)
 | |
|       const requestedPath = getRequestedPath(this)
 | |
|       if (useHtmlMinification(globalOptions, requestedPath)) {
 | |
|         html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})
 | |
|       }
 | |
|       if (!this.getHeader('content-type')) {
 | |
|         this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|       }
 | |
|       this.send(html)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function viewEta (page, data, opts) {
 | |
|     if (opts && opts.layout) {
 | |
|       try {
 | |
|         layoutIsValid(opts.layout)
 | |
|         const that = this
 | |
|         return withLayout(viewEta, opts.layout).call(that, page, data)
 | |
|       } catch (error) {
 | |
|         this.send(error)
 | |
|         return
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!page) {
 | |
|       this.send(new Error('Missing page'))
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     lru.define = lru.set
 | |
|     engine.configure({
 | |
|       templates: globalOptions.templates ? globalOptions.templates : lru
 | |
|     })
 | |
| 
 | |
|     const config = Object.assign({
 | |
|       cache: prod,
 | |
|       views: templatesDir
 | |
|     }, globalOptions)
 | |
| 
 | |
|     const sendContent = html => {
 | |
|       if (
 | |
|         config.useHtmlMinifier &&
 | |
|         typeof config.useHtmlMinifier.minify === 'function' &&
 | |
|         !isPathExcludedMinification(getRequestedPath(this), config.pathsToExcludeHtmlMinifier)
 | |
|       ) {
 | |
|         html = config.useHtmlMinifier.minify(
 | |
|           html,
 | |
|           config.htmlMinifierOptions || {}
 | |
|         )
 | |
|       }
 | |
|       this.header('Content-Type', 'text/html; charset=' + charset)
 | |
|       this.send(html)
 | |
|     }
 | |
| 
 | |
|     data = Object.assign({}, defaultCtx, this.locals, data)
 | |
|     // Append view extension (Eta will append '.eta' by default,
 | |
|     // but this also allows custom extensions)
 | |
|     page = getPage(page, 'eta')
 | |
|     if (opts?.async ?? globalOptions.async) {
 | |
|       engine
 | |
|         .renderFile(page, data, config)
 | |
|         .then(sendContent)
 | |
|         .catch(err => this.send(err))
 | |
|     } else {
 | |
|       engine.renderFile(page, data, config, (err, html) => {
 | |
|         err
 | |
|           ? this.send(err)
 | |
|           : sendContent(html)
 | |
|       })
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (prod && type === 'handlebars' && globalOptions.partials) {
 | |
|     getPartials(type, { partials: globalOptions.partials || {}, requestedPath: getRequestedPath(this) }, (err, partialsObject) => {
 | |
|       if (err) {
 | |
|         next(err)
 | |
|         return
 | |
|       }
 | |
|       Object.keys(partialsObject).forEach((name) => {
 | |
|         engine.registerPartial(name, engine.compile(partialsObject[name]))
 | |
|       })
 | |
|       next()
 | |
|     })
 | |
|   } else {
 | |
|     next()
 | |
|   }
 | |
| 
 | |
|   function withLayout (render, layout) {
 | |
|     if (layout) {
 | |
|       return function (page, data, opts) {
 | |
|         if (opts && opts.layout) throw new Error('A layout can either be set globally or on render, not both.')
 | |
|         const that = this
 | |
|         data = Object.assign({}, defaultCtx, this.locals, data)
 | |
|         render.call({
 | |
|           getHeader: () => { },
 | |
|           header: () => { },
 | |
|           send: (result) => {
 | |
|             if (result instanceof Error) {
 | |
|               throw result
 | |
|             }
 | |
| 
 | |
|             data = Object.assign((data || {}), { body: result })
 | |
|             render.call(that, layout, data, opts)
 | |
|           }
 | |
|         }, page, data, opts)
 | |
|       }
 | |
|     }
 | |
|     return render
 | |
|   }
 | |
| 
 | |
|   function resolveTemplateDir (_opts) {
 | |
|     if (_opts.root) {
 | |
|       return _opts.root
 | |
|     }
 | |
| 
 | |
|     return Array.isArray(_opts.templates)
 | |
|       ? _opts.templates.map((dir) => resolve(dir))
 | |
|       : resolve(_opts.templates || './')
 | |
|   }
 | |
| 
 | |
|   function hasAccessToLayoutFile (fileName, ext) {
 | |
|     try {
 | |
|       accessSync(join(templatesDir, getPage(fileName, ext)))
 | |
| 
 | |
|       return true
 | |
|     } catch (e) {
 | |
|       return false
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| module.exports = fp(fastifyView, {
 | |
|   fastify: '4.x',
 | |
|   name: '@fastify/view'
 | |
| })
 | |
| module.exports.default = fastifyView
 | |
| module.exports.fastifyView = fastifyView
 |