├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .taprc ├── LICENSE ├── README.md ├── benchmark.js ├── benchmark ├── express.js ├── fastify-dot.js ├── fastify-edge.js ├── fastify-ejs-async.js ├── fastify-ejs-global-layout.js ├── fastify-ejs-local-layout.js ├── fastify-ejs-minify.js ├── fastify-eta.js ├── fastify-handlebars.js ├── fastify-liquid.js ├── fastify-mustache.js ├── fastify-nunjucks.js ├── fastify-pug.js ├── fastify-twig.js ├── fastify-viewAsync.js ├── fastify.js └── setup.js ├── eslint.config.js ├── examples ├── example-async.js ├── example-ejs-with-some-options.js └── example.js ├── index.js ├── package.json ├── templates ├── body-data.hbs ├── body.hbs ├── body.mustache ├── body.twig ├── content.ejs ├── content.eta ├── double-quotes-variable.liquid ├── ejs-async.ejs ├── error.hbs ├── footer.art ├── footer.ejs ├── fragment-footer.html ├── fragment-header.ejs ├── fragment-header.eta ├── header.art ├── header.ejs ├── index-bare.html ├── index-for-layout.ejs ├── index-for-layout.eta ├── index-for-layout.hbs ├── index-layout-body.ejs ├── index-layout-content.ejs ├── index-linking-other-pages.ejs ├── index-linking-other-pages.eta ├── index-with-2-partials.mustache ├── index-with-custom-tag.liquid ├── index-with-global.njk ├── index-with-includes-and-attribute-missing.ejs ├── index-with-includes-and-attribute-missing.eta ├── index-with-includes-one-missing.ejs ├── index-with-includes-one-missing.eta ├── index-with-includes-without-ext.html ├── index-with-includes.ejs ├── index-with-includes.eta ├── index-with-no-data.ejs ├── index-with-no-data.eta ├── index-with-partials.hbs ├── index.edge ├── index.ejs ├── index.eta ├── index.hbs ├── index.html ├── index.liquid ├── index.mustache ├── index.njk ├── index.pug ├── index.twig ├── layout-eta.html ├── layout-ts-content-no-data.ejs ├── layout-ts-content-with-data.ejs ├── layout-ts.ejs ├── layout-with-includes.ejs ├── layout.dot ├── layout.ejs ├── layout.eta ├── layout.hbs ├── layout.html ├── layout.pug ├── nunjucks-layout │ └── layout.njk ├── nunjucks-template │ └── index.njk ├── ok.html ├── partial-1.mustache ├── partial-2.mustache ├── sample.pug ├── template.twig ├── testdef.def ├── testdot.dot └── testjst.jst ├── test ├── helper.js ├── snap │ └── test-ejs-with-snapshot.js ├── test-dot.js ├── test-edge.js ├── test-ejs-async.js ├── test-ejs.js ├── test-eta.js ├── test-handlebars.js ├── test-import-engine.mjs ├── test-liquid.js ├── test-mustache.js ├── test-nunjucks.js ├── test-pug.js ├── test-twig.js └── test.js └── types ├── index-global-layout.test-d.ts ├── index.d.ts └── index.test-d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set 2 | * text=auto 3 | 4 | # Require Unix line endings 5 | * text eol=lf -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: write 24 | pull-requests: write 25 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 26 | with: 27 | license-check: true 28 | lint: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | out/ 11 | tap-snapshots/ 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional stylelint cache 61 | .stylelintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Next.js build output 90 | .next 91 | out 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # vuepress v2.x temp and cache directory 107 | .temp 108 | .cache 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | # Vim swap files 136 | *.swp 137 | 138 | # macOS files 139 | .DS_Store 140 | 141 | # Clinic 142 | .clinic 143 | 144 | # lock files 145 | bun.lockb 146 | package-lock.json 147 | pnpm-lock.yaml 148 | yarn.lock 149 | 150 | # editor files 151 | .vscode 152 | .idea 153 | 154 | #tap files 155 | .tap/ 156 | 157 | # temporary/work/output folders 158 | # build/ 159 | out 160 | temp/ 161 | tmp/ 162 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | disable-coverage: true 2 | files: 3 | - test/*.{js,mjs} 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fastify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { readdirSync } = require('node:fs') 4 | const { spawn } = require('node:child_process') 5 | const autocannon = require('autocannon') 6 | const { join } = require('node:path') 7 | 8 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 9 | 10 | const benchmarkDir = join(__dirname, 'benchmark') 11 | 12 | // Keep track of spawned processes and kill if runner killed 13 | const processes = [] 14 | 15 | process.on('SIGINT', () => { 16 | for (const p of processes) { 17 | p.kill('SIGKILL') 18 | } 19 | process.exit() 20 | }) 21 | 22 | ;(async function () { 23 | const benchmarkFiles = readdirSync(benchmarkDir) 24 | // don't include setup file as a benchmark 25 | .filter((fileName) => fileName !== 'setup.js') 26 | // sort by filename length to ensure base benchmarks run first 27 | .sort((a, b) => a.length - b.length) 28 | 29 | for (const benchmarkFile of benchmarkFiles) { 30 | let benchmarkProcess 31 | 32 | try { 33 | // Spawn benchmark process 34 | benchmarkProcess = spawn('node', [benchmarkFile], { 35 | detached: true, 36 | cwd: benchmarkDir 37 | }) 38 | 39 | processes.push(benchmarkProcess) 40 | 41 | // wait for `server listening` from benchmark 42 | await Promise.race([ 43 | new Promise((resolve) => { 44 | const stdOutCb = (d) => { 45 | if (d.toString().includes('server listening')) { 46 | benchmarkProcess.stdout.removeListener('data', stdOutCb) 47 | resolve() 48 | } 49 | } 50 | benchmarkProcess.stdout.on('data', stdOutCb) 51 | }), 52 | delay(5000).then(() => Promise.reject(new Error('timed out waiting for server listening'))) 53 | ]) 54 | 55 | // fire single initial request as warmup 56 | await fetch('http://localhost:3000/') 57 | 58 | // run autocannon 59 | const result = await autocannon({ 60 | url: 'http://localhost:3000/', 61 | connections: 100, 62 | duration: 5, 63 | pipelining: 10 64 | }) 65 | if (result.non2xx > 0) { 66 | throw Object.assign(new Error('Some requests did not return 200'), { 67 | statusCodeStats: result.statusCodeStats 68 | }) 69 | } 70 | console.log(`${benchmarkFile}: ${result.requests.average} req/s`) 71 | } catch (err) { 72 | console.error(`${benchmarkFile}:`, err) 73 | } finally { 74 | if (benchmarkProcess) { 75 | benchmarkProcess.kill('SIGKILL') 76 | processes.pop() 77 | } 78 | } 79 | } 80 | })() 81 | -------------------------------------------------------------------------------- /benchmark/express.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const express = require('express') 6 | const app = express() 7 | 8 | app.set('view engine', 'ejs') 9 | 10 | app.get('/', (_req, res) => { 11 | res.render('../../templates/index.ejs', { text: 'text' }) 12 | }) 13 | 14 | app.listen(3000, err => { 15 | if (err) throw err 16 | console.log('server listening on 3000') 17 | }) 18 | -------------------------------------------------------------------------------- /benchmark/fastify-dot.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { dot: require('dot') }, 5 | route: (_req, reply) => { reply.view('testdot', { text: 'text' }) } 6 | }) 7 | -------------------------------------------------------------------------------- /benchmark/fastify-edge.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Edge } = require('edge.js') 4 | const { join } = require('node:path') 5 | const edge = new Edge() 6 | edge.mount(join(__dirname, '..', 'templates')) 7 | 8 | require('./setup.js')({ 9 | engine: { edge }, 10 | route: (_req, reply) => { 11 | reply.view('index.edge', { text: 'text' }) 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /benchmark/fastify-ejs-async.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { ejs: require('ejs') }, 5 | route: (_req, reply) => { reply.view('ejs-async.ejs', { text: 'text' }) }, 6 | options: { async: true } 7 | }) 8 | -------------------------------------------------------------------------------- /benchmark/fastify-ejs-global-layout.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { ejs: require('ejs') }, 5 | route: (_req, reply) => { reply.view('index-for-layout.ejs', { text: 'text' }) }, 6 | pluginOptions: { 7 | layout: 'layout.html' 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /benchmark/fastify-ejs-local-layout.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { ejs: require('ejs') }, 5 | route: (_req, reply) => { reply.view('index-for-layout.ejs', { text: 'text' }, { layout: 'layout.html' }) } 6 | }) 7 | -------------------------------------------------------------------------------- /benchmark/fastify-ejs-minify.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { ejs: require('ejs') }, 5 | route: (_req, reply) => { reply.view('index.ejs', { text: 'text' }) }, 6 | options: { 7 | useHtmlMinifier: require('html-minifier-terser'), 8 | htmlMinifierOptions: { 9 | removeComments: true, 10 | removeCommentsFromCDATA: true, 11 | collapseWhitespace: true, 12 | collapseBooleanAttributes: true, 13 | removeAttributeQuotes: true, 14 | removeEmptyAttributes: true 15 | } 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /benchmark/fastify-eta.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Eta } = require('eta') 4 | const eta = new Eta() 5 | 6 | require('./setup.js')({ 7 | engine: { eta }, 8 | route: (_req, reply) => { reply.view('index.eta', { text: 'text' }) } 9 | }) 10 | -------------------------------------------------------------------------------- /benchmark/fastify-handlebars.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { handlebars: require('handlebars') }, 5 | route: (_req, reply) => { reply.view('index.html', { text: 'text' }) } 6 | }) 7 | -------------------------------------------------------------------------------- /benchmark/fastify-liquid.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Liquid } = require('liquidjs') 4 | const liquid = new Liquid() 5 | 6 | require('./setup.js')({ 7 | engine: { liquid }, 8 | route: (_req, reply) => { reply.view('index.liquid', { text: 'text' }) } 9 | }) 10 | -------------------------------------------------------------------------------- /benchmark/fastify-mustache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { mustache: require('mustache') }, 5 | route: (_req, reply) => { reply.view('index.html', { text: 'text' }) } 6 | }) 7 | -------------------------------------------------------------------------------- /benchmark/fastify-nunjucks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { nunjucks: require('nunjucks') }, 5 | route: (_req, reply) => { reply.view('index.njk', { text: 'text' }) } 6 | }) 7 | -------------------------------------------------------------------------------- /benchmark/fastify-pug.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { pug: require('pug') }, 5 | route: (_req, reply) => { reply.view('index.pug', { text: 'text' }) } 6 | }) 7 | -------------------------------------------------------------------------------- /benchmark/fastify-twig.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { twig: require('twig') }, 5 | route: (_req, reply) => { reply.view('index.twig', { title: 'fastify', text: 'text' }) } 6 | }) 7 | -------------------------------------------------------------------------------- /benchmark/fastify-viewAsync.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { ejs: require('ejs') }, 5 | route: (_req, reply) => { return reply.viewAsync('index.ejs', { text: 'text' }) } 6 | }) 7 | -------------------------------------------------------------------------------- /benchmark/fastify.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./setup.js')({ 4 | engine: { ejs: require('ejs') }, 5 | route: (_req, reply) => { reply.view('index.ejs', { text: 'text' }) } 6 | }) 7 | -------------------------------------------------------------------------------- /benchmark/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const fastify = require('fastify')() 6 | const path = require('node:path') 7 | 8 | module.exports = function ({ engine, route, options = {}, pluginOptions }) { 9 | fastify.register(require('../index'), { 10 | engine, 11 | options, 12 | root: path.join(__dirname, '../templates'), 13 | ...pluginOptions 14 | }) 15 | 16 | fastify.get('/', route) 17 | 18 | fastify.listen({ port: 3000 }, err => { 19 | if (err) throw err 20 | console.log(`server listening on ${fastify.server.address().port}`) 21 | }) 22 | 23 | return fastify 24 | } 25 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /examples/example-async.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { promisify } = require('node:util') 4 | const sleep = promisify(setTimeout) 5 | const templates = 'templates' 6 | 7 | const fastify = require('fastify')({ 8 | logger: true 9 | }) 10 | 11 | fastify.register(require('..'), { 12 | engine: { 13 | nunjucks: require('nunjucks') 14 | }, 15 | templates 16 | }) 17 | 18 | async function something () { 19 | await sleep(1000) 20 | return new Date() 21 | } 22 | 23 | fastify.get('/', async (_req, reply) => { 24 | const t = await something() 25 | return reply.view('/index.njk', { text: t }) 26 | }) 27 | 28 | fastify.listen({ port: 3000 }, err => { 29 | if (err) throw err 30 | console.log(`server listening on ${fastify.server.address().port}`) 31 | }) 32 | -------------------------------------------------------------------------------- /examples/example-ejs-with-some-options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastify = require('fastify')() 4 | const resolve = require('node:path').resolve 5 | const templatesFolder = 'templates' 6 | const data = { text: 'Hello from EJS Templates' } 7 | 8 | fastify.register(require('..'), { 9 | engine: { 10 | ejs: require('ejs') 11 | }, 12 | defaultContext: { 13 | header: 'header value defined as default context', 14 | footer: 'footer value defined as default context' 15 | }, 16 | includeViewExtension: true, 17 | layout: 'layout', 18 | templates: templatesFolder, 19 | options: { 20 | filename: resolve(templatesFolder) 21 | }, 22 | charset: 'utf-8' // sample usage, but specifying the same value already used as default 23 | }) 24 | 25 | fastify.get('/', (_req, reply) => { 26 | // reply.type('text/html; charset=utf-8').view('index-linking-other-pages', data) // sample for specifying with type 27 | reply.view('index-linking-other-pages', data) 28 | }) 29 | 30 | fastify.get('/include-test', (_req, reply) => { 31 | reply.view('index-with-includes', data) 32 | }) 33 | 34 | fastify.get('/include-one-include-missing-test', (_req, reply) => { 35 | reply.view('index-with-includes-one-missing', data) 36 | }) 37 | 38 | fastify.get('/include-one-attribute-missing-test', (_req, reply) => { 39 | reply.view('index-with-includes-and-attribute-missing', data) 40 | }) 41 | 42 | fastify.listen({ port: 3000 }, err => { 43 | if (err) throw err 44 | console.log(`server listening on ${fastify.server.address().port}`) 45 | }) 46 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastify = require('fastify')() 4 | 5 | fastify.register(require('..'), { 6 | engine: { 7 | ejs: require('ejs') 8 | } 9 | }) 10 | 11 | fastify.get('/', (_req, reply) => { 12 | reply.view('/templates/index.ejs', { text: 'text' }) 13 | }) 14 | 15 | fastify.listen({ port: 3000 }, err => { 16 | if (err) throw err 17 | console.log(`server listening on ${fastify.server.address().port}`) 18 | }) 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { readFile } = require('node:fs/promises') 3 | const fp = require('fastify-plugin') 4 | const { accessSync, existsSync, mkdirSync, readdirSync } = require('node:fs') 5 | const { basename, dirname, extname, join, resolve } = require('node:path') 6 | const { LruMap } = require('toad-cache') 7 | const supportedEngines = ['ejs', 'nunjucks', 'pug', 'handlebars', 'mustache', 'twig', 'liquid', 'dot', 'eta', 'edge'] 8 | 9 | const viewCache = Symbol('@fastify/view/cache') 10 | 11 | const fastifyViewCache = fp( 12 | async function cachePlugin (fastify, opts) { 13 | const lru = new LruMap(opts.maxCache || 100) 14 | fastify.decorate(viewCache, lru) 15 | }, 16 | { 17 | fastify: '5.x', 18 | name: '@fastify/view/cache' 19 | } 20 | ) 21 | 22 | async function fastifyView (fastify, opts) { 23 | if (fastify[viewCache] === undefined) { 24 | await fastify.register(fastifyViewCache, opts) 25 | } 26 | if (!opts.engine) { 27 | throw new Error('Missing engine') 28 | } 29 | const type = Object.keys(opts.engine)[0] 30 | if (supportedEngines.indexOf(type) === -1) { 31 | throw new Error(`'${type}' not yet supported, PR? :)`) 32 | } 33 | const charset = opts.charset || 'utf-8' 34 | const propertyName = opts.propertyName || 'view' 35 | const asyncPropertyName = opts.asyncPropertyName || `${propertyName}Async` 36 | const engine = await opts.engine[type] 37 | const globalOptions = opts.options || {} 38 | const templatesDir = resolveTemplateDir(opts) 39 | const includeViewExtension = opts.includeViewExtension || false 40 | const viewExt = opts.viewExt || '' 41 | const prod = typeof opts.production === 'boolean' ? opts.production : process.env.NODE_ENV === 'production' 42 | const defaultCtx = opts.defaultContext 43 | const globalLayoutFileName = opts.layout 44 | 45 | /** 46 | * @type {Map} 47 | */ 48 | const readFileMap = new Map() 49 | 50 | function readFileSemaphore (filePath) { 51 | if (readFileMap.has(filePath) === false) { 52 | const promise = readFile(filePath, 'utf-8') 53 | readFileMap.set(filePath, promise) 54 | return promise.finally(() => readFileMap.delete(filePath)) 55 | } 56 | return readFileMap.get(filePath) 57 | } 58 | 59 | function templatesDirIsValid (_templatesDir) { 60 | if (Array.isArray(_templatesDir) && type !== 'nunjucks') { 61 | throw new Error('Only Nunjucks supports the "templates" option as an array') 62 | } 63 | } 64 | 65 | function layoutIsValid (_layoutFileName) { 66 | if (type !== 'dot' && type !== 'handlebars' && type !== 'ejs' && type !== 'eta') { 67 | throw new Error('Only Dot, Handlebars, EJS and Eta support the "layout" option') 68 | } 69 | 70 | if (!hasAccessToLayoutFile(_layoutFileName, getDefaultExtension(type))) { 71 | throw new Error(`unable to access template "${_layoutFileName}"`) 72 | } 73 | } 74 | 75 | function setupNunjucksEnv (_engine) { 76 | if (type === 'nunjucks') { 77 | const env = _engine.configure(templatesDir, globalOptions) 78 | if (typeof globalOptions.onConfigure === 'function') { 79 | globalOptions.onConfigure(env) 80 | } 81 | return env 82 | } 83 | return null 84 | } 85 | 86 | templatesDirIsValid(templatesDir) 87 | 88 | if (globalLayoutFileName) { 89 | layoutIsValid(globalLayoutFileName) 90 | } 91 | 92 | const dotRender = type === 'dot' ? viewDot.call(fastify, preProcessDot.call(fastify, templatesDir, globalOptions)) : null 93 | const nunjucksEnv = setupNunjucksEnv(engine) 94 | 95 | const renders = { 96 | ejs: withLayout(viewEjs, globalLayoutFileName), 97 | handlebars: withLayout(viewHandlebars, globalLayoutFileName), 98 | mustache: viewMustache, 99 | nunjucks: viewNunjucks, 100 | twig: viewTwig, 101 | liquid: viewLiquid, 102 | dot: withLayout(dotRender, globalLayoutFileName), 103 | eta: withLayout(viewEta, globalLayoutFileName), 104 | edge: viewEdge, 105 | _default: view 106 | } 107 | 108 | const renderer = renders[type] ? renders[type] : renders._default 109 | 110 | async function asyncRender (page) { 111 | if (!page) { 112 | throw new Error('Missing page') 113 | } 114 | 115 | let result = await renderer.apply(this, arguments) 116 | 117 | if (minify && !isPathExcludedMinification(this)) { 118 | result = await minify(result, globalOptions.htmlMinifierOptions) 119 | } 120 | 121 | if (this.getHeader && !this.getHeader('Content-Type')) { 122 | this.header('Content-Type', 'text/html; charset=' + charset) 123 | } 124 | 125 | return result 126 | } 127 | 128 | function viewDecorator () { 129 | const args = Array.from(arguments) 130 | 131 | let done 132 | if (typeof args[args.length - 1] === 'function') { 133 | done = args.pop() 134 | } 135 | 136 | const promise = asyncRender.apply({}, args) 137 | 138 | if (typeof done === 'function') { 139 | promise.then(done.bind(null, null), done) 140 | return 141 | } 142 | 143 | return promise 144 | } 145 | 146 | viewDecorator.clearCache = function () { 147 | fastify[viewCache].clear() 148 | } 149 | 150 | fastify.decorate(propertyName, viewDecorator) 151 | 152 | fastify.decorateReply(propertyName, async function (page, data, opts) { 153 | try { 154 | const html = await asyncRender.call(this, page, data, opts) 155 | this.send(html) 156 | } catch (err) { 157 | this.send(err) 158 | } 159 | 160 | return this 161 | }) 162 | 163 | fastify.decorateReply(asyncPropertyName, asyncRender) 164 | 165 | if (!fastify.hasReplyDecorator('locals')) { 166 | fastify.decorateReply('locals', null) 167 | 168 | fastify.addHook('onRequest', (_req, reply, done) => { 169 | reply.locals = {} 170 | done() 171 | }) 172 | } 173 | 174 | function getPage (page, extension) { 175 | const pageLRU = `getPage-${page}-${extension}` 176 | let result = fastify[viewCache].get(pageLRU) 177 | 178 | if (typeof result === 'string') { 179 | return result 180 | } 181 | 182 | const filename = basename(page, extname(page)) 183 | result = join(dirname(page), filename + getExtension(page, extension)) 184 | 185 | fastify[viewCache].set(pageLRU, result) 186 | 187 | return result 188 | } 189 | 190 | function getDefaultExtension (type) { 191 | const mappedExtensions = { 192 | handlebars: 'hbs', 193 | nunjucks: 'njk' 194 | } 195 | 196 | return viewExt || (mappedExtensions[type] || type) 197 | } 198 | 199 | function getExtension (page, extension) { 200 | let filextension = extname(page) 201 | if (!filextension) { 202 | filextension = '.' + getDefaultExtension(type) 203 | } 204 | 205 | return viewExt ? `.${viewExt}` : (includeViewExtension ? `.${extension}` : filextension) 206 | } 207 | 208 | const minify = typeof globalOptions.useHtmlMinifier?.minify === 'function' 209 | ? globalOptions.useHtmlMinifier.minify 210 | : null 211 | 212 | const minifyExcludedPaths = Array.isArray(globalOptions.pathsToExcludeHtmlMinifier) 213 | ? new Set(globalOptions.pathsToExcludeHtmlMinifier) 214 | : null 215 | 216 | function getRequestedPath (fastify) { 217 | return fastify?.request?.routeOptions.url ?? null 218 | } 219 | function isPathExcludedMinification (that) { 220 | return minifyExcludedPaths?.has(getRequestedPath(that)) 221 | } 222 | function onTemplatesLoaded (file, data) { 223 | if (type === 'handlebars') { 224 | data = engine.compile(data, globalOptions.compileOptions) 225 | } 226 | fastify[viewCache].set(file, data) 227 | return data 228 | } 229 | 230 | // Gets template as string (or precompiled for Handlebars) 231 | // from LRU cache or filesystem. 232 | const getTemplate = async function (file) { 233 | if (typeof file === 'function') { 234 | return file 235 | } 236 | let isRaw = false 237 | if (typeof file === 'object' && file.raw) { 238 | isRaw = true 239 | file = file.raw 240 | } 241 | const data = fastify[viewCache].get(file) 242 | if (data && prod) { 243 | return data 244 | } 245 | if (isRaw) { 246 | return onTemplatesLoaded(file, file) 247 | } 248 | const fileData = await readFileSemaphore(join(templatesDir, file)) 249 | return onTemplatesLoaded(file, fileData) 250 | } 251 | 252 | // Gets partials as collection of strings from LRU cache or filesystem. 253 | const getPartials = async function (page, { partials, requestedPath }) { 254 | const cacheKey = getPartialsCacheKey(page, partials, requestedPath) 255 | const partialsObj = fastify[viewCache].get(cacheKey) 256 | if (partialsObj && prod) { 257 | return partialsObj 258 | } else { 259 | const partialKeys = Object.keys(partials) 260 | if (partialKeys.length === 0) { 261 | return {} 262 | } 263 | const partialsHtml = {} 264 | await Promise.all(partialKeys.map(async (key) => { 265 | partialsHtml[key] = await readFileSemaphore(join(templatesDir, partials[key])) 266 | })) 267 | fastify[viewCache].set(cacheKey, partialsHtml) 268 | return partialsHtml 269 | } 270 | } 271 | 272 | function getPartialsCacheKey (page, partials, requestedPath) { 273 | let cacheKey = page 274 | 275 | for (const key of Object.keys(partials)) { 276 | cacheKey += `|${key}:${partials[key]}` 277 | } 278 | 279 | cacheKey += `|${requestedPath}-Partials` 280 | 281 | return cacheKey 282 | } 283 | 284 | function readCallbackParser (page, html, localOptions) { 285 | if ((type === 'ejs') && viewExt && !globalOptions.includer) { 286 | globalOptions.includer = (originalPath, parsedPath) => ({ 287 | filename: parsedPath || join(templatesDir, originalPath + '.' + viewExt) 288 | }) 289 | } 290 | if (localOptions) { 291 | for (const key in globalOptions) { 292 | if (!Object.hasOwn(localOptions, key)) localOptions[key] = globalOptions[key] 293 | } 294 | } else localOptions = globalOptions 295 | 296 | const compiledPage = engine.compile(html, localOptions) 297 | 298 | fastify[viewCache].set(page, compiledPage) 299 | return compiledPage 300 | } 301 | 302 | function readCallback (page, _data, localOptions, html) { 303 | globalOptions.filename = join(templatesDir, page) 304 | return readCallbackParser(page, html, localOptions) 305 | } 306 | 307 | function preProcessDot (templatesDir, options) { 308 | // Process all templates to in memory functions 309 | // https://github.com/olado/doT#security-considerations 310 | const destinationDir = options.destination || join(__dirname, 'out') 311 | if (!existsSync(destinationDir)) { 312 | mkdirSync(destinationDir) 313 | } 314 | 315 | const renderer = engine.process(Object.assign( 316 | {}, 317 | options, 318 | { 319 | path: templatesDir, 320 | destination: destinationDir 321 | } 322 | )) 323 | 324 | // .jst files are compiled to .js files so we need to require them 325 | for (const file of readdirSync(destinationDir, { withFileTypes: false })) { 326 | renderer[basename(file, '.js')] = require(resolve(join(destinationDir, file))) 327 | } 328 | if (Object.keys(renderer).length === 0) { 329 | this.log.warn(`WARN: no template found in ${templatesDir}`) 330 | } 331 | 332 | return renderer 333 | } 334 | 335 | async function view (page, data, opts) { 336 | data = Object.assign({}, defaultCtx, this.locals, data) 337 | if (typeof page === 'function') { 338 | return page(data) 339 | } 340 | let isRaw = false 341 | if (typeof page === 'object' && page.raw) { 342 | isRaw = true 343 | page = page.raw.toString() 344 | } else { 345 | // append view extension 346 | page = getPage(page, type) 347 | } 348 | const toHtml = fastify[viewCache].get(page) 349 | 350 | if (toHtml && prod) { 351 | return toHtml(data) 352 | } else if (isRaw) { 353 | const compiledPage = readCallbackParser(page, page, opts) 354 | return compiledPage(data) 355 | } 356 | 357 | const file = await readFileSemaphore(join(templatesDir, page)) 358 | const render = readCallback(page, data, opts, file) 359 | return render(data) 360 | } 361 | 362 | async function viewEjs (page, data, opts) { 363 | if (opts?.layout) { 364 | layoutIsValid(opts.layout) 365 | return withLayout(viewEjs, opts.layout).call(this, page, data) 366 | } 367 | data = Object.assign({}, defaultCtx, this.locals, data) 368 | if (typeof page === 'function') { 369 | return page(data) 370 | } 371 | let isRaw = false 372 | if (typeof page === 'object' && page.raw) { 373 | isRaw = true 374 | page = page.raw.toString() 375 | } else { 376 | // append view extension 377 | page = getPage(page, type) 378 | } 379 | const toHtml = fastify[viewCache].get(page) 380 | 381 | if (toHtml && prod) { 382 | return toHtml(data) 383 | } else if (isRaw) { 384 | const compiledPage = readCallbackParser(page, page, opts) 385 | return compiledPage(data) 386 | } 387 | 388 | const file = await readFileSemaphore(join(templatesDir, page)) 389 | const render = readCallback(page, data, opts, file) 390 | return render(data) 391 | } 392 | 393 | async function viewNunjucks (page, data) { 394 | data = Object.assign({}, defaultCtx, this.locals, data) 395 | let render 396 | if (typeof page === 'string') { 397 | // Append view extension. 398 | page = getPage(page, 'njk') 399 | render = nunjucksEnv.render.bind(nunjucksEnv, page) 400 | } else if (typeof page === 'object' && typeof page.render === 'function') { 401 | render = page.render.bind(page) 402 | } else if (typeof page === 'object' && page.raw) { 403 | render = nunjucksEnv.renderString.bind(nunjucksEnv, page.raw.toString()) 404 | } else { 405 | throw new Error('Unknown template type') 406 | } 407 | return new Promise((resolve, reject) => { 408 | render(data, (err, html) => { 409 | if (err) { 410 | reject(err) 411 | return 412 | } 413 | 414 | resolve(html) 415 | }) 416 | }) 417 | } 418 | 419 | async function viewHandlebars (page, data, opts) { 420 | if (opts?.layout) { 421 | layoutIsValid(opts.layout) 422 | return withLayout(viewHandlebars, opts.layout).call(this, page, data) 423 | } 424 | 425 | let options 426 | 427 | if (globalOptions.useDataVariables) { 428 | options = { 429 | data: defaultCtx ? Object.assign({}, defaultCtx, this.locals) : this.locals 430 | } 431 | } else { 432 | data = Object.assign({}, defaultCtx, this.locals, data) 433 | } 434 | 435 | if (typeof page === 'string') { 436 | // append view extension 437 | page = getPage(page, 'hbs') 438 | } 439 | const requestedPath = getRequestedPath(this) 440 | const template = await getTemplate(page) 441 | 442 | if (prod) { 443 | return template(data, options) 444 | } else { 445 | const partialsObject = await getPartials(type, { partials: globalOptions.partials || {}, requestedPath }) 446 | 447 | Object.keys(partialsObject).forEach((name) => { 448 | engine.registerPartial(name, engine.compile(partialsObject[name], globalOptions.compileOptions)) 449 | }) 450 | 451 | return template(data, options) 452 | } 453 | } 454 | 455 | async function viewMustache (page, data, opts) { 456 | const options = Object.assign({}, opts) 457 | data = Object.assign({}, defaultCtx, this.locals, data) 458 | if (typeof page === 'string') { 459 | // append view extension 460 | page = getPage(page, 'mustache') 461 | } 462 | const partials = Object.assign({}, globalOptions.partials || {}, options.partials || {}) 463 | const requestedPath = getRequestedPath(this) 464 | const templateString = await getTemplate(page) 465 | const partialsObject = await getPartials(page, { partials, requestedPath }) 466 | 467 | let html 468 | if (typeof templateString === 'function') { 469 | html = templateString(data, partialsObject) 470 | } else { 471 | html = engine.render(templateString, data, partialsObject) 472 | } 473 | 474 | return html 475 | } 476 | 477 | async function viewTwig (page, data) { 478 | data = Object.assign({}, defaultCtx, globalOptions, this.locals, data) 479 | let render 480 | if (typeof page === 'string') { 481 | // Append view extension. 482 | page = getPage(page, 'twig') 483 | render = engine.renderFile.bind(engine, join(templatesDir, page)) 484 | } else if (typeof page === 'object' && typeof page.render === 'function') { 485 | render = (data, cb) => cb(null, page.render(data)) 486 | } else if (typeof page === 'object' && page.raw) { 487 | render = (data, cb) => cb(null, engine.twig({ data: page.raw.toString() }).render(data)) 488 | } else { 489 | throw new Error('Unknown template type') 490 | } 491 | return new Promise((resolve, reject) => { 492 | render(data, (err, html) => { 493 | if (err) { 494 | reject(err) 495 | return 496 | } 497 | 498 | resolve(html) 499 | }) 500 | }) 501 | } 502 | 503 | async function viewLiquid (page, data, opts) { 504 | data = Object.assign({}, defaultCtx, this.locals, data) 505 | let render 506 | if (typeof page === 'string') { 507 | // Append view extension. 508 | page = getPage(page, 'liquid') 509 | render = engine.renderFile.bind(engine, join(templatesDir, page)) 510 | } else if (typeof page === 'function') { 511 | render = page 512 | } else if (typeof page === 'object' && page.raw) { 513 | const templates = engine.parse(page.raw) 514 | render = engine.render.bind(engine, templates) 515 | } 516 | 517 | return render(data, opts) 518 | } 519 | 520 | function viewDot (renderModule) { 521 | return async function _viewDot (page, data, opts) { 522 | if (opts?.layout) { 523 | layoutIsValid(opts.layout) 524 | return withLayout(dotRender, opts.layout).call(this, page, data) 525 | } 526 | data = Object.assign({}, defaultCtx, this.locals, data) 527 | let render 528 | if (typeof page === 'function') { 529 | render = page 530 | } else if (typeof page === 'object' && page.raw) { 531 | render = engine.template(page.raw.toString(), { ...engine.templateSettings, ...globalOptions, ...page.settings }, page.imports) 532 | } else { 533 | render = renderModule[page] 534 | } 535 | return render(data) 536 | } 537 | } 538 | 539 | async function viewEta (page, data, opts) { 540 | if (opts?.layout) { 541 | layoutIsValid(opts.layout) 542 | return withLayout(viewEta, opts.layout).call(this, page, data) 543 | } 544 | 545 | if (globalOptions.templatesSync) { 546 | engine.templatesSync = globalOptions.templatesSync 547 | } 548 | 549 | engine.configure({ 550 | views: templatesDir, 551 | cache: prod || globalOptions.templatesSync 552 | }) 553 | 554 | const config = Object.assign({ 555 | cache: prod, 556 | views: templatesDir 557 | }, globalOptions) 558 | 559 | data = Object.assign({}, defaultCtx, this.locals, data) 560 | 561 | if (typeof page === 'function') { 562 | const ret = await page.call(engine, data, config) 563 | return ret 564 | } 565 | 566 | let render, renderAsync 567 | if (typeof page === 'object' && page.raw) { 568 | page = page.raw.toString() 569 | render = engine.renderString.bind(engine) 570 | renderAsync = engine.renderStringAsync.bind(engine) 571 | } else { 572 | // Append view extension (Eta will append '.eta' by default, 573 | // but this also allows custom extensions) 574 | page = getPage(page, 'eta') 575 | render = engine.render.bind(engine) 576 | renderAsync = engine.renderAsync.bind(engine) 577 | } 578 | 579 | /* c8 ignore next */ 580 | if (opts?.async ?? globalOptions.async) { 581 | return renderAsync(page, data, config) 582 | } else { 583 | return render(page, data, config) 584 | } 585 | } 586 | 587 | if (prod && type === 'handlebars' && globalOptions.partials) { 588 | const partialsObject = await getPartials(type, { partials: globalOptions.partials, requestedPath: getRequestedPath(this) }) 589 | Object.keys(partialsObject).forEach((name) => { 590 | engine.registerPartial(name, engine.compile(partialsObject[name], globalOptions.compileOptions)) 591 | }) 592 | } 593 | 594 | async function viewEdge (page, data, opts) { 595 | data = Object.assign({}, defaultCtx, this.locals, data) 596 | 597 | switch (typeof page) { 598 | case 'string': 599 | return engine.render(getPage(page, 'edge'), data) 600 | case 'function': 601 | return page(data) 602 | case 'object': 603 | return engine.renderRaw(page, data) 604 | default: 605 | throw new Error('Unknown page type') 606 | } 607 | } 608 | 609 | function withLayout (render, layout) { 610 | if (layout) { 611 | return async function (page, data, opts) { 612 | if (opts?.layout) throw new Error('A layout can either be set globally or on render, not both.') 613 | data = Object.assign({}, defaultCtx, this.locals, data) 614 | const result = await render.call(this, page, data, opts) 615 | data = Object.assign(data, { body: result }) 616 | return render.call(this, layout, data, opts) 617 | } 618 | } 619 | return render 620 | } 621 | 622 | function resolveTemplateDir (_opts) { 623 | if (_opts.root) { 624 | return _opts.root 625 | } 626 | 627 | return Array.isArray(_opts.templates) 628 | ? _opts.templates.map((dir) => resolve(dir)) 629 | : resolve(_opts.templates || './') 630 | } 631 | 632 | function hasAccessToLayoutFile (fileName, ext) { 633 | const layoutKey = `layout-${fileName}-${ext}` 634 | let result = fastify[viewCache].get(layoutKey) 635 | 636 | if (typeof result === 'boolean') { 637 | return result 638 | } 639 | 640 | try { 641 | accessSync(join(templatesDir, getPage(fileName, ext))) 642 | result = true 643 | } catch { 644 | result = false 645 | } 646 | 647 | fastify[viewCache].set(layoutKey, result) 648 | 649 | return result 650 | } 651 | } 652 | 653 | module.exports = fp(fastifyView, { 654 | fastify: '5.x', 655 | name: '@fastify/view' 656 | }) 657 | module.exports.default = fastifyView 658 | module.exports.fastifyView = fastifyView 659 | module.exports.fastifyViewCache = viewCache 660 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/view", 3 | "version": "11.1.0", 4 | "description": "Template plugin for Fastify", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "benchmark": "node benchmark.js", 10 | "example": "node examples/example.js", 11 | "example-with-options": "node examples/example-ejs-with-some-options.js", 12 | "example-typescript": "npx ts-node types/index.test-d.ts", 13 | "lint": "eslint", 14 | "lint:fix": "eslint --fix", 15 | "test-with-snapshot": "cross-env TAP_SNAPSHOT=1 tap test/snap/*", 16 | "test": "npm run test:unit && npm run test-with-snapshot && npm run test:typescript", 17 | "test:unit": "c8 borp test/*.js", 18 | "test:typescript": "tsd" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/fastify/point-of-view.git" 23 | }, 24 | "keywords": [ 25 | "fastify", 26 | "template", 27 | "view", 28 | "speed", 29 | "ejs", 30 | "nunjucks", 31 | "pug", 32 | "handlebars", 33 | "mustache", 34 | "twig", 35 | "eta" 36 | ], 37 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)", 38 | "contributors": [ 39 | { 40 | "name": "Matteo Collina", 41 | "email": "hello@matteocollina.com" 42 | }, 43 | { 44 | "name": "Manuel Spigolon", 45 | "email": "behemoth89@gmail.com" 46 | }, 47 | { 48 | "name": "Aras Abbasi", 49 | "email": "aras.abbasi@gmail.com" 50 | }, 51 | { 52 | "name": "Frazer Smith", 53 | "email": "frazer.dev@icloud.com", 54 | "url": "https://github.com/fdawgs" 55 | } 56 | ], 57 | "license": "MIT", 58 | "bugs": { 59 | "url": "https://github.com/fastify/point-of-view/issues" 60 | }, 61 | "homepage": "https://github.com/fastify/point-of-view#readme", 62 | "funding": [ 63 | { 64 | "type": "github", 65 | "url": "https://github.com/sponsors/fastify" 66 | }, 67 | { 68 | "type": "opencollective", 69 | "url": "https://opencollective.com/fastify" 70 | } 71 | ], 72 | "dependencies": { 73 | "fastify-plugin": "^5.0.0", 74 | "toad-cache": "^3.7.0" 75 | }, 76 | "devDependencies": { 77 | "@fastify/pre-commit": "^2.1.0", 78 | "@types/node": "^22.0.0", 79 | "autocannon": "^8.0.0", 80 | "borp": "^0.20.0", 81 | "c8": "^10.1.3", 82 | "cross-env": "^7.0.3", 83 | "dot": "^1.1.3", 84 | "edge.js": "^6.2.1", 85 | "ejs": "^3.1.10", 86 | "eslint": "^9.17.0", 87 | "eta": "^3.4.0", 88 | "express": "^5.1.0", 89 | "fastify": "^5.0.0", 90 | "handlebars": "^4.7.8", 91 | "html-minifier-terser": "^7.2.0", 92 | "liquidjs": "^10.11.0", 93 | "mustache": "^4.2.0", 94 | "neostandard": "^0.12.0", 95 | "nunjucks": "^3.2.4", 96 | "pino": "^9.0.0", 97 | "pug": "^3.0.2", 98 | "split2": "^4.2.0", 99 | "tap": "^21.0.0", 100 | "tsd": "^0.32.0", 101 | "twig": "^1.17.1" 102 | }, 103 | "pre-commit": [ 104 | "lint", 105 | "test" 106 | ], 107 | "publishConfig": { 108 | "access": "public" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /templates/body-data.hbs: -------------------------------------------------------------------------------- 1 |

{{ @text }}

2 | -------------------------------------------------------------------------------- /templates/body.hbs: -------------------------------------------------------------------------------- 1 |

{{ text }}

2 | -------------------------------------------------------------------------------- /templates/body.mustache: -------------------------------------------------------------------------------- 1 |

{{ text }}

-------------------------------------------------------------------------------- /templates/body.twig: -------------------------------------------------------------------------------- 1 | Twig.js! 2 | -------------------------------------------------------------------------------- /templates/content.ejs: -------------------------------------------------------------------------------- 1 | <% layout('./templates/layout') -%>
<%= text %>
2 | -------------------------------------------------------------------------------- /templates/content.eta: -------------------------------------------------------------------------------- 1 | <% layout('./layout') -%> 2 | 3 |
<%= it.text %>
4 | -------------------------------------------------------------------------------- /templates/double-quotes-variable.liquid: -------------------------------------------------------------------------------- 1 |

{{ "text" }}

2 | -------------------------------------------------------------------------------- /templates/ejs-async.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% async function test() { return(2); } %> 5 | <%- await test(); %> 6 | 7 | -------------------------------------------------------------------------------- /templates/error.hbs: -------------------------------------------------------------------------------- 1 | {{badHelper}} 2 | -------------------------------------------------------------------------------- /templates/footer.art: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/footer.ejs: -------------------------------------------------------------------------------- 1 | <%= footer %> -------------------------------------------------------------------------------- /templates/fragment-footer.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /templates/fragment-header.ejs: -------------------------------------------------------------------------------- 1 |
2 | Sample header (ejs) 3 |
4 | -------------------------------------------------------------------------------- /templates/fragment-header.eta: -------------------------------------------------------------------------------- 1 |
2 | Sample header (eta) 3 |
4 | -------------------------------------------------------------------------------- /templates/header.art: -------------------------------------------------------------------------------- 1 |
2 |

{{title}}

3 |
-------------------------------------------------------------------------------- /templates/header.ejs: -------------------------------------------------------------------------------- 1 | <%= header %> -------------------------------------------------------------------------------- /templates/index-bare.html: -------------------------------------------------------------------------------- 1 |

test


-------------------------------------------------------------------------------- /templates/index-for-layout.ejs: -------------------------------------------------------------------------------- 1 |

<%= text %>

2 | -------------------------------------------------------------------------------- /templates/index-for-layout.eta: -------------------------------------------------------------------------------- 1 |

<%= it.text %>

2 | -------------------------------------------------------------------------------- /templates/index-for-layout.hbs: -------------------------------------------------------------------------------- 1 |

{{text}}

-------------------------------------------------------------------------------- /templates/index-layout-body.ejs: -------------------------------------------------------------------------------- 1 | <%- body %> 2 | -------------------------------------------------------------------------------- /templates/index-layout-content.ejs: -------------------------------------------------------------------------------- 1 | <%- content %> 2 | -------------------------------------------------------------------------------- /templates/index-linking-other-pages.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

<%= text %>

6 |
7 |
8 |

Other EJS pages with includes:

9 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/index-linking-other-pages.eta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

<%= it.text %>

6 |
7 |
8 |

Other Eta pages with includes:

9 |
    10 |
  • Normal page, here
  • 11 |
  • One include not exist, here 12 | (to raise errors) 13 |
  • 14 |
  • One attribute not exist, here 15 | (to raise errors) 16 |
  • 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/index-with-2-partials.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{> partial1 }} 6 | {{> partial2 }} 7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/index-with-custom-tag.liquid: -------------------------------------------------------------------------------- 1 | {%header content: "welcome to liquid" | capitalize%} 2 | 3 |
    4 | {% for todo in todos %} 5 |
  • {{forloop.index}} - {{todo}}
  • 6 | {% endfor %} 7 |
8 | -------------------------------------------------------------------------------- /templates/index-with-global.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

{{ text }}

6 |

{{ myGlobalVar }}

7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/index-with-includes-and-attribute-missing.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <% include('fragment-header.ejs') %> 6 |

<%= text %>

7 |

A not existing page attribute: <%= not_existing_text %>

8 | <% include('fragment-footer.html') %> 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/index-with-includes-and-attribute-missing.eta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%~ include('fragment-header.eta') %> 6 |

<%= it.text %>

7 |

A not existing page attribute: <%= it.not_existing_text %>

8 | <%~ include('fragment-footer.html') %> 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/index-with-includes-one-missing.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <% include('fragment-header.ejs') %> 6 | <% include('fragment-content-not-existing.ejs') %> 7 |

<%= text %>

8 | <% include('fragment-footer.html') %> 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/index-with-includes-one-missing.eta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%~ include('fragment-header.eta') %> 6 | <%~ include('fragment-content-not-existing.eta') %> 7 |

<%= it.text %>

8 | <%~ include('fragment-footer.html') %> 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/index-with-includes-without-ext.html: -------------------------------------------------------------------------------- 1 | <%- include('ok'); %> 2 | -------------------------------------------------------------------------------- /templates/index-with-includes.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- include('fragment-header.ejs') %> 6 |

<%= text %>

7 | <%- include('fragment-footer.html') %> 8 | 9 | 10 | -------------------------------------------------------------------------------- /templates/index-with-includes.eta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%~ include('fragment-header.eta') %> 6 |

<%= it.text %>

7 | <%~ include('fragment-footer.html') %> 8 | 9 | 10 | -------------------------------------------------------------------------------- /templates/index-with-no-data.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

No data

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/index-with-no-data.eta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

No data

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/index-with-partials.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{> body }} 6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/index.edge: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

{{ text }}

6 | 7 | -------------------------------------------------------------------------------- /templates/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

<%= text %>

6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /templates/index.eta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

<%= it.text.toString() %>

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

{{ text }}

6 | 7 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

{{ text }}

6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/index.liquid: -------------------------------------------------------------------------------- 1 |

{{ text }}

2 | -------------------------------------------------------------------------------- /templates/index.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{> body }} 6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/index.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

{{ text }}

6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | body 5 | p #{text} 6 | -------------------------------------------------------------------------------- /templates/index.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 |

{{ text }}

8 | 9 | -------------------------------------------------------------------------------- /templates/layout-eta.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%~ it.body %> 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/layout-ts-content-no-data.ejs: -------------------------------------------------------------------------------- 1 |

No data

-------------------------------------------------------------------------------- /templates/layout-ts-content-with-data.ejs: -------------------------------------------------------------------------------- 1 |

<%= text %>

-------------------------------------------------------------------------------- /templates/layout-ts.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%- body -%> 5 | 6 | 7 | -------------------------------------------------------------------------------- /templates/layout-with-includes.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include('./header') %> 4 | 5 | <%- body %> 6 |
<%- include('./footer') %> 7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/layout.dot: -------------------------------------------------------------------------------- 1 | header: {{! it.text }} 2 | {{= it.body }} 3 | footer 4 | -------------------------------------------------------------------------------- /templates/layout.ejs: -------------------------------------------------------------------------------- 1 |

<%= header %>

<%- body -%>
<%= footer %>
2 | -------------------------------------------------------------------------------- /templates/layout.eta: -------------------------------------------------------------------------------- 1 |

<%= it.header %>

<%~ it.body -%>
<%= it.footer %>
2 | -------------------------------------------------------------------------------- /templates/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{{body}}} 6 | 7 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- body %> 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | body 5 | block content 6 | -------------------------------------------------------------------------------- /templates/nunjucks-layout/layout.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block content %}{% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/nunjucks-template/index.njk: -------------------------------------------------------------------------------- 1 | {% extends "layout.njk" %} 2 | 3 | {% block content %}

{{ text }}

{% endblock %} 4 | -------------------------------------------------------------------------------- /templates/ok.html: -------------------------------------------------------------------------------- 1 | ok 2 | -------------------------------------------------------------------------------- /templates/partial-1.mustache: -------------------------------------------------------------------------------- 1 | Partial 1 - b4d932b9-4baa-4c99-8d14-d45411b9361e 2 | -------------------------------------------------------------------------------- /templates/partial-2.mustache: -------------------------------------------------------------------------------- 1 | Partial 2 - fdab0fe2-6dab-4429-ae9f-dfcb791d1d3d 2 | -------------------------------------------------------------------------------- /templates/sample.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | p #{text} 5 | -------------------------------------------------------------------------------- /templates/template.twig: -------------------------------------------------------------------------------- 1 | {% include "./body.twig" %} 2 | -------------------------------------------------------------------------------- /templates/testdef.def: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /templates/testdot.dot: -------------------------------------------------------------------------------- 1 | foo {{=it && it.text}} 2 | {{#def.testdef}}

foo

3 | -------------------------------------------------------------------------------- /templates/testjst.jst: -------------------------------------------------------------------------------- 1 | {{=it && it.data}} 2 | {{#def.testdef}} 3 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const POV = require('..') 5 | const Fastify = require('fastify') 6 | const minifier = require('html-minifier-terser') 7 | const fs = require('node:fs') 8 | const dot = require('dot') 9 | const handlebars = require('handlebars') 10 | const { Liquid } = require('liquidjs') 11 | const nunjucks = require('nunjucks') 12 | const pug = require('pug') 13 | const Twig = require('twig') 14 | 15 | const data = { text: 'text' } 16 | const minifierOpts = { 17 | removeComments: true, 18 | removeCommentsFromCDATA: true, 19 | collapseWhitespace: true, 20 | collapseBooleanAttributes: true, 21 | removeAttributeQuotes: true, 22 | removeEmptyAttributes: true 23 | } 24 | 25 | module.exports.dotHtmlMinifierTests = function (compileOptions, withMinifierOptions) { 26 | const options = withMinifierOptions ? minifierOpts : {} 27 | 28 | test('reply.view with dot engine and html-minifier-terser', async t => { 29 | t.plan(4) 30 | const fastify = Fastify() 31 | dot.log = false 32 | 33 | fastify.register(POV, { 34 | engine: { 35 | dot 36 | }, 37 | root: 'templates', 38 | options: { 39 | useHtmlMinifier: minifier, 40 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }) 41 | } 42 | }) 43 | 44 | fastify.get('/', (_req, reply) => { 45 | reply.view('testdot', data) 46 | }) 47 | 48 | await fastify.listen({ port: 0 }) 49 | 50 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 51 | 52 | const responseContent = await result.text() 53 | 54 | t.assert.strictEqual(result.status, 200) 55 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 56 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 57 | t.assert.strictEqual(await minifier.minify(dot.process(compileOptions).testdot(data), options), responseContent) 58 | 59 | await fastify.close() 60 | }) 61 | test('reply.view with dot engine and paths excluded from html-minifier-terser', async t => { 62 | t.plan(4) 63 | const fastify = Fastify() 64 | dot.log = false 65 | 66 | fastify.register(POV, { 67 | engine: { 68 | dot 69 | }, 70 | root: 'templates', 71 | options: { 72 | useHtmlMinifier: minifier, 73 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }), 74 | pathsToExcludeHtmlMinifier: ['/test'] 75 | } 76 | }) 77 | 78 | fastify.get('/test', (_req, reply) => { 79 | reply.view('testdot', data) 80 | }) 81 | 82 | await fastify.listen({ port: 0 }) 83 | 84 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/test') 85 | 86 | const responseContent = await result.text() 87 | 88 | t.assert.strictEqual(result.status, 200) 89 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 90 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 91 | t.assert.strictEqual(dot.process(compileOptions).testdot(data), responseContent) 92 | 93 | await fastify.close() 94 | }) 95 | } 96 | 97 | module.exports.etaHtmlMinifierTests = function (withMinifierOptions) { 98 | const { Eta } = require('eta') 99 | const eta = new Eta() 100 | 101 | const options = withMinifierOptions ? minifierOpts : {} 102 | 103 | test('reply.view with eta engine and html-minifier-terser', async t => { 104 | t.plan(4) 105 | const fastify = Fastify() 106 | 107 | fastify.register(POV, { 108 | engine: { 109 | eta 110 | }, 111 | options: { 112 | useHtmlMinifier: minifier, 113 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }) 114 | } 115 | }) 116 | 117 | fastify.get('/', (_req, reply) => { 118 | reply.view('templates/index.eta', data) 119 | }) 120 | 121 | await fastify.listen({ port: 0 }) 122 | 123 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 124 | 125 | const responseContent = await result.text() 126 | 127 | t.assert.strictEqual(result.status, 200) 128 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 129 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 130 | t.assert.strictEqual(await minifier.minify(eta.renderString(fs.readFileSync('./templates/index.eta', 'utf8'), data), options), responseContent) 131 | 132 | await fastify.close() 133 | }) 134 | 135 | test('reply.view with eta engine and async and html-minifier-terser', async t => { 136 | t.plan(4) 137 | const fastify = Fastify() 138 | 139 | fastify.register(POV, { 140 | engine: { 141 | eta 142 | }, 143 | options: { 144 | useHtmlMinifier: minifier, 145 | async: true, 146 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }) 147 | } 148 | }) 149 | 150 | fastify.get('/', (_req, reply) => { 151 | reply.view('templates/index.eta', data) 152 | }) 153 | 154 | await fastify.listen({ port: 0 }) 155 | 156 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 157 | 158 | const responseContent = await result.text() 159 | 160 | t.assert.strictEqual(result.status, 200) 161 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 162 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 163 | t.assert.strictEqual(await minifier.minify(eta.renderString(fs.readFileSync('./templates/index.eta', 'utf8'), data), options), responseContent) 164 | 165 | await fastify.close() 166 | }) 167 | test('reply.view with eta engine and paths excluded from html-minifier-terser', async t => { 168 | t.plan(4) 169 | const fastify = Fastify() 170 | 171 | fastify.register(POV, { 172 | engine: { 173 | eta 174 | }, 175 | options: { 176 | useHtmlMinifier: minifier, 177 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }), 178 | pathsToExcludeHtmlMinifier: ['/test'] 179 | } 180 | }) 181 | 182 | fastify.get('/test', (_req, reply) => { 183 | reply.view('templates/index.eta', data) 184 | }) 185 | 186 | await fastify.listen({ port: 0 }) 187 | 188 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/test') 189 | 190 | const responseContent = await result.text() 191 | 192 | t.assert.strictEqual(result.status, 200) 193 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 194 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 195 | t.assert.strictEqual(eta.renderString(fs.readFileSync('./templates/index.eta', 'utf8'), data), responseContent) 196 | 197 | await fastify.close() 198 | }) 199 | } 200 | 201 | module.exports.handleBarsHtmlMinifierTests = function (withMinifierOptions) { 202 | const options = withMinifierOptions ? minifierOpts : {} 203 | 204 | test('fastify.view with handlebars engine and html-minifier-terser', (t, end) => { 205 | t.plan(2) 206 | const fastify = Fastify() 207 | 208 | fastify.register(POV, { 209 | engine: { 210 | handlebars 211 | }, 212 | options: { 213 | useHtmlMinifier: minifier, 214 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }), 215 | partials: { body: './templates/body.hbs' } 216 | } 217 | }) 218 | 219 | fastify.ready(err => { 220 | t.assert.ifError(err) 221 | 222 | fastify.view('./templates/index.html', data).then(async compiled => { 223 | t.assert.strictEqual(await minifier.minify(handlebars.compile(fs.readFileSync('./templates/index.html', 'utf8'))(data), options), compiled) 224 | fastify.close() 225 | end() 226 | }) 227 | }) 228 | }) 229 | } 230 | 231 | module.exports.liquidHtmlMinifierTests = function (withMinifierOptions) { 232 | const options = withMinifierOptions ? minifierOpts : {} 233 | 234 | test('reply.view with liquid engine and html-minifier-terser', async t => { 235 | t.plan(4) 236 | const fastify = Fastify() 237 | const engine = new Liquid() 238 | 239 | fastify.register(POV, { 240 | engine: { 241 | liquid: engine 242 | }, 243 | options: { 244 | useHtmlMinifier: minifier, 245 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }) 246 | } 247 | }) 248 | 249 | fastify.get('/', (_req, reply) => { 250 | reply.view('./templates/index.liquid', data) 251 | }) 252 | 253 | await fastify.listen({ port: 0 }) 254 | 255 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 256 | 257 | const responseContent = await result.text() 258 | 259 | t.assert.strictEqual(result.status, 200) 260 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 261 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 262 | 263 | const html = await engine.renderFile('./templates/index.liquid', data) 264 | 265 | t.assert.strictEqual(await minifier.minify(html, options), responseContent) 266 | 267 | await fastify.close() 268 | }) 269 | test('reply.view with liquid engine and paths excluded from html-minifier-terser', async t => { 270 | t.plan(4) 271 | const fastify = Fastify() 272 | const engine = new Liquid() 273 | 274 | fastify.register(POV, { 275 | engine: { 276 | liquid: engine 277 | }, 278 | options: { 279 | useHtmlMinifier: minifier, 280 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }), 281 | pathsToExcludeHtmlMinifier: ['/test'] 282 | } 283 | }) 284 | 285 | fastify.get('/test', (_req, reply) => { 286 | reply.view('./templates/index.liquid', data) 287 | }) 288 | 289 | await fastify.listen({ port: 0 }) 290 | 291 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/test') 292 | 293 | const responseContent = await result.text() 294 | 295 | t.assert.strictEqual(result.status, 200) 296 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 297 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 298 | 299 | const html = await engine.renderFile('./templates/index.liquid', data) 300 | 301 | t.assert.strictEqual((await minifier.minify(html, options)).trim(), responseContent.trim()) 302 | 303 | await fastify.close() 304 | }) 305 | } 306 | 307 | module.exports.nunjucksHtmlMinifierTests = function (withMinifierOptions) { 308 | const options = withMinifierOptions ? minifierOpts : {} 309 | 310 | test('reply.view with nunjucks engine, full path templates folder, and html-minifier-terser', async t => { 311 | t.plan(4) 312 | const fastify = Fastify() 313 | 314 | fastify.register(POV, { 315 | engine: { 316 | nunjucks 317 | }, 318 | templates: 'templates', 319 | options: { 320 | useHtmlMinifier: minifier, 321 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }) 322 | } 323 | }) 324 | 325 | fastify.get('/', (_req, reply) => { 326 | reply.view('./index.njk', data) 327 | }) 328 | 329 | await fastify.listen({ port: 0 }) 330 | 331 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 332 | 333 | const responseContent = await result.text() 334 | 335 | t.assert.strictEqual(result.status, 200) 336 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 337 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 338 | 339 | t.assert.strictEqual(await minifier.minify(nunjucks.render('./index.njk', data), options), responseContent) 340 | 341 | await fastify.close() 342 | }) 343 | test('reply.view with nunjucks engine, full path templates folder, and paths excluded from html-minifier-terser', async t => { 344 | t.plan(4) 345 | const fastify = Fastify() 346 | 347 | fastify.register(POV, { 348 | engine: { 349 | nunjucks 350 | }, 351 | templates: 'templates', 352 | options: { 353 | useHtmlMinifier: minifier, 354 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }), 355 | pathsToExcludeHtmlMinifier: ['/test'] 356 | } 357 | }) 358 | 359 | fastify.get('/test', (_req, reply) => { 360 | reply.view('./index.njk', data) 361 | }) 362 | 363 | await fastify.listen({ port: 0 }) 364 | 365 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/test') 366 | 367 | const responseContent = await result.text() 368 | 369 | t.assert.strictEqual(result.status, 200) 370 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 371 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 372 | 373 | t.assert.strictEqual(nunjucks.render('./index.njk', data), responseContent) 374 | 375 | await fastify.close() 376 | }) 377 | } 378 | 379 | module.exports.pugHtmlMinifierTests = function (withMinifierOptions) { 380 | const options = withMinifierOptions ? minifierOpts : {} 381 | 382 | test('reply.view with pug engine and html-minifier-terser', async t => { 383 | t.plan(4) 384 | const fastify = Fastify() 385 | 386 | fastify.register(POV, { 387 | engine: { 388 | pug 389 | }, 390 | options: { 391 | useHtmlMinifier: minifier, 392 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }) 393 | } 394 | }) 395 | 396 | fastify.get('/', (_req, reply) => { 397 | reply.view('./templates/index.pug', data) 398 | }) 399 | 400 | await fastify.listen({ port: 0 }) 401 | 402 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 403 | 404 | const responseContent = await result.text() 405 | 406 | t.assert.strictEqual(result.status, 200) 407 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 408 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 409 | 410 | t.assert.strictEqual(await minifier.minify(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), data), options), responseContent) 411 | 412 | await fastify.close() 413 | }) 414 | test('reply.view with pug engine and paths excluded from html-minifier-terser', async t => { 415 | t.plan(4) 416 | const fastify = Fastify() 417 | 418 | fastify.register(POV, { 419 | engine: { 420 | pug 421 | }, 422 | options: { 423 | useHtmlMinifier: minifier, 424 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }), 425 | pathsToExcludeHtmlMinifier: ['/test'] 426 | } 427 | }) 428 | 429 | fastify.get('/test', (_req, reply) => { 430 | reply.view('./templates/index.pug', data) 431 | }) 432 | 433 | await fastify.listen({ port: 0 }) 434 | 435 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/test') 436 | 437 | const responseContent = await result.text() 438 | 439 | t.assert.strictEqual(result.status, 200) 440 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 441 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 442 | 443 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), data), responseContent) 444 | 445 | await fastify.close() 446 | }) 447 | } 448 | 449 | module.exports.twigHtmlMinifierTests = function (withMinifierOptions) { 450 | const options = withMinifierOptions ? minifierOpts : {} 451 | 452 | test('reply.view with twig engine and html-minifier-terser', async t => { 453 | t.plan(5) 454 | const fastify = Fastify() 455 | 456 | fastify.register(POV, { 457 | engine: { 458 | twig: Twig 459 | }, 460 | options: { 461 | useHtmlMinifier: minifier, 462 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }) 463 | } 464 | }) 465 | 466 | fastify.get('/', (_req, reply) => { 467 | reply.view('./templates/index.twig', data) 468 | }) 469 | 470 | await fastify.listen({ port: 0 }) 471 | 472 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 473 | 474 | const responseContent = await result.text() 475 | 476 | t.assert.strictEqual(result.status, 200) 477 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 478 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 479 | 480 | await new Promise((resolve) => { 481 | Twig.renderFile('./templates/index.twig', data, async (err, html) => { 482 | t.assert.ifError(err) 483 | t.assert.strictEqual(await minifier.minify(html, options), responseContent) 484 | resolve() 485 | }) 486 | }) 487 | 488 | await fastify.close() 489 | }) 490 | test('reply.view with twig engine and paths excluded from html-minifier-terser', async t => { 491 | t.plan(5) 492 | const fastify = Fastify() 493 | 494 | fastify.register(POV, { 495 | engine: { 496 | twig: Twig 497 | }, 498 | options: { 499 | useHtmlMinifier: minifier, 500 | ...(withMinifierOptions && { htmlMinifierOptions: minifierOpts }), 501 | pathsToExcludeHtmlMinifier: ['/test'] 502 | } 503 | }) 504 | 505 | fastify.get('/test', (_req, reply) => { 506 | reply.view('./templates/index.twig', data) 507 | }) 508 | 509 | await fastify.listen({ port: 0 }) 510 | 511 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/test') 512 | 513 | const responseContent = await result.text() 514 | 515 | t.assert.strictEqual(result.status, 200) 516 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 517 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 518 | 519 | await new Promise((resolve) => { 520 | Twig.renderFile('./templates/index.twig', data, async (err, html) => { 521 | t.assert.ifError(err) 522 | t.assert.strictEqual(html, responseContent) 523 | resolve() 524 | }) 525 | }) 526 | 527 | await fastify.close() 528 | }) 529 | } 530 | -------------------------------------------------------------------------------- /test/snap/test-ejs-with-snapshot.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const test = t.test 5 | const Fastify = require('fastify') 6 | const path = require('node:path') 7 | const ejs = require('ejs') 8 | const templatesFolder = 'templates' 9 | const options = { 10 | filename: path.resolve(templatesFolder), 11 | views: [path.join(__dirname, '..')] 12 | } 13 | 14 | test('reply.view with ejs engine, template folder specified, include files (ejs and html) used in template, includeViewExtension property as true; requires TAP snapshots enabled', async t => { 15 | t.plan(6) 16 | const fastify = Fastify() 17 | 18 | const data = { text: 'text' } 19 | 20 | fastify.register(require('../../index'), { 21 | engine: { 22 | ejs 23 | }, 24 | includeViewExtension: true, 25 | templates: templatesFolder, 26 | options 27 | }) 28 | 29 | fastify.get('/', (_req, reply) => { 30 | reply.type('text/html; charset=utf-8').view('index-linking-other-pages', data) // sample for specifying with type 31 | }) 32 | 33 | await fastify.listen({ port: 0 }) 34 | 35 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 36 | 37 | const responseContent = await result.text() 38 | 39 | t.equal(result.status, 200) 40 | t.equal(result.headers.get('content-length'), '' + responseContent.length) 41 | t.equal(result.headers.get('content-type'), 'text/html; charset=utf-8') 42 | 43 | let content = null 44 | 45 | await new Promise(resolve => { 46 | ejs.renderFile(path.join(templatesFolder, 'index-linking-other-pages.ejs'), data, options, function (err, str) { 47 | content = str 48 | t.error(err) 49 | t.equal(content.length, responseContent.length) 50 | resolve() 51 | }) 52 | }) 53 | t.matchSnapshot(content.replace(/\r?\n/g, ''), 'output') // normalize new lines for cross-platform 54 | 55 | await fastify.close() 56 | }) 57 | 58 | test('reply.view with ejs engine, templates with folder specified, include files and attributes; requires TAP snapshots enabled; home', async t => { 59 | t.plan(6) 60 | const fastify = Fastify() 61 | 62 | const data = { text: 'Hello from EJS Templates' } 63 | 64 | fastify.register(require('../../index'), { 65 | engine: { 66 | ejs 67 | }, 68 | includeViewExtension: true, 69 | templates: templatesFolder, 70 | options 71 | }) 72 | 73 | fastify.get('/', (_req, reply) => { 74 | reply.type('text/html; charset=utf-8').view('index', data) 75 | }) 76 | 77 | await fastify.listen({ port: 0 }) 78 | 79 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 80 | 81 | const responseContent = await result.text() 82 | 83 | t.equal(result.status, 200) 84 | t.equal(result.headers.get('content-length'), '' + responseContent.length) 85 | t.equal(result.headers.get('content-type'), 'text/html; charset=utf-8') 86 | 87 | let content = null 88 | await new Promise(resolve => { 89 | ejs.renderFile(path.join(templatesFolder, 'index.ejs'), data, options, function (err, str) { 90 | content = str 91 | t.error(err) 92 | t.equal(content.length, responseContent.length) 93 | resolve() 94 | }) 95 | }) 96 | t.matchSnapshot(content.replace(/\r?\n/g, '')) // normalize new lines for cross-platform 97 | 98 | await fastify.close() 99 | }) 100 | 101 | test('reply.view with ejs engine, templates with folder specified, include files and attributes; requires TAP snapshots enabled; page with includes', async t => { 102 | t.plan(6) 103 | const fastify = Fastify() 104 | 105 | const data = { text: 'Hello from EJS Templates' } 106 | 107 | fastify.register(require('../../index'), { 108 | engine: { 109 | ejs 110 | }, 111 | includeViewExtension: true, 112 | templates: templatesFolder, 113 | options 114 | }) 115 | 116 | fastify.get('/include-test', (_req, reply) => { 117 | reply.type('text/html; charset=utf-8').view('index-with-includes', data) 118 | }) 119 | 120 | await fastify.listen({ port: 0 }) 121 | 122 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/include-test') 123 | 124 | const responseContent = await result.text() 125 | 126 | t.equal(result.status, 200) 127 | t.equal(result.headers.get('content-length'), '' + responseContent.length) 128 | t.equal(result.headers.get('content-type'), 'text/html; charset=utf-8') 129 | 130 | let content = null 131 | await new Promise(resolve => { 132 | ejs.renderFile(path.join(templatesFolder, 'index-with-includes.ejs'), data, options, function (err, str) { 133 | content = str 134 | t.error(err) 135 | t.equal(content.length, responseContent.length) 136 | resolve() 137 | }) 138 | }) 139 | t.matchSnapshot(content.replace(/\r?\n/g, '')) // normalize new lines for cross-platform 140 | 141 | await fastify.close() 142 | }) 143 | 144 | test('reply.view with ejs engine, templates with folder specified, include files and attributes; requires TAP snapshots enabled; page with one include missing', async t => { 145 | t.plan(6) 146 | const fastify = Fastify() 147 | 148 | const data = { text: 'Hello from EJS Templates' } 149 | 150 | fastify.register(require('../../index'), { 151 | engine: { 152 | ejs 153 | }, 154 | includeViewExtension: true, 155 | templates: templatesFolder, 156 | options 157 | }) 158 | 159 | fastify.get('/include-one-include-missing-test', (_req, reply) => { 160 | reply.type('text/html; charset=utf-8').view('index-with-includes-one-missing', data) 161 | }) 162 | 163 | await fastify.listen({ port: 0 }) 164 | 165 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/include-one-include-missing-test') 166 | 167 | const responseContent = await result.text() 168 | 169 | t.equal(result.status, 500) 170 | t.equal(result.headers.get('content-length'), '' + responseContent.length) 171 | t.equal(result.headers.get('content-type'), 'application/json; charset=utf-8') 172 | 173 | let content = null 174 | await new Promise(resolve => { 175 | ejs.renderFile(path.join(templatesFolder, 'index-with-includes-one-missing.ejs'), data, options, function (err, str) { 176 | content = str 177 | t.ok(err instanceof Error) 178 | t.equal(content, undefined) 179 | resolve() 180 | }) 181 | }) 182 | t.matchSnapshot(content) 183 | 184 | await fastify.close() 185 | }) 186 | 187 | test('reply.view with ejs engine, templates with folder specified, include files and attributes; requires TAP snapshots enabled; page with one attribute missing', async t => { 188 | t.plan(6) 189 | const fastify = Fastify() 190 | 191 | const data = { text: 'Hello from EJS Templates' } 192 | 193 | fastify.register(require('../../index'), { 194 | engine: { 195 | ejs 196 | }, 197 | includeViewExtension: true, 198 | templates: templatesFolder, 199 | options 200 | }) 201 | 202 | fastify.get('/include-one-attribute-missing-test', (_req, reply) => { 203 | reply.type('text/html; charset=utf-8').view('index-with-includes-and-attribute-missing', data) 204 | }) 205 | 206 | await fastify.listen({ port: 0 }) 207 | 208 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/include-one-attribute-missing-test') 209 | 210 | const responseContent = await result.text() 211 | 212 | t.equal(result.status, 500) 213 | t.equal(result.headers.get('content-length'), '' + responseContent.length) 214 | t.equal(result.headers.get('content-type'), 'application/json; charset=utf-8') 215 | 216 | let content = null 217 | await new Promise(resolve => { 218 | ejs.renderFile(path.join(templatesFolder, 'index-with-includes-and-attribute-missing.ejs'), data, options, function (err, str) { 219 | content = str 220 | t.ok(err instanceof Error) 221 | t.equal(content, undefined) 222 | resolve() 223 | }) 224 | }) 225 | 226 | t.matchSnapshot(content) 227 | 228 | await fastify.close() 229 | }) 230 | -------------------------------------------------------------------------------- /test/test-dot.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { existsSync, rmdirSync, readFileSync } = require('node:fs') 6 | const { join } = require('node:path') 7 | const pino = require('pino') 8 | const split = require('split2') 9 | const compileOptions = { 10 | path: 'templates', 11 | destination: 'out', 12 | log: false 13 | } 14 | 15 | require('./helper').dotHtmlMinifierTests(compileOptions, true) 16 | require('./helper').dotHtmlMinifierTests(compileOptions, false) 17 | 18 | test('reply.view with dot engine .dot file', async t => { 19 | t.plan(4) 20 | const fastify = Fastify() 21 | const data = { text: 'text' } 22 | const engine = require('dot') 23 | engine.log = false 24 | 25 | fastify.register(require('../index'), { 26 | engine: { 27 | dot: engine 28 | }, 29 | root: 'templates' 30 | }) 31 | 32 | fastify.get('/', (_req, reply) => { 33 | reply.view('testdot', data) 34 | }) 35 | 36 | await fastify.listen({ port: 0 }) 37 | 38 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 39 | 40 | const responseContent = await result.text() 41 | 42 | t.assert.strictEqual(result.status, 200) 43 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 44 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 45 | t.assert.strictEqual(responseContent, engine.process({ path: 'templates', destination: 'out' }).testdot(data)) 46 | 47 | await fastify.close() 48 | }) 49 | 50 | test('reply.view with dot engine .dot file should create non-existent destination', async t => { 51 | t.plan(1) 52 | const fastify = Fastify() 53 | const engine = require('dot') 54 | engine.log = false 55 | 56 | fastify.register(require('../index'), { 57 | engine: { 58 | dot: engine 59 | }, 60 | options: { 61 | destination: 'non-existent' 62 | } 63 | }) 64 | 65 | t.after(() => rmdirSync('non-existent')) 66 | 67 | fastify.get('/', (_req, reply) => { 68 | reply.view('testdot') 69 | }) 70 | 71 | await fastify.listen({ port: 0 }) 72 | 73 | t.assert.ok(existsSync('non-existent')) 74 | 75 | await fastify.close() 76 | }) 77 | 78 | test('reply.view with dot engine .dot file should log WARN if template not found', async t => { 79 | t.plan(1) 80 | const splitStream = split(JSON.parse) 81 | splitStream.on('data', (line) => { 82 | t.assert.strictEqual(line.msg, `WARN: no template found in ${join(__dirname, '..')}`) 83 | }) 84 | const logger = pino({ level: 'warn' }, splitStream) 85 | const fastify = Fastify({ 86 | loggerInstance: logger 87 | }) 88 | const engine = require('dot') 89 | engine.log = false 90 | 91 | t.after(() => rmdirSync('empty')) 92 | 93 | fastify.register(require('../index'), { 94 | engine: { 95 | dot: engine 96 | }, 97 | options: { 98 | destination: 'empty' 99 | } 100 | }) 101 | 102 | await fastify.listen({ port: 0 }) 103 | await fastify.close() 104 | }) 105 | 106 | test('reply.view with dot engine .jst file', async t => { 107 | t.plan(4) 108 | const fastify = Fastify() 109 | const data = { text: 'text' } 110 | const engine = require('dot') 111 | engine.log = false 112 | 113 | fastify.register(require('../index'), { 114 | engine: { 115 | dot: engine 116 | }, 117 | root: 'templates' 118 | }) 119 | 120 | fastify.get('/', (_req, reply) => { 121 | reply.view('testjst', data) 122 | }) 123 | 124 | await fastify.listen({ port: 0 }) 125 | 126 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 127 | 128 | const responseContent = await result.text() 129 | 130 | t.assert.strictEqual(result.status, 200) 131 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 132 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 133 | engine.process(compileOptions) 134 | t.assert.strictEqual(responseContent, require('../out/testjst')(data)) 135 | 136 | await fastify.close() 137 | }) 138 | 139 | test('reply.view with dot engine without data-parameter but defaultContext', async t => { 140 | t.plan(4) 141 | const fastify = Fastify() 142 | const data = { text: 'text' } 143 | 144 | const engine = require('dot') 145 | engine.log = false 146 | 147 | fastify.register(require('../index'), { 148 | engine: { 149 | dot: engine 150 | }, 151 | defaultContext: data, 152 | root: 'templates' 153 | }) 154 | 155 | fastify.get('/', (_req, reply) => { 156 | reply.view('testdot') 157 | }) 158 | 159 | await fastify.listen({ port: 0 }) 160 | 161 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 162 | 163 | const responseContent = await result.text() 164 | 165 | t.assert.strictEqual(result.status, 200) 166 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 167 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 168 | t.assert.strictEqual(responseContent, engine.process(compileOptions).testdot(data)) 169 | 170 | await fastify.close() 171 | }) 172 | 173 | test('reply.view with dot engine without data-parameter but without defaultContext', async t => { 174 | t.plan(4) 175 | const fastify = Fastify() 176 | 177 | const engine = require('dot') 178 | engine.log = false 179 | 180 | fastify.register(require('../index'), { 181 | engine: { 182 | dot: engine 183 | }, 184 | root: 'templates' 185 | }) 186 | 187 | fastify.get('/', (_req, reply) => { 188 | reply.view('testdot') 189 | }) 190 | 191 | await fastify.listen({ port: 0 }) 192 | 193 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 194 | 195 | const responseContent = await result.text() 196 | 197 | t.assert.strictEqual(result.status, 200) 198 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 199 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 200 | engine.process(compileOptions) 201 | t.assert.strictEqual(responseContent, engine.process(compileOptions).testdot()) 202 | 203 | await fastify.close() 204 | }) 205 | 206 | test('reply.view with dot engine with data-parameter and defaultContext', async t => { 207 | t.plan(4) 208 | const fastify = Fastify() 209 | const data = { text: 'text' } 210 | 211 | const engine = require('dot') 212 | engine.log = false 213 | 214 | fastify.register(require('../index'), { 215 | engine: { 216 | dot: engine 217 | }, 218 | defaultContext: data, 219 | root: 'templates' 220 | }) 221 | 222 | fastify.get('/', (_req, reply) => { 223 | reply.view('testdot', {}) 224 | }) 225 | 226 | await fastify.listen({ port: 0 }) 227 | 228 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 229 | 230 | const responseContent = await result.text() 231 | 232 | t.assert.strictEqual(result.status, 200) 233 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 234 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 235 | t.assert.strictEqual(responseContent, engine.process(compileOptions).testdot(data)) 236 | 237 | await fastify.close() 238 | }) 239 | 240 | test('reply.view for dot engine without data-parameter and defaultContext but with reply.locals', async t => { 241 | t.plan(4) 242 | const fastify = Fastify() 243 | const localsData = { text: 'text from locals' } 244 | 245 | const engine = require('dot') 246 | engine.log = false 247 | 248 | fastify.register(require('../index'), { 249 | engine: { 250 | dot: engine 251 | }, 252 | root: 'templates' 253 | }) 254 | 255 | fastify.addHook('preHandler', function (_request, reply, done) { 256 | reply.locals = localsData 257 | done() 258 | }) 259 | 260 | fastify.get('/', (_req, reply) => { 261 | reply.view('testdot', {}) 262 | }) 263 | 264 | await fastify.listen({ port: 0 }) 265 | 266 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 267 | 268 | const responseContent = await result.text() 269 | 270 | t.assert.strictEqual(result.status, 200) 271 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 272 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 273 | t.assert.strictEqual(responseContent, engine.process(compileOptions).testdot(localsData)) 274 | 275 | await fastify.close() 276 | }) 277 | 278 | test('reply.view for dot engine without defaultContext but with reply.locals and data-parameter', async t => { 279 | t.plan(4) 280 | const fastify = Fastify() 281 | const localsData = { text: 'text from locals' } 282 | const data = { text: 'text' } 283 | 284 | const engine = require('dot') 285 | engine.log = false 286 | 287 | fastify.register(require('../index'), { 288 | engine: { 289 | dot: engine 290 | }, 291 | root: 'templates' 292 | }) 293 | 294 | fastify.addHook('preHandler', function (_request, reply, done) { 295 | reply.locals = localsData 296 | done() 297 | }) 298 | 299 | fastify.get('/', (_req, reply) => { 300 | reply.view('testdot', data) 301 | }) 302 | 303 | await fastify.listen({ port: 0 }) 304 | 305 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 306 | 307 | const responseContent = await result.text() 308 | 309 | t.assert.strictEqual(result.status, 200) 310 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 311 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 312 | t.assert.strictEqual(responseContent, engine.process(compileOptions).testdot(data)) 313 | 314 | await fastify.close() 315 | }) 316 | 317 | test('reply.view for dot engine without data-parameter but with reply.locals and defaultContext', async t => { 318 | t.plan(4) 319 | const fastify = Fastify() 320 | const localsData = { text: 'text from locals' } 321 | const defaultContext = { text: 'text' } 322 | 323 | const engine = require('dot') 324 | engine.log = false 325 | 326 | fastify.register(require('../index'), { 327 | engine: { 328 | dot: engine 329 | }, 330 | defaultContext, 331 | root: 'templates' 332 | }) 333 | 334 | fastify.addHook('preHandler', function (_request, reply, done) { 335 | reply.locals = localsData 336 | done() 337 | }) 338 | 339 | fastify.get('/', (_req, reply) => { 340 | reply.view('testdot') 341 | }) 342 | 343 | await fastify.listen({ port: 0 }) 344 | 345 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 346 | 347 | const responseContent = await result.text() 348 | 349 | t.assert.strictEqual(result.status, 200) 350 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 351 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 352 | t.assert.strictEqual(responseContent, engine.process(compileOptions).testdot(localsData)) 353 | 354 | await fastify.close() 355 | }) 356 | 357 | test('reply.view for dot engine with data-parameter and reply.locals and defaultContext', async t => { 358 | t.plan(4) 359 | const fastify = Fastify() 360 | const localsData = { text: 'text from locals' } 361 | const defaultContext = { text: 'text from context' } 362 | const data = { text: 'text' } 363 | 364 | const engine = require('dot') 365 | 366 | fastify.register(require('../index'), { 367 | engine: { 368 | dot: engine 369 | }, 370 | defaultContext, 371 | root: 'templates' 372 | }) 373 | 374 | fastify.addHook('preHandler', function (_request, reply, done) { 375 | reply.locals = localsData 376 | done() 377 | }) 378 | 379 | fastify.get('/', (_req, reply) => { 380 | reply.view('testdot', data) 381 | }) 382 | 383 | await fastify.listen({ port: 0 }) 384 | 385 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 386 | 387 | const responseContent = await result.text() 388 | 389 | t.assert.strictEqual(result.status, 200) 390 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 391 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 392 | t.assert.strictEqual(responseContent, engine.process(compileOptions).testdot(data)) 393 | 394 | await fastify.close() 395 | }) 396 | 397 | test('fastify.view with dot engine, should throw page missing', (t, end) => { 398 | t.plan(3) 399 | const fastify = Fastify() 400 | const engine = require('dot') 401 | engine.log = false 402 | 403 | fastify.register(require('../index'), { 404 | engine: { 405 | dot: engine 406 | } 407 | }) 408 | 409 | fastify.ready(err => { 410 | t.assert.ifError(err) 411 | 412 | fastify.view(null, {}, err => { 413 | t.assert.ok(err instanceof Error) 414 | t.assert.strictEqual(err.message, 'Missing page') 415 | fastify.close() 416 | end() 417 | }) 418 | }) 419 | }) 420 | 421 | test('reply.view with dot engine with layout option', async t => { 422 | t.plan(4) 423 | const fastify = Fastify() 424 | const engine = require('dot') 425 | const data = { text: 'text' } 426 | 427 | fastify.register(require('../index'), { 428 | engine: { 429 | dot: engine 430 | }, 431 | root: 'templates', 432 | layout: 'layout' 433 | }) 434 | 435 | fastify.get('/', (_req, reply) => { 436 | reply.view('testdot', data) 437 | }) 438 | 439 | await fastify.listen({ port: 0 }) 440 | 441 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 442 | 443 | const responseContent = await result.text() 444 | 445 | t.assert.strictEqual(result.status, 200) 446 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 447 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 448 | t.assert.strictEqual('header: textfoo text1

foo

footer', responseContent) 449 | 450 | await fastify.close() 451 | }) 452 | 453 | test('reply.view with dot engine with layout option on render', async t => { 454 | t.plan(4) 455 | const fastify = Fastify() 456 | const engine = require('dot') 457 | const data = { text: 'text' } 458 | 459 | fastify.register(require('../index'), { 460 | engine: { 461 | dot: engine 462 | }, 463 | root: 'templates' 464 | }) 465 | 466 | fastify.get('/', (_req, reply) => { 467 | reply.view('testdot', data, { layout: 'layout' }) 468 | }) 469 | 470 | await fastify.listen({ port: 0 }) 471 | 472 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 473 | 474 | const responseContent = await result.text() 475 | 476 | t.assert.strictEqual(result.status, 200) 477 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 478 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 479 | t.assert.strictEqual('header: textfoo text1

foo

footer', responseContent) 480 | 481 | await fastify.close() 482 | }) 483 | 484 | test('reply.view with dot engine with layout option on render', async t => { 485 | t.plan(4) 486 | const fastify = Fastify() 487 | const engine = require('dot') 488 | const data = { text: 'text' } 489 | 490 | fastify.register(require('../index'), { 491 | engine: { 492 | dot: engine 493 | }, 494 | root: 'templates' 495 | }) 496 | 497 | fastify.get('/', (_req, reply) => { 498 | reply.view('testdot', data, { layout: 'layout' }) 499 | }) 500 | 501 | await fastify.listen({ port: 0 }) 502 | 503 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 504 | 505 | const responseContent = await result.text() 506 | 507 | t.assert.strictEqual(result.status, 200) 508 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 509 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 510 | t.assert.strictEqual('header: textfoo text1

foo

footer', responseContent) 511 | 512 | await fastify.close() 513 | }) 514 | 515 | test('reply.view should return 500 if layout is missing on render', async t => { 516 | t.plan(1) 517 | const fastify = Fastify() 518 | const engine = require('dot') 519 | const data = { text: 'text' } 520 | 521 | fastify.register(require('../index'), { 522 | engine: { 523 | dot: engine 524 | }, 525 | root: 'templates' 526 | }) 527 | 528 | fastify.get('/', (_req, reply) => { 529 | reply.view('testdot', data, { layout: 'non-existing-layout' }) 530 | }) 531 | 532 | await fastify.listen({ port: 0 }) 533 | 534 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 535 | 536 | t.assert.strictEqual(result.status, 500) 537 | 538 | await fastify.close() 539 | }) 540 | 541 | test('reply.view with dot engine and raw template', async t => { 542 | t.plan(4) 543 | const fastify = Fastify() 544 | const data = { text: 'text' } 545 | const engine = require('dot') 546 | engine.log = false 547 | 548 | fastify.register(require('../index'), { 549 | engine: { 550 | dot: engine 551 | } 552 | }) 553 | 554 | fastify.get('/', (_req, reply) => { 555 | reply.view({ raw: readFileSync('./templates/testdot.dot'), imports: { testdef: readFileSync('./templates/testdef.def') } }, data) 556 | }) 557 | 558 | await fastify.listen({ port: 0 }) 559 | 560 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 561 | 562 | const responseContent = await result.text() 563 | 564 | t.assert.strictEqual(result.status, 200) 565 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 566 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 567 | t.assert.strictEqual(responseContent, engine.process({ path: 'templates', destination: 'out' }).testdot(data)) 568 | 569 | await fastify.close() 570 | }) 571 | 572 | test('reply.view with dot engine and function template', async t => { 573 | t.plan(4) 574 | const fastify = Fastify() 575 | const data = { text: 'text' } 576 | const engine = require('dot') 577 | engine.log = false 578 | 579 | fastify.register(require('../index'), { 580 | engine: { 581 | dot: engine 582 | } 583 | }) 584 | 585 | fastify.get('/', (_req, reply) => { 586 | reply.header('Content-Type', 'text/html').view(engine.process({ path: 'templates', destination: 'out' }).testdot, data) 587 | }) 588 | 589 | await fastify.listen({ port: 0 }) 590 | 591 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 592 | 593 | const responseContent = await result.text() 594 | 595 | t.assert.strictEqual(result.status, 200) 596 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 597 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html') 598 | t.assert.strictEqual(responseContent, engine.process({ path: 'templates', destination: 'out' }).testdot(data)) 599 | 600 | await fastify.close() 601 | }) 602 | -------------------------------------------------------------------------------- /test/test-edge.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const { join } = require('node:path') 6 | 7 | require('./helper').liquidHtmlMinifierTests(true) 8 | require('./helper').liquidHtmlMinifierTests(false) 9 | 10 | test('reply.view with liquid engine', async t => { 11 | t.plan(4) 12 | const fastify = Fastify() 13 | const { Edge } = require('edge.js') 14 | const data = { text: 'text' } 15 | 16 | const engine = new Edge() 17 | engine.mount(join(__dirname, '..', 'templates')) 18 | 19 | fastify.register(require('../index'), { 20 | engine: { 21 | edge: engine 22 | } 23 | }) 24 | 25 | fastify.get('/', (_req, reply) => { 26 | reply.view('index.edge', data) 27 | }) 28 | 29 | await fastify.listen({ port: 0 }) 30 | 31 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 32 | const responseContent = await result.text() 33 | 34 | t.assert.strictEqual(result.status, 200) 35 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 36 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 37 | 38 | const html = await engine.render('index.edge', data) 39 | t.assert.strictEqual(html, responseContent) 40 | 41 | await fastify.close() 42 | }) 43 | -------------------------------------------------------------------------------- /test/test-ejs-async.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const fs = require('node:fs') 6 | const minifier = require('html-minifier-terser') 7 | 8 | test('reply.view with ejs engine and async: true (global option)', async t => { 9 | t.plan(4) 10 | const fastify = Fastify() 11 | const ejs = require('ejs') 12 | 13 | fastify.register(require('../index'), { 14 | engine: { ejs }, 15 | options: { async: true }, 16 | templates: 'templates' 17 | }) 18 | 19 | fastify.get('/', (_req, reply) => { 20 | reply.view('ejs-async.ejs') 21 | }) 22 | 23 | await fastify.listen({ port: 0 }) 24 | 25 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 26 | 27 | const responseContent = await result.text() 28 | 29 | t.assert.strictEqual(result.status, 200) 30 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 31 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 32 | t.assert.strictEqual(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), responseContent) 33 | 34 | await fastify.close() 35 | }) 36 | 37 | test('reply.view with ejs engine, async: true (global option), and production: true', async t => { 38 | const numTests = 2 39 | t.plan(numTests * 4) 40 | const fastify = Fastify() 41 | const ejs = require('ejs') 42 | 43 | fastify.register(require('../index'), { 44 | engine: { ejs }, 45 | production: true, 46 | options: { async: true }, 47 | templates: 'templates' 48 | }) 49 | 50 | fastify.get('/', (_req, reply) => { 51 | reply.view('ejs-async.ejs') 52 | }) 53 | 54 | await fastify.listen({ port: 0 }) 55 | 56 | for (let i = 0; i < numTests; i++) { 57 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 58 | 59 | const responseContent = await result.text() 60 | 61 | t.assert.strictEqual(result.status, 200) 62 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 63 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 64 | t.assert.strictEqual(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), responseContent) 65 | 66 | if (i === numTests - 1) await fastify.close() 67 | } 68 | }) 69 | 70 | const minifierOpts = { collapseWhitespace: true } 71 | test('reply.view with ejs engine, async: true (global option), and html-minifier-terser', async t => { 72 | t.plan(4) 73 | const fastify = Fastify() 74 | const ejs = require('ejs') 75 | 76 | fastify.register(require('../index'), { 77 | engine: { ejs }, 78 | options: { 79 | async: true, 80 | useHtmlMinifier: minifier, 81 | htmlMinifierOptions: minifierOpts 82 | }, 83 | templates: 'templates' 84 | }) 85 | 86 | fastify.get('/', (_req, reply) => { 87 | reply.view('ejs-async.ejs') 88 | }) 89 | 90 | await fastify.listen({ port: 0 }) 91 | 92 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 93 | 94 | const responseContent = await result.text() 95 | 96 | t.assert.strictEqual(result.status, 200) 97 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 98 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 99 | t.assert.strictEqual(await minifier.minify(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), minifierOpts), responseContent) 100 | 101 | await fastify.close() 102 | }) 103 | 104 | test('reply.view with ejs engine, async: true (global option), and html-minifier without option', async t => { 105 | t.plan(4) 106 | const fastify = Fastify() 107 | const ejs = require('ejs') 108 | 109 | fastify.register(require('../index'), { 110 | engine: { ejs }, 111 | options: { 112 | async: true, 113 | useHtmlMinifier: minifier 114 | }, 115 | templates: 'templates' 116 | }) 117 | 118 | fastify.get('/', (_req, reply) => { 119 | reply.view('ejs-async.ejs') 120 | }) 121 | 122 | await fastify.listen({ port: 0 }) 123 | 124 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 125 | 126 | const responseContent = await result.text() 127 | 128 | t.assert.strictEqual(result.status, 200) 129 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 130 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 131 | t.assert.strictEqual(await minifier.minify(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true })), responseContent) 132 | 133 | await fastify.close() 134 | }) 135 | 136 | test('reply.view with ejs engine, async: true (global option), and html-minifier in production mode', async t => { 137 | const numTests = 3 138 | t.plan(numTests * 4) 139 | const fastify = Fastify() 140 | const ejs = require('ejs') 141 | 142 | fastify.register(require('../index'), { 143 | engine: { ejs }, 144 | production: true, 145 | options: { 146 | async: true, 147 | useHtmlMinifier: minifier, 148 | htmlMinifierOptions: minifierOpts 149 | }, 150 | templates: 'templates' 151 | }) 152 | 153 | fastify.get('/', (_req, reply) => { 154 | reply.view('ejs-async.ejs') 155 | }) 156 | 157 | await fastify.listen({ port: 0 }) 158 | 159 | for (let i = 0; i < numTests; i++) { 160 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 161 | 162 | const responseContent = await result.text() 163 | 164 | t.assert.strictEqual(result.status, 200) 165 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 166 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 167 | t.assert.strictEqual(await minifier.minify(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), minifierOpts), responseContent) 168 | 169 | if (i === numTests - 1) await fastify.close() 170 | } 171 | }) 172 | 173 | test('reply.view with ejs engine and async: true (local option)', async t => { 174 | t.plan(4) 175 | const fastify = Fastify() 176 | const ejs = require('ejs') 177 | 178 | fastify.register(require('../index'), { 179 | engine: { ejs }, 180 | templates: 'templates' 181 | }) 182 | 183 | fastify.get('/', (_req, reply) => { 184 | reply.view('ejs-async.ejs', {}, { async: true }) 185 | }) 186 | 187 | await fastify.listen({ port: 0 }) 188 | 189 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 190 | 191 | const responseContent = await result.text() 192 | 193 | t.assert.strictEqual(result.status, 200) 194 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 195 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 196 | t.assert.strictEqual(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), responseContent) 197 | 198 | await fastify.close() 199 | }) 200 | 201 | test('reply.view with ejs engine, async: true (local option), and production: true', async t => { 202 | const numTests = 5 203 | t.plan(numTests * 4) 204 | const fastify = Fastify() 205 | const ejs = require('ejs') 206 | 207 | fastify.register(require('../index'), { 208 | engine: { ejs }, 209 | production: true, 210 | templates: 'templates' 211 | }) 212 | 213 | fastify.get('/', (_req, reply) => { 214 | reply.view('ejs-async.ejs', {}, { async: true }) 215 | }) 216 | 217 | await fastify.listen({ port: 0 }) 218 | 219 | for (let i = 0; i < numTests; i++) { 220 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 221 | 222 | const responseContent = await result.text() 223 | 224 | t.assert.strictEqual(result.status, 200) 225 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 226 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 227 | t.assert.strictEqual(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), responseContent) 228 | 229 | if (i === numTests - 1) await fastify.close() 230 | } 231 | }) 232 | 233 | test('reply.view with ejs engine, async: true (local override), and html-minifier-terser', async t => { 234 | t.plan(4) 235 | const fastify = Fastify() 236 | const ejs = require('ejs') 237 | 238 | fastify.register(require('../index'), { 239 | engine: { ejs }, 240 | options: { 241 | async: false, 242 | useHtmlMinifier: minifier, 243 | htmlMinifierOptions: minifierOpts 244 | }, 245 | templates: 'templates' 246 | }) 247 | 248 | fastify.get('/', (_req, reply) => { 249 | reply.view('ejs-async.ejs', {}, { async: true }) 250 | }) 251 | 252 | await fastify.listen({ port: 0 }) 253 | 254 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 255 | 256 | const responseContent = await result.text() 257 | 258 | t.assert.strictEqual(result.status, 200) 259 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 260 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 261 | t.assert.strictEqual(await minifier.minify(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), minifierOpts), responseContent) 262 | 263 | await fastify.close() 264 | }) 265 | 266 | test('reply.view with ejs engine, async: false (local override), and html-minifier-terser', async t => { 267 | t.plan(4) 268 | const fastify = Fastify() 269 | const ejs = require('ejs') 270 | 271 | fastify.register(require('../index'), { 272 | engine: { ejs }, 273 | options: { 274 | async: true, 275 | useHtmlMinifier: minifier, 276 | htmlMinifierOptions: minifierOpts 277 | }, 278 | templates: 'templates' 279 | }) 280 | 281 | fastify.get('/', (_req, reply) => { 282 | reply.view('index.ejs', { text: 'text' }, { async: false }) 283 | }) 284 | 285 | await fastify.listen({ port: 0 }) 286 | 287 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 288 | 289 | const responseContent = await result.text() 290 | 291 | t.assert.strictEqual(result.status, 200) 292 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 293 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 294 | t.assert.strictEqual(await minifier.minify(await ejs.render(fs.readFileSync('./templates/index.ejs', 'utf8'), { text: 'text' }, { async: false }), minifierOpts), responseContent) 295 | 296 | await fastify.close() 297 | }) 298 | 299 | test('reply.view with ejs engine, async: true (local override), and html-minifier-terser in production mode', async t => { 300 | const numTests = 3 301 | t.plan(numTests * 4) 302 | const fastify = Fastify() 303 | const ejs = require('ejs') 304 | 305 | fastify.register(require('../index'), { 306 | engine: { ejs }, 307 | production: true, 308 | options: { 309 | async: false, 310 | useHtmlMinifier: minifier, 311 | htmlMinifierOptions: minifierOpts 312 | }, 313 | templates: 'templates' 314 | }) 315 | 316 | fastify.get('/', (_req, reply) => { 317 | reply.view('ejs-async.ejs', {}, { async: true }) 318 | }) 319 | 320 | await fastify.listen({ port: 0 }) 321 | 322 | for (let i = 0; i < numTests; i++) { 323 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 324 | 325 | const responseContent = await result.text() 326 | 327 | t.assert.strictEqual(result.status, 200) 328 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 329 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 330 | t.assert.strictEqual(await minifier.minify(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), minifierOpts), responseContent) 331 | 332 | if (i === numTests - 1) await fastify.close() 333 | } 334 | }) 335 | 336 | test('reply.view with ejs engine, async: false (local override), and html-minifier-terser in production mode', async t => { 337 | const numTests = 2 338 | t.plan(numTests * 4) 339 | const fastify = Fastify() 340 | const ejs = require('ejs') 341 | 342 | fastify.register(require('../index'), { 343 | engine: { ejs }, 344 | production: true, 345 | options: { 346 | async: true, 347 | useHtmlMinifier: minifier, 348 | htmlMinifierOptions: minifierOpts 349 | }, 350 | templates: 'templates' 351 | }) 352 | 353 | fastify.get('/', (_req, reply) => { 354 | reply.view('index.ejs', { text: 'text' }, { async: false }) 355 | }) 356 | 357 | await fastify.listen({ port: 0 }) 358 | 359 | for (let i = 0; i < numTests; i++) { 360 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 361 | 362 | const responseContent = await result.text() 363 | 364 | t.assert.strictEqual(result.status, 200) 365 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 366 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 367 | t.assert.strictEqual(await minifier.minify(await ejs.render(fs.readFileSync('./templates/index.ejs', 'utf8'), { text: 'text' }, { async: false }), minifierOpts), responseContent) 368 | 369 | if (i === numTests - 1) await fastify.close() 370 | } 371 | }) 372 | 373 | test('reply.view with ejs engine and async: true and raw template', async t => { 374 | t.plan(4) 375 | const fastify = Fastify() 376 | const ejs = require('ejs') 377 | 378 | fastify.register(require('../index'), { 379 | engine: { ejs } 380 | }) 381 | 382 | fastify.get('/', (_req, reply) => { 383 | reply.view({ raw: fs.readFileSync('./templates/ejs-async.ejs', 'utf8') }, {}, { async: true }) 384 | }) 385 | 386 | await fastify.listen({ port: 0 }) 387 | 388 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 389 | 390 | const responseContent = await result.text() 391 | 392 | t.assert.strictEqual(result.status, 200) 393 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 394 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 395 | t.assert.strictEqual(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), responseContent) 396 | 397 | await fastify.close() 398 | }) 399 | 400 | test('reply.view with ejs engine and async: true and function template', async t => { 401 | t.plan(4) 402 | const fastify = Fastify() 403 | const ejs = require('ejs') 404 | 405 | fastify.register(require('../index'), { 406 | engine: { ejs } 407 | }) 408 | 409 | fastify.get('/', (_req, reply) => { 410 | reply.view(ejs.compile(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), { async: true }), {}) 411 | }) 412 | 413 | await fastify.listen({ port: 0 }) 414 | 415 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 416 | 417 | const responseContent = await result.text() 418 | 419 | t.assert.strictEqual(result.status, 200) 420 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 421 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 422 | t.assert.strictEqual(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), responseContent) 423 | 424 | await fastify.close() 425 | }) 426 | 427 | test('reply.view with promise error', async t => { 428 | t.plan(1) 429 | const fastify = Fastify() 430 | const ejs = require('ejs') 431 | 432 | fastify.register(require('../index'), { 433 | engine: { ejs } 434 | }) 435 | 436 | fastify.get('/', (_req, reply) => { 437 | reply.view(() => Promise.reject(new Error('error')), {}) 438 | }) 439 | 440 | await fastify.listen({ port: 0 }) 441 | 442 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 443 | 444 | t.assert.strictEqual(result.status, 500) 445 | 446 | await fastify.close() 447 | }) 448 | 449 | test('reply.viewAsync with ejs engine and async: true (global option)', async t => { 450 | t.plan(4) 451 | const fastify = Fastify() 452 | const ejs = require('ejs') 453 | 454 | fastify.register(require('../index'), { 455 | engine: { ejs }, 456 | options: { async: true }, 457 | templates: 'templates' 458 | }) 459 | 460 | fastify.get('/', async (_req, reply) => { 461 | return reply.viewAsync('ejs-async.ejs') 462 | }) 463 | 464 | await fastify.listen({ port: 0 }) 465 | 466 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 467 | 468 | const responseContent = await result.text() 469 | 470 | t.assert.strictEqual(result.status, 200) 471 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 472 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 473 | t.assert.strictEqual(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), responseContent) 474 | 475 | await fastify.close() 476 | }) 477 | 478 | test('reply.viewAsync with ejs engine, async: true (global option), and html-minifier-terser', async t => { 479 | t.plan(4) 480 | const fastify = Fastify() 481 | const ejs = require('ejs') 482 | 483 | fastify.register(require('../index'), { 484 | engine: { ejs }, 485 | options: { 486 | async: true, 487 | useHtmlMinifier: minifier, 488 | htmlMinifierOptions: minifierOpts 489 | }, 490 | templates: 'templates' 491 | }) 492 | 493 | fastify.get('/', (_req, reply) => { 494 | return reply.viewAsync('ejs-async.ejs') 495 | }) 496 | 497 | await fastify.listen({ port: 0 }) 498 | 499 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 500 | 501 | const responseContent = await result.text() 502 | 503 | t.assert.strictEqual(result.status, 200) 504 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 505 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 506 | t.assert.strictEqual(await minifier.minify(await ejs.render(fs.readFileSync('./templates/ejs-async.ejs', 'utf8'), {}, { async: true }), minifierOpts), responseContent) 507 | 508 | await fastify.close() 509 | }) 510 | -------------------------------------------------------------------------------- /test/test-import-engine.mjs: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify' 2 | import fs from 'node:fs' 3 | import { test } from 'node:test' 4 | 5 | test('using an imported engine as a promise', async t => { 6 | t.plan(1) 7 | const fastify = Fastify() 8 | const data = { text: 'text' } 9 | const ejs = import('ejs') 10 | 11 | fastify.register(import('../index.js'), { engine: { ejs }, templates: 'templates' }) 12 | 13 | fastify.get('/', (_req, reply) => { 14 | reply.view('index.ejs', data) 15 | }) 16 | 17 | await fastify.listen({ port: 0 }) 18 | 19 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 20 | 21 | t.assert.strictEqual((await ejs).render(fs.readFileSync('./templates/index.ejs', 'utf8'), data), await result.text()) 22 | fastify.close() 23 | }) 24 | -------------------------------------------------------------------------------- /test/test-liquid.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fs = require('node:fs') 5 | const Fastify = require('fastify') 6 | 7 | require('./helper').liquidHtmlMinifierTests(true) 8 | require('./helper').liquidHtmlMinifierTests(false) 9 | 10 | test('reply.view with liquid engine', async t => { 11 | t.plan(4) 12 | const fastify = Fastify() 13 | const { Liquid } = require('liquidjs') 14 | const data = { text: 'text' } 15 | 16 | const engine = new Liquid() 17 | 18 | fastify.register(require('../index'), { 19 | engine: { 20 | liquid: engine 21 | } 22 | }) 23 | 24 | fastify.get('/', (_req, reply) => { 25 | reply.view('./templates/index.liquid', data) 26 | }) 27 | 28 | await fastify.listen({ port: 0 }) 29 | 30 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 31 | const responseContent = await result.text() 32 | 33 | t.assert.strictEqual(result.status, 200) 34 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 35 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 36 | 37 | const html = await engine.renderFile('./templates/index.liquid', data) 38 | t.assert.strictEqual(html, responseContent) 39 | 40 | await fastify.close() 41 | }) 42 | 43 | test('reply.view with liquid engine without data-parameter but defaultContext', async t => { 44 | t.plan(4) 45 | const fastify = Fastify() 46 | const { Liquid } = require('liquidjs') 47 | const data = { text: 'text' } 48 | 49 | const engine = new Liquid() 50 | 51 | fastify.register(require('../index'), { 52 | engine: { 53 | liquid: engine 54 | }, 55 | defaultContext: data 56 | }) 57 | 58 | fastify.get('/', (_req, reply) => { 59 | reply.view('./templates/index.liquid') 60 | }) 61 | 62 | await fastify.listen({ port: 0 }) 63 | 64 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 65 | const responseContent = await result.text() 66 | 67 | t.assert.strictEqual(result.status, 200) 68 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 69 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 70 | 71 | const html = await engine.renderFile('./templates/index.liquid', data) 72 | t.assert.strictEqual(html, responseContent) 73 | 74 | await fastify.close() 75 | }) 76 | 77 | test('reply.view with liquid engine without data-parameter but without defaultContext', async t => { 78 | t.plan(4) 79 | const fastify = Fastify() 80 | const { Liquid } = require('liquidjs') 81 | 82 | const engine = new Liquid() 83 | 84 | fastify.register(require('../index'), { 85 | engine: { 86 | liquid: engine 87 | } 88 | }) 89 | 90 | fastify.get('/', (_req, reply) => { 91 | reply.view('./templates/index.liquid') 92 | }) 93 | 94 | await fastify.listen({ port: 0 }) 95 | 96 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 97 | const responseContent = await result.text() 98 | 99 | t.assert.strictEqual(result.status, 200) 100 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 101 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 102 | 103 | const html = await engine.renderFile('./templates/index.liquid') 104 | t.assert.strictEqual(html, responseContent) 105 | 106 | await fastify.close() 107 | }) 108 | 109 | test('reply.view with liquid engine with data-parameter and defaultContext', async t => { 110 | t.plan(4) 111 | const fastify = Fastify() 112 | const { Liquid } = require('liquidjs') 113 | const data = { text: 'text' } 114 | 115 | const engine = new Liquid() 116 | 117 | fastify.register(require('../index'), { 118 | engine: { 119 | liquid: engine 120 | }, 121 | defaultContext: data 122 | }) 123 | 124 | fastify.get('/', (_req, reply) => { 125 | reply.view('./templates/index.liquid', {}) 126 | }) 127 | 128 | await fastify.listen({ port: 0 }) 129 | 130 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 131 | const responseContent = await result.text() 132 | 133 | t.assert.strictEqual(result.status, 200) 134 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 135 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 136 | 137 | const html = await engine.renderFile('./templates/index.liquid', data) 138 | t.assert.strictEqual(html, responseContent) 139 | 140 | await fastify.close() 141 | }) 142 | 143 | test('reply.view for liquid engine without data-parameter and defaultContext but with reply.locals', async t => { 144 | t.plan(4) 145 | const fastify = Fastify() 146 | const { Liquid } = require('liquidjs') 147 | const localsData = { text: 'text from locals' } 148 | 149 | const engine = new Liquid() 150 | 151 | fastify.register(require('../index'), { 152 | engine: { 153 | liquid: engine 154 | } 155 | }) 156 | 157 | fastify.addHook('preHandler', function (_request, reply, done) { 158 | reply.locals = localsData 159 | done() 160 | }) 161 | 162 | fastify.get('/', (_req, reply) => { 163 | reply.view('./templates/index.liquid', {}) 164 | }) 165 | 166 | await fastify.listen({ port: 0 }) 167 | 168 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 169 | const responseContent = await result.text() 170 | 171 | t.assert.strictEqual(result.status, 200) 172 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 173 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 174 | 175 | const html = await engine.renderFile('./templates/index.liquid', localsData) 176 | t.assert.strictEqual(html, responseContent) 177 | 178 | await fastify.close() 179 | }) 180 | 181 | test('reply.view for liquid engine without defaultContext but with reply.locals and data-parameter', async t => { 182 | t.plan(4) 183 | const fastify = Fastify() 184 | const { Liquid } = require('liquidjs') 185 | const localsData = { text: 'text from locals' } 186 | const data = { text: 'text' } 187 | 188 | const engine = new Liquid() 189 | 190 | fastify.register(require('../index'), { 191 | engine: { 192 | liquid: engine 193 | } 194 | }) 195 | 196 | fastify.addHook('preHandler', function (_request, reply, done) { 197 | reply.locals = localsData 198 | done() 199 | }) 200 | 201 | fastify.get('/', (_req, reply) => { 202 | reply.view('./templates/index.liquid', data) 203 | }) 204 | 205 | await fastify.listen({ port: 0 }) 206 | 207 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 208 | const responseContent = await result.text() 209 | 210 | t.assert.strictEqual(result.status, 200) 211 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 212 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 213 | 214 | const html = await engine.renderFile('./templates/index.liquid', data) 215 | t.assert.strictEqual(html, responseContent) 216 | 217 | await fastify.close() 218 | }) 219 | 220 | test('reply.view for liquid engine without data-parameter but with reply.locals and defaultContext', async t => { 221 | t.plan(4) 222 | const fastify = Fastify() 223 | const { Liquid } = require('liquidjs') 224 | const localsData = { text: 'text from locals' } 225 | const defaultContext = { text: 'text' } 226 | 227 | const engine = new Liquid() 228 | 229 | fastify.register(require('../index'), { 230 | engine: { 231 | liquid: engine 232 | }, 233 | defaultContext 234 | }) 235 | 236 | fastify.addHook('preHandler', function (_request, reply, done) { 237 | reply.locals = localsData 238 | done() 239 | }) 240 | 241 | fastify.get('/', (_req, reply) => { 242 | reply.view('./templates/index.liquid') 243 | }) 244 | 245 | await fastify.listen({ port: 0 }) 246 | 247 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 248 | const responseContent = await result.text() 249 | 250 | t.assert.strictEqual(result.status, 200) 251 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 252 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 253 | 254 | const html = await engine.renderFile('./templates/index.liquid', localsData) 255 | t.assert.strictEqual(html, responseContent) 256 | 257 | await fastify.close() 258 | }) 259 | 260 | test('reply.view for liquid engine with data-parameter and reply.locals and defaultContext', async t => { 261 | t.plan(4) 262 | const fastify = Fastify() 263 | const { Liquid } = require('liquidjs') 264 | const localsData = { text: 'text from locals' } 265 | const defaultContext = { text: 'text from context' } 266 | const data = { text: 'text' } 267 | 268 | const engine = new Liquid() 269 | 270 | fastify.register(require('../index'), { 271 | engine: { 272 | liquid: engine 273 | }, 274 | defaultContext 275 | }) 276 | 277 | fastify.addHook('preHandler', function (_request, reply, done) { 278 | reply.locals = localsData 279 | done() 280 | }) 281 | 282 | fastify.get('/', (_req, reply) => { 283 | reply.view('./templates/index.liquid', data) 284 | }) 285 | 286 | await fastify.listen({ port: 0 }) 287 | 288 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 289 | const responseContent = await result.text() 290 | 291 | t.assert.strictEqual(result.status, 200) 292 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 293 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 294 | 295 | const html = await engine.renderFile('./templates/index.liquid', data) 296 | t.assert.strictEqual(html, responseContent) 297 | 298 | await fastify.close() 299 | }) 300 | 301 | test('reply.view with liquid engine and custom tag', async t => { 302 | t.plan(4) 303 | const fastify = Fastify() 304 | const { Liquid } = require('liquidjs') 305 | const data = { text: 'text' } 306 | 307 | const engine = new Liquid() 308 | 309 | engine.registerTag('header', { 310 | parse: function (token) { 311 | const [key, val] = token.args.split(':') 312 | this[key] = val 313 | }, 314 | render: async function (scope, emitter) { 315 | const title = await this.liquid.evalValue(this.content, scope) 316 | emitter.write(`

${title}

`) 317 | } 318 | }) 319 | 320 | fastify.register(require('../index'), { 321 | engine: { 322 | liquid: engine 323 | } 324 | }) 325 | 326 | fastify.get('/', (_req, reply) => { 327 | reply.view('./templates/index-with-custom-tag.liquid', data) 328 | }) 329 | 330 | await fastify.listen({ port: 0 }) 331 | 332 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 333 | const responseContent = await result.text() 334 | 335 | t.assert.strictEqual(result.status, 200) 336 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 337 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 338 | 339 | const html = await engine.renderFile('./templates/index-with-custom-tag.liquid', data) 340 | t.assert.strictEqual(html, responseContent) 341 | 342 | await fastify.close() 343 | }) 344 | 345 | test('reply.view with liquid engine and double quoted variable', async t => { 346 | t.plan(4) 347 | const fastify = Fastify() 348 | const { Liquid } = require('liquidjs') 349 | const data = { text: 'foo' } 350 | 351 | const engine = new Liquid() 352 | 353 | fastify.register(require('../index'), { 354 | engine: { 355 | liquid: engine 356 | } 357 | }) 358 | 359 | fastify.get('/', (_req, reply) => { 360 | reply.view('./templates/double-quotes-variable.liquid', data) 361 | }) 362 | 363 | await fastify.listen({ port: 0 }) 364 | 365 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 366 | const responseContent = await result.text() 367 | 368 | t.assert.strictEqual(result.status, 200) 369 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 370 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 371 | 372 | const html = await engine.renderFile('./templates/double-quotes-variable.liquid', data) 373 | t.assert.strictEqual(html, responseContent) 374 | 375 | await fastify.close() 376 | }) 377 | 378 | test('fastify.view with liquid engine, should throw page missing', (t, end) => { 379 | t.plan(3) 380 | const fastify = Fastify() 381 | const { Liquid } = require('liquidjs') 382 | const engine = new Liquid() 383 | 384 | fastify.register(require('../index'), { 385 | engine: { 386 | liquid: engine 387 | } 388 | }) 389 | 390 | fastify.ready(err => { 391 | t.assert.ifError(err) 392 | 393 | fastify.view(null, {}, err => { 394 | t.assert.ok(err instanceof Error) 395 | t.assert.strictEqual(err.message, 'Missing page') 396 | fastify.close() 397 | end() 398 | }) 399 | }) 400 | }) 401 | 402 | test('fastify.view with liquid engine template that does not exist errors correctly', (t, end) => { 403 | t.plan(3) 404 | const fastify = Fastify() 405 | const { Liquid } = require('liquidjs') 406 | const engine = new Liquid() 407 | 408 | fastify.register(require('../index'), { 409 | engine: { 410 | liquid: engine 411 | } 412 | }) 413 | 414 | fastify.ready(err => { 415 | t.assert.ifError(err) 416 | 417 | fastify.view('./I-Dont-Exist', {}, err => { 418 | t.assert.ok(err instanceof Error) 419 | t.assert.match(err.message, /ENOENT/) 420 | fastify.close() 421 | end() 422 | }) 423 | }) 424 | }) 425 | 426 | test('reply.view with liquid engine and raw template', async t => { 427 | t.plan(4) 428 | const fastify = Fastify() 429 | const { Liquid } = require('liquidjs') 430 | const data = { text: 'text' } 431 | 432 | const engine = new Liquid() 433 | 434 | fastify.register(require('../index'), { 435 | engine: { 436 | liquid: engine 437 | } 438 | }) 439 | 440 | fastify.get('/', (_req, reply) => { 441 | reply.view({ raw: fs.readFileSync('./templates/index.liquid', 'utf8') }, data) 442 | }) 443 | 444 | await fastify.listen({ port: 0 }) 445 | 446 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 447 | const responseContent = await result.text() 448 | 449 | t.assert.strictEqual(result.status, 200) 450 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 451 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 452 | 453 | const html = await engine.renderFile('./templates/index.liquid', data) 454 | t.assert.strictEqual(html, responseContent) 455 | 456 | await fastify.close() 457 | }) 458 | 459 | test('reply.view with liquid engine and function template', async t => { 460 | t.plan(4) 461 | const fastify = Fastify() 462 | const { Liquid } = require('liquidjs') 463 | const data = { text: 'text' } 464 | 465 | const engine = new Liquid() 466 | 467 | fastify.register(require('../index'), { 468 | engine: { 469 | liquid: engine 470 | } 471 | }) 472 | 473 | fastify.get('/', (_req, reply) => { 474 | reply.header('Content-Type', 'text/html').view(engine.renderFile.bind(engine, './templates/index.liquid'), data) 475 | }) 476 | 477 | await fastify.listen({ port: 0 }) 478 | 479 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 480 | const responseContent = await result.text() 481 | 482 | t.assert.strictEqual(result.status, 200) 483 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 484 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html') 485 | 486 | const html = await engine.renderFile('./templates/index.liquid', data) 487 | t.assert.strictEqual(html, responseContent) 488 | 489 | await fastify.close() 490 | }) 491 | 492 | test('reply.view with liquid engine and unknown template type', async t => { 493 | t.plan(1) 494 | const fastify = Fastify() 495 | const { Liquid } = require('liquidjs') 496 | const data = { text: 'text' } 497 | 498 | const engine = new Liquid() 499 | 500 | fastify.register(require('../index'), { 501 | engine: { 502 | liquid: engine 503 | } 504 | }) 505 | 506 | fastify.get('/', (_req, reply) => { 507 | reply.view({ }, data) 508 | }) 509 | 510 | await fastify.listen({ port: 0 }) 511 | 512 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 513 | 514 | t.assert.strictEqual(result.status, 500) 515 | 516 | await fastify.close() 517 | }) 518 | -------------------------------------------------------------------------------- /test/test-nunjucks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fs = require('node:fs') 5 | const Fastify = require('fastify') 6 | const path = require('node:path') 7 | 8 | require('./helper').nunjucksHtmlMinifierTests(true) 9 | require('./helper').nunjucksHtmlMinifierTests(false) 10 | 11 | test('reply.view with nunjucks engine and custom templates folder', async t => { 12 | t.plan(4) 13 | const fastify = Fastify() 14 | const nunjucks = require('nunjucks') 15 | const data = { text: 'text' } 16 | 17 | fastify.register(require('../index'), { 18 | engine: { 19 | nunjucks 20 | }, 21 | templates: 'templates' 22 | }) 23 | 24 | fastify.get('/', (_req, reply) => { 25 | reply.view('index.njk', data) 26 | }) 27 | 28 | await fastify.listen({ port: 0 }) 29 | 30 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 31 | 32 | const responseContent = await result.text() 33 | 34 | t.assert.strictEqual(result.status, 200) 35 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 36 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 37 | t.assert.strictEqual(nunjucks.render('index.njk', data), responseContent) 38 | 39 | await fastify.close() 40 | }) 41 | 42 | test('reply.view with nunjucks engine and custom templates array of folders', async t => { 43 | t.plan(4) 44 | const fastify = Fastify() 45 | const nunjucks = require('nunjucks') 46 | const data = { text: 'text' } 47 | 48 | fastify.register(require('../index'), { 49 | engine: { 50 | nunjucks 51 | }, 52 | templates: [ 53 | 'templates/nunjucks-layout', 54 | 'templates/nunjucks-template' 55 | ] 56 | }) 57 | 58 | fastify.get('/', (_req, reply) => { 59 | reply.view('index.njk', data) 60 | }) 61 | 62 | await fastify.listen({ port: 0 }) 63 | 64 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 65 | 66 | const responseContent = await result.text() 67 | 68 | t.assert.strictEqual(result.status, 200) 69 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 70 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 71 | t.assert.strictEqual(nunjucks.render('index.njk', data), responseContent) 72 | 73 | await fastify.close() 74 | }) 75 | 76 | test('reply.view for nunjucks engine without data-parameter but defaultContext', async t => { 77 | t.plan(4) 78 | const fastify = Fastify() 79 | const nunjucks = require('nunjucks') 80 | const data = { text: 'text' } 81 | 82 | fastify.register(require('../index'), { 83 | engine: { 84 | nunjucks 85 | }, 86 | defaultContext: data 87 | }) 88 | 89 | fastify.get('/', (_req, reply) => { 90 | reply.view('./templates/index.njk') 91 | }) 92 | 93 | await fastify.listen({ port: 0 }) 94 | 95 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 96 | 97 | const responseContent = await result.text() 98 | 99 | t.assert.strictEqual(result.status, 200) 100 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 101 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 102 | t.assert.strictEqual(nunjucks.render('./templates/index.njk', data), responseContent) 103 | 104 | await fastify.close() 105 | }) 106 | 107 | test('reply.view for nunjucks engine without data-parameter and without defaultContext', async t => { 108 | t.plan(4) 109 | const fastify = Fastify() 110 | const nunjucks = require('nunjucks') 111 | 112 | fastify.register(require('../index'), { 113 | engine: { 114 | nunjucks 115 | } 116 | }) 117 | 118 | fastify.get('/', (_req, reply) => { 119 | reply.view('./templates/index.njk') 120 | }) 121 | 122 | await fastify.listen({ port: 0 }) 123 | 124 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 125 | 126 | const responseContent = await result.text() 127 | 128 | t.assert.strictEqual(result.status, 200) 129 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 130 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 131 | t.assert.strictEqual(nunjucks.render('./templates/index.njk'), responseContent) 132 | 133 | await fastify.close() 134 | }) 135 | 136 | test('reply.view for nunjucks engine without data-parameter and defaultContext but with reply.locals', async t => { 137 | t.plan(4) 138 | const fastify = Fastify() 139 | const nunjucks = require('nunjucks') 140 | const localsData = { text: 'text from locals' } 141 | 142 | fastify.register(require('../index'), { 143 | engine: { 144 | nunjucks 145 | } 146 | }) 147 | 148 | fastify.addHook('preHandler', function (_request, reply, done) { 149 | reply.locals = localsData 150 | done() 151 | }) 152 | 153 | fastify.get('/', (_req, reply) => { 154 | reply.view('./templates/index.njk') 155 | }) 156 | 157 | await fastify.listen({ port: 0 }) 158 | 159 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 160 | 161 | const responseContent = await result.text() 162 | 163 | t.assert.strictEqual(result.status, 200) 164 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 165 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 166 | t.assert.strictEqual(nunjucks.render('./templates/index.njk', localsData), responseContent) 167 | 168 | await fastify.close() 169 | }) 170 | 171 | test('reply.view for nunjucks engine without defaultContext but with reply.locals and data-parameter', async t => { 172 | t.plan(4) 173 | const fastify = Fastify() 174 | const nunjucks = require('nunjucks') 175 | const localsData = { text: 'text from locals' } 176 | const data = { text: 'text' } 177 | 178 | fastify.register(require('../index'), { 179 | engine: { 180 | nunjucks 181 | } 182 | }) 183 | 184 | fastify.addHook('preHandler', function (_request, reply, done) { 185 | reply.locals = localsData 186 | done() 187 | }) 188 | 189 | fastify.get('/', (_req, reply) => { 190 | reply.view('./templates/index.njk', data) 191 | }) 192 | 193 | await fastify.listen({ port: 0 }) 194 | 195 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 196 | 197 | const responseContent = await result.text() 198 | 199 | t.assert.strictEqual(result.status, 200) 200 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 201 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 202 | t.assert.strictEqual(nunjucks.render('./templates/index.njk', data), responseContent) 203 | 204 | await fastify.close() 205 | }) 206 | 207 | test('reply.view for nunjucks engine without data-parameter but with reply.locals and defaultContext', async t => { 208 | t.plan(4) 209 | const fastify = Fastify() 210 | const nunjucks = require('nunjucks') 211 | const localsData = { text: 'text from locals' } 212 | const contextData = { text: 'text from context' } 213 | 214 | fastify.register(require('../index'), { 215 | engine: { 216 | nunjucks 217 | }, 218 | defaultContext: contextData 219 | }) 220 | 221 | fastify.addHook('preHandler', function (_request, reply, done) { 222 | reply.locals = localsData 223 | done() 224 | }) 225 | 226 | fastify.get('/', (_req, reply) => { 227 | reply.view('./templates/index.njk') 228 | }) 229 | 230 | await fastify.listen({ port: 0 }) 231 | 232 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 233 | 234 | const responseContent = await result.text() 235 | 236 | t.assert.strictEqual(result.status, 200) 237 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 238 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 239 | t.assert.strictEqual(nunjucks.render('./templates/index.njk', localsData), responseContent) 240 | 241 | await fastify.close() 242 | }) 243 | 244 | test('reply.view for nunjucks engine with data-parameter and reply.locals and defaultContext', async t => { 245 | t.plan(4) 246 | const fastify = Fastify() 247 | const nunjucks = require('nunjucks') 248 | const localsData = { text: 'text from locals' } 249 | const contextData = { text: 'text from context' } 250 | const data = { text: 'text' } 251 | 252 | fastify.register(require('../index'), { 253 | engine: { 254 | nunjucks 255 | }, 256 | defaultContext: contextData 257 | }) 258 | 259 | fastify.addHook('preHandler', function (_request, reply, done) { 260 | reply.locals = localsData 261 | done() 262 | }) 263 | 264 | fastify.get('/', (_req, reply) => { 265 | reply.view('./templates/index.njk', data) 266 | }) 267 | 268 | await fastify.listen({ port: 0 }) 269 | 270 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 271 | 272 | const responseContent = await result.text() 273 | 274 | t.assert.strictEqual(result.status, 200) 275 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 276 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 277 | t.assert.strictEqual(nunjucks.render('./templates/index.njk', data), responseContent) 278 | 279 | await fastify.close() 280 | }) 281 | 282 | test('reply.view with nunjucks engine, will preserve content-type', async t => { 283 | t.plan(4) 284 | const fastify = Fastify() 285 | const nunjucks = require('nunjucks') 286 | const data = { text: 'text' } 287 | 288 | fastify.register(require('../index'), { 289 | engine: { 290 | nunjucks 291 | } 292 | }) 293 | 294 | fastify.get('/', (_req, reply) => { 295 | reply.header('Content-Type', 'text/xml') 296 | reply.view('./templates/index.njk', data) 297 | }) 298 | 299 | await fastify.listen({ port: 0 }) 300 | 301 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 302 | 303 | const responseContent = await result.text() 304 | 305 | t.assert.strictEqual(result.status, 200) 306 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 307 | t.assert.strictEqual(result.headers.get('content-type'), 'text/xml') 308 | t.assert.strictEqual(nunjucks.render('./templates/index.njk', data), responseContent) 309 | 310 | await fastify.close() 311 | }) 312 | 313 | test('reply.view with nunjucks engine and full path templates folder', async t => { 314 | t.plan(4) 315 | const fastify = Fastify() 316 | const nunjucks = require('nunjucks') 317 | const data = { text: 'text' } 318 | 319 | fastify.register(require('../index'), { 320 | engine: { 321 | nunjucks 322 | }, 323 | templates: path.join(__dirname, '../templates') 324 | }) 325 | 326 | fastify.get('/', (_req, reply) => { 327 | reply.view('./index.njk', data) 328 | }) 329 | 330 | await fastify.listen({ port: 0 }) 331 | 332 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 333 | 334 | const responseContent = await result.text() 335 | 336 | t.assert.strictEqual(result.status, 200) 337 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 338 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 339 | // Global Nunjucks templates dir changed here. 340 | t.assert.strictEqual(nunjucks.render('./index.njk', data), responseContent) 341 | 342 | await fastify.close() 343 | }) 344 | 345 | test('reply.view with nunjucks engine and includeViewExtension is true', async t => { 346 | t.plan(4) 347 | const fastify = Fastify() 348 | const nunjucks = require('nunjucks') 349 | const data = { text: 'text' } 350 | 351 | fastify.register(require('../index'), { 352 | engine: { 353 | nunjucks 354 | }, 355 | includeViewExtension: true 356 | }) 357 | 358 | fastify.get('/', (_req, reply) => { 359 | reply.view('./templates/index', data) 360 | }) 361 | 362 | await fastify.listen({ port: 0 }) 363 | 364 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 365 | 366 | const responseContent = await result.text() 367 | 368 | t.assert.strictEqual(result.status, 200) 369 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 370 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 371 | // Global Nunjucks templates dir is `./` here. 372 | t.assert.strictEqual(nunjucks.render('./templates/index.njk', data), responseContent) 373 | 374 | await fastify.close() 375 | }) 376 | 377 | test('reply.view with nunjucks engine using onConfigure callback', async t => { 378 | t.plan(5) 379 | const fastify = Fastify() 380 | const nunjucks = require('nunjucks') 381 | const data = { text: 'text' } 382 | 383 | fastify.register(require('../index'), { 384 | engine: { 385 | nunjucks 386 | }, 387 | options: { 388 | onConfigure: env => { 389 | env.addGlobal('myGlobalVar', 'my global var value') 390 | } 391 | } 392 | }) 393 | 394 | fastify.get('/', (_req, reply) => { 395 | reply.view('./templates/index-with-global.njk', data) 396 | }) 397 | 398 | await fastify.listen({ port: 0 }) 399 | 400 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 401 | 402 | const responseContent = await result.text() 403 | 404 | t.assert.strictEqual(result.status, 200) 405 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 406 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 407 | // Global Nunjucks templates dir is `./` here. 408 | t.assert.strictEqual(nunjucks.render('./templates/index-with-global.njk', data), responseContent) 409 | t.assert.match(responseContent, /.*

my global var value<\/p>/) 410 | 411 | await fastify.close() 412 | }) 413 | 414 | test('fastify.view with nunjucks engine', (t, end) => { 415 | t.plan(6) 416 | const fastify = Fastify() 417 | const nunjucks = require('nunjucks') 418 | const data = { text: 'text' } 419 | 420 | fastify.register(require('../index'), { 421 | engine: { 422 | nunjucks 423 | } 424 | }) 425 | 426 | fastify.ready(err => { 427 | t.assert.ifError(err) 428 | 429 | fastify.view('templates/index.njk', data, (err, compiled) => { 430 | t.assert.ifError(err) 431 | t.assert.strictEqual(nunjucks.render('./templates/index.njk', data), compiled) 432 | 433 | fastify.ready(err => { 434 | t.assert.ifError(err) 435 | 436 | fastify.view('templates/index.njk', data, (err, compiled) => { 437 | t.assert.ifError(err) 438 | t.assert.strictEqual(nunjucks.render('./templates/index.njk', data), compiled) 439 | fastify.close() 440 | end() 441 | }) 442 | }) 443 | }) 444 | }) 445 | }) 446 | 447 | test('fastify.view with nunjucks should throw page missing', (t, end) => { 448 | t.plan(3) 449 | const fastify = Fastify() 450 | const nunjucks = require('nunjucks') 451 | 452 | fastify.register(require('../index'), { 453 | engine: { 454 | nunjucks 455 | } 456 | }) 457 | 458 | fastify.ready(err => { 459 | t.assert.ifError(err) 460 | 461 | fastify.view(null, {}, err => { 462 | t.assert.ok(err instanceof Error) 463 | t.assert.strictEqual(err.message, 'Missing page') 464 | fastify.close() 465 | end() 466 | }) 467 | }) 468 | }) 469 | 470 | test('fastify.view with nunjucks engine should return 500 if render fails', async t => { 471 | t.plan(2) 472 | const fastify = Fastify() 473 | const nunjucks = { 474 | configure: () => ({ 475 | render: (_, __, callback) => { callback(Error('Render Error')) } 476 | }) 477 | } 478 | 479 | fastify.register(require('../index'), { 480 | engine: { 481 | nunjucks 482 | } 483 | }) 484 | 485 | fastify.get('/', (_req, reply) => { 486 | reply.view('./templates/index.njk') 487 | }) 488 | 489 | await fastify.listen({ port: 0 }) 490 | 491 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 492 | 493 | const responseContent = await result.text() 494 | 495 | t.assert.strictEqual(result.status, 500) 496 | t.assert.strictEqual(JSON.parse(responseContent).message, 'Render Error') 497 | 498 | await fastify.close() 499 | }) 500 | 501 | test('reply.view with nunjucks engine and raw template', async t => { 502 | t.plan(4) 503 | const fastify = Fastify() 504 | const nunjucks = require('nunjucks') 505 | const data = { text: 'text' } 506 | 507 | fastify.register(require('../index'), { 508 | engine: { 509 | nunjucks 510 | }, 511 | templates: 'templates' 512 | }) 513 | 514 | fastify.get('/', (_req, reply) => { 515 | reply.view({ raw: fs.readFileSync('./templates/index.njk') }, data) 516 | }) 517 | 518 | await fastify.listen({ port: 0 }) 519 | 520 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 521 | 522 | const responseContent = await result.text() 523 | 524 | t.assert.strictEqual(result.status, 200) 525 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 526 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 527 | t.assert.strictEqual(nunjucks.render('index.njk', data), responseContent) 528 | 529 | await fastify.close() 530 | }) 531 | 532 | test('reply.view with nunjucks engine and function template', async t => { 533 | t.plan(4) 534 | const fastify = Fastify() 535 | const nunjucks = require('nunjucks') 536 | const data = { text: 'text' } 537 | 538 | fastify.register(require('../index'), { 539 | engine: { 540 | nunjucks 541 | }, 542 | templates: 'templates' 543 | }) 544 | 545 | fastify.get('/', (_req, reply) => { 546 | reply.view(nunjucks.compile(fs.readFileSync('./templates/index.njk').toString()), data) 547 | }) 548 | 549 | await fastify.listen({ port: 0 }) 550 | 551 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 552 | 553 | const responseContent = await result.text() 554 | 555 | t.assert.strictEqual(result.status, 200) 556 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 557 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 558 | t.assert.strictEqual(nunjucks.render('index.njk', data), responseContent) 559 | 560 | await fastify.close() 561 | }) 562 | 563 | test('reply.view with nunjucks engine and unknown template type', async t => { 564 | t.plan(1) 565 | const fastify = Fastify() 566 | const nunjucks = require('nunjucks') 567 | const data = { text: 'text' } 568 | 569 | fastify.register(require('../index'), { 570 | engine: { 571 | nunjucks 572 | }, 573 | templates: 'templates' 574 | }) 575 | 576 | fastify.get('/', (_req, reply) => { 577 | reply.view({ }, data) 578 | }) 579 | 580 | await fastify.listen({ port: 0 }) 581 | 582 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 583 | 584 | t.assert.strictEqual(result.status, 500) 585 | 586 | await fastify.close() 587 | }) 588 | -------------------------------------------------------------------------------- /test/test-pug.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const fs = require('node:fs') 6 | 7 | require('./helper').pugHtmlMinifierTests(true) 8 | require('./helper').pugHtmlMinifierTests(false) 9 | 10 | test('reply.view with pug engine', async t => { 11 | t.plan(4) 12 | const fastify = Fastify() 13 | const pug = require('pug') 14 | const data = { text: 'text' } 15 | 16 | fastify.register(require('../index'), { 17 | engine: { 18 | pug 19 | } 20 | }) 21 | 22 | fastify.get('/', (_req, reply) => { 23 | reply.view('./templates/index.pug', data) 24 | }) 25 | 26 | await fastify.listen({ port: 0 }) 27 | 28 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 29 | 30 | const responseContent = await result.text() 31 | 32 | t.assert.strictEqual(result.status, 200) 33 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 34 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 35 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), data), responseContent) 36 | 37 | await fastify.close() 38 | }) 39 | 40 | test('reply.view with pug engine in production mode should use cache', async t => { 41 | t.plan(4) 42 | const fastify = Fastify() 43 | const pug = require('pug') 44 | const POV = require('..') 45 | 46 | fastify.decorate(POV.fastifyViewCache, { 47 | get: () => { 48 | return () => '

Cached Response
' 49 | }, 50 | set: () => { } 51 | }) 52 | 53 | fastify.register(POV, { 54 | engine: { 55 | pug 56 | }, 57 | production: true 58 | }) 59 | 60 | fastify.get('/', (_req, reply) => { 61 | reply.view('./templates/index.pug') 62 | }) 63 | 64 | await fastify.listen({ port: 0 }) 65 | 66 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 67 | 68 | const responseContent = await result.text() 69 | 70 | t.assert.strictEqual(result.status, 200) 71 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 72 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 73 | t.assert.strictEqual('
Cached Response
', responseContent) 74 | 75 | await fastify.close() 76 | }) 77 | 78 | test('reply.view with pug engine and includes', async t => { 79 | t.plan(4) 80 | const fastify = Fastify() 81 | const pug = require('pug') 82 | const data = { text: 'text' } 83 | 84 | fastify.register(require('../index'), { 85 | engine: { 86 | pug 87 | } 88 | }) 89 | 90 | fastify.get('/', (_req, reply) => { 91 | reply.view('./templates/sample.pug', data) 92 | }) 93 | 94 | await fastify.listen({ port: 0 }) 95 | 96 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 97 | 98 | const responseContent = await result.text() 99 | 100 | t.assert.strictEqual(result.status, 200) 101 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 102 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 103 | t.assert.strictEqual(pug.renderFile('./templates/sample.pug', data), responseContent) 104 | 105 | await fastify.close() 106 | }) 107 | 108 | test('reply.view for pug without data-parameter but defaultContext', async t => { 109 | t.plan(4) 110 | const fastify = Fastify() 111 | const pug = require('pug') 112 | const data = { text: 'text' } 113 | 114 | fastify.register(require('../index'), { 115 | engine: { 116 | pug 117 | }, 118 | defaultContext: data 119 | }) 120 | 121 | fastify.get('/', (_req, reply) => { 122 | reply.view('./templates/index.pug') 123 | }) 124 | 125 | await fastify.listen({ port: 0 }) 126 | 127 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 128 | 129 | const responseContent = await result.text() 130 | 131 | t.assert.strictEqual(result.status, 200) 132 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 133 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 134 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), data), responseContent) 135 | 136 | await fastify.close() 137 | }) 138 | 139 | test('reply.view for pug without data-parameter and without defaultContext', async t => { 140 | t.plan(4) 141 | const fastify = Fastify() 142 | const pug = require('pug') 143 | 144 | fastify.register(require('../index'), { 145 | engine: { 146 | pug 147 | } 148 | }) 149 | 150 | fastify.get('/', (_req, reply) => { 151 | reply.view('./templates/index.pug') 152 | }) 153 | 154 | await fastify.listen({ port: 0 }) 155 | 156 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 157 | 158 | const responseContent = await result.text() 159 | 160 | t.assert.strictEqual(result.status, 200) 161 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 162 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 163 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8')), responseContent) 164 | 165 | await fastify.close() 166 | }) 167 | 168 | test('reply.view with pug engine and defaultContext', async t => { 169 | t.plan(4) 170 | const fastify = Fastify() 171 | const pug = require('pug') 172 | const data = { text: 'text' } 173 | 174 | fastify.register(require('../index'), { 175 | engine: { 176 | pug 177 | }, 178 | defaultContext: data 179 | }) 180 | 181 | fastify.get('/', (_req, reply) => { 182 | reply.view('./templates/index.pug', {}) 183 | }) 184 | 185 | await fastify.listen({ port: 0 }) 186 | 187 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 188 | 189 | const responseContent = await result.text() 190 | 191 | t.assert.strictEqual(result.status, 200) 192 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 193 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 194 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), data), responseContent) 195 | 196 | await fastify.close() 197 | }) 198 | 199 | test('reply.view for pug engine without data-parameter and defaultContext but with reply.locals', async t => { 200 | t.plan(4) 201 | const fastify = Fastify() 202 | const pug = require('pug') 203 | const localsData = { text: 'text from locals' } 204 | 205 | fastify.register(require('../index'), { 206 | engine: { 207 | pug 208 | } 209 | }) 210 | 211 | fastify.addHook('preHandler', function (_request, reply, done) { 212 | reply.locals = localsData 213 | done() 214 | }) 215 | 216 | fastify.get('/', (_req, reply) => { 217 | reply.view('./templates/index.pug') 218 | }) 219 | 220 | await fastify.listen({ port: 0 }) 221 | 222 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 223 | 224 | const responseContent = await result.text() 225 | 226 | t.assert.strictEqual(result.status, 200) 227 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 228 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 229 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), localsData), responseContent) 230 | 231 | await fastify.close() 232 | }) 233 | 234 | test('reply.view for pug engine without defaultContext but with reply.locals and data-parameter', async t => { 235 | t.plan(4) 236 | const fastify = Fastify() 237 | const pug = require('pug') 238 | const localsData = { text: 'text from locals' } 239 | const data = { text: 'text' } 240 | 241 | fastify.register(require('../index'), { 242 | engine: { 243 | pug 244 | } 245 | }) 246 | 247 | fastify.addHook('preHandler', function (_request, reply, done) { 248 | reply.locals = localsData 249 | done() 250 | }) 251 | 252 | fastify.get('/', (_req, reply) => { 253 | reply.view('./templates/index.pug', data) 254 | }) 255 | 256 | await fastify.listen({ port: 0 }) 257 | 258 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 259 | 260 | const responseContent = await result.text() 261 | 262 | t.assert.strictEqual(result.status, 200) 263 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 264 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 265 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), data), responseContent) 266 | 267 | await fastify.close() 268 | }) 269 | 270 | test('reply.view for pug engine without data-parameter but with reply.locals and defaultContext', async t => { 271 | t.plan(4) 272 | const fastify = Fastify() 273 | const pug = require('pug') 274 | const localsData = { text: 'text from locals' } 275 | const contextData = { text: 'text from context' } 276 | 277 | fastify.register(require('../index'), { 278 | engine: { 279 | pug 280 | }, 281 | defaultContext: contextData 282 | }) 283 | 284 | fastify.addHook('preHandler', function (_request, reply, done) { 285 | reply.locals = localsData 286 | done() 287 | }) 288 | 289 | fastify.get('/', (_req, reply) => { 290 | reply.view('./templates/index.pug') 291 | }) 292 | 293 | await fastify.listen({ port: 0 }) 294 | 295 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 296 | 297 | const responseContent = await result.text() 298 | 299 | t.assert.strictEqual(result.status, 200) 300 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 301 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 302 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), localsData), responseContent) 303 | 304 | await fastify.close() 305 | }) 306 | 307 | test('reply.view for pug engine with data-parameter and reply.locals and defaultContext', async t => { 308 | t.plan(4) 309 | const fastify = Fastify() 310 | const pug = require('pug') 311 | const localsData = { text: 'text from locals' } 312 | const contextData = { text: 'text from context' } 313 | const data = { text: 'text' } 314 | 315 | fastify.register(require('../index'), { 316 | engine: { 317 | pug 318 | }, 319 | defaultContext: contextData 320 | }) 321 | 322 | fastify.addHook('preHandler', function (_request, reply, done) { 323 | reply.locals = localsData 324 | done() 325 | }) 326 | 327 | fastify.get('/', (_req, reply) => { 328 | reply.view('./templates/index.pug', data) 329 | }) 330 | 331 | await fastify.listen({ port: 0 }) 332 | 333 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 334 | 335 | const responseContent = await result.text() 336 | 337 | t.assert.strictEqual(result.status, 200) 338 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 339 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 340 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), data), responseContent) 341 | 342 | await fastify.close() 343 | }) 344 | 345 | test('reply.view with pug engine, will preserve content-type', async t => { 346 | t.plan(4) 347 | const fastify = Fastify() 348 | const pug = require('pug') 349 | const data = { text: 'text' } 350 | 351 | fastify.register(require('../index'), { 352 | engine: { 353 | pug 354 | } 355 | }) 356 | 357 | fastify.get('/', (_req, reply) => { 358 | reply.header('Content-Type', 'text/xml') 359 | reply.view('./templates/index.pug', data) 360 | }) 361 | 362 | await fastify.listen({ port: 0 }) 363 | 364 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 365 | 366 | const responseContent = await result.text() 367 | 368 | t.assert.strictEqual(result.status, 200) 369 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 370 | t.assert.strictEqual(result.headers.get('content-type'), 'text/xml') 371 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), data), responseContent) 372 | 373 | await fastify.close() 374 | }) 375 | 376 | test('fastify.view with pug engine, should throw page missing', (t, end) => { 377 | t.plan(3) 378 | const fastify = Fastify() 379 | const pug = require('pug') 380 | 381 | fastify.register(require('../index'), { 382 | engine: { 383 | pug 384 | } 385 | }) 386 | 387 | fastify.ready(err => { 388 | t.assert.ifError(err) 389 | 390 | fastify.view(null, {}, err => { 391 | t.assert.ok(err instanceof Error) 392 | t.assert.strictEqual(err.message, 'Missing page') 393 | fastify.close() 394 | end() 395 | }) 396 | }) 397 | }) 398 | 399 | test('reply.view with pug engine, should throw error if non existent template path', async t => { 400 | t.plan(3) 401 | const fastify = Fastify() 402 | const pug = require('pug') 403 | 404 | fastify.register(require('../index'), { 405 | engine: { 406 | pug 407 | }, 408 | templates: 'non-existent' 409 | }) 410 | 411 | fastify.get('/', (_req, reply) => { 412 | reply.view('./test/index.html') 413 | }) 414 | 415 | await fastify.listen({ port: 0 }) 416 | 417 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 418 | 419 | const responseContent = await result.text() 420 | 421 | t.assert.strictEqual(result.status, 500) 422 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 423 | t.assert.strictEqual(result.headers.get('content-type'), 'application/json; charset=utf-8') 424 | 425 | await fastify.close() 426 | }) 427 | 428 | test('reply.view with pug engine should return 500 if compile fails', async t => { 429 | t.plan(2) 430 | const fastify = Fastify() 431 | const pug = { 432 | compile: () => { throw Error('Compile Error') } 433 | } 434 | 435 | fastify.register(require('../index'), { 436 | engine: { 437 | pug 438 | } 439 | }) 440 | 441 | fastify.get('/', (_req, reply) => { 442 | reply.view('./templates/index.pug') 443 | }) 444 | 445 | await fastify.listen({ port: 0 }) 446 | 447 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 448 | 449 | const responseContent = await result.text() 450 | 451 | t.assert.strictEqual(result.status, 500) 452 | t.assert.strictEqual(JSON.parse(responseContent).message, 'Compile Error') 453 | 454 | await fastify.close() 455 | }) 456 | 457 | test('reply.view with pug engine and raw template', async t => { 458 | t.plan(4) 459 | const fastify = Fastify() 460 | const pug = require('pug') 461 | const data = { text: 'text' } 462 | 463 | fastify.register(require('../index'), { 464 | engine: { 465 | pug 466 | } 467 | }) 468 | 469 | fastify.get('/', (_req, reply) => { 470 | reply.view({ raw: fs.readFileSync('./templates/index.pug', 'utf8') }, data) 471 | }) 472 | 473 | await fastify.listen({ port: 0 }) 474 | 475 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 476 | 477 | const responseContent = await result.text() 478 | 479 | t.assert.strictEqual(result.status, 200) 480 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 481 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 482 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), data), responseContent) 483 | 484 | await fastify.close() 485 | }) 486 | 487 | test('reply.view with pug engine and function template', async t => { 488 | t.plan(4) 489 | const fastify = Fastify() 490 | const pug = require('pug') 491 | const data = { text: 'text' } 492 | 493 | fastify.register(require('../index'), { 494 | engine: { 495 | pug 496 | } 497 | }) 498 | 499 | fastify.get('/', (_req, reply) => { 500 | reply.view(pug.compile(fs.readFileSync('./templates/index.pug', 'utf8')), data) 501 | }) 502 | 503 | await fastify.listen({ port: 0 }) 504 | 505 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 506 | 507 | const responseContent = await result.text() 508 | 509 | t.assert.strictEqual(result.status, 200) 510 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 511 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 512 | t.assert.strictEqual(pug.render(fs.readFileSync('./templates/index.pug', 'utf8'), data), responseContent) 513 | 514 | await fastify.close() 515 | }) 516 | -------------------------------------------------------------------------------- /test/test-twig.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fs = require('node:fs') 5 | const Fastify = require('fastify') 6 | 7 | require('./helper').twigHtmlMinifierTests(true) 8 | require('./helper').twigHtmlMinifierTests(false) 9 | 10 | test('reply.view with twig engine', async t => { 11 | t.plan(5) 12 | const fastify = Fastify() 13 | const Twig = require('twig') 14 | const data = { title: 'fastify', text: 'text' } 15 | 16 | fastify.register(require('../index'), { 17 | engine: { 18 | twig: Twig 19 | } 20 | }) 21 | 22 | fastify.get('/', (_req, reply) => { 23 | reply.view('./templates/index.twig', data) 24 | }) 25 | 26 | await fastify.listen({ port: 0 }) 27 | 28 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 29 | 30 | const responseContent = await result.text() 31 | 32 | t.assert.strictEqual(result.status, 200) 33 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 34 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 35 | await new Promise(resolve => { 36 | Twig.renderFile('./templates/index.twig', data, (err, html) => { 37 | t.assert.ifError(err) 38 | t.assert.strictEqual(html, responseContent) 39 | resolve() 40 | }) 41 | }) 42 | 43 | await fastify.close() 44 | }) 45 | 46 | test('reply.view with twig engine and simple include', async t => { 47 | t.plan(5) 48 | const fastify = Fastify() 49 | const Twig = require('twig') 50 | const data = { title: 'fastify', text: 'text' } 51 | 52 | fastify.register(require('../index'), { 53 | engine: { 54 | twig: Twig 55 | } 56 | }) 57 | 58 | fastify.get('/', (_req, reply) => { 59 | reply.view('./templates/template.twig', data) 60 | }) 61 | 62 | await fastify.listen({ port: 0 }) 63 | 64 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 65 | 66 | const responseContent = await result.text() 67 | 68 | t.assert.strictEqual(result.status, 200) 69 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 70 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 71 | await new Promise(resolve => { 72 | Twig.renderFile('./templates/template.twig', data, (err, html) => { 73 | t.assert.ifError(err) 74 | t.assert.strictEqual(html, responseContent) 75 | resolve() 76 | }) 77 | }) 78 | 79 | await fastify.close() 80 | }) 81 | 82 | test('reply.view for twig without data-parameter but defaultContext', async t => { 83 | t.plan(5) 84 | const fastify = Fastify() 85 | const Twig = require('twig') 86 | const data = { title: 'fastify', text: 'text' } 87 | 88 | fastify.register(require('../index'), { 89 | engine: { 90 | twig: Twig 91 | }, 92 | defaultContext: data 93 | }) 94 | 95 | fastify.get('/', (_req, reply) => { 96 | reply.view('./templates/index.twig') 97 | }) 98 | 99 | await fastify.listen({ port: 0 }) 100 | 101 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 102 | 103 | const responseContent = await result.text() 104 | 105 | t.assert.strictEqual(result.status, 200) 106 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 107 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 108 | await new Promise(resolve => { 109 | Twig.renderFile('./templates/index.twig', data, (err, html) => { 110 | t.assert.ifError(err) 111 | t.assert.strictEqual(html, responseContent) 112 | resolve() 113 | }) 114 | }) 115 | 116 | await fastify.close() 117 | }) 118 | 119 | test('reply.view for twig without data-parameter and without defaultContext', async t => { 120 | t.plan(5) 121 | const fastify = Fastify() 122 | const Twig = require('twig') 123 | 124 | fastify.register(require('../index'), { 125 | engine: { 126 | twig: Twig 127 | } 128 | }) 129 | 130 | fastify.get('/', (_req, reply) => { 131 | reply.view('./templates/index.twig') 132 | }) 133 | 134 | await fastify.listen({ port: 0 }) 135 | 136 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 137 | 138 | const responseContent = await result.text() 139 | 140 | t.assert.strictEqual(result.status, 200) 141 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 142 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 143 | await new Promise(resolve => { 144 | Twig.renderFile('./templates/index.twig', (err, html) => { 145 | t.assert.ifError(err) 146 | t.assert.strictEqual(html, responseContent) 147 | resolve() 148 | }) 149 | }) 150 | 151 | await fastify.close() 152 | }) 153 | 154 | test('reply.view with twig engine and defaultContext', async t => { 155 | t.plan(5) 156 | const fastify = Fastify() 157 | const Twig = require('twig') 158 | const data = { title: 'fastify', text: 'text' } 159 | 160 | fastify.register(require('../index'), { 161 | engine: { 162 | twig: Twig 163 | }, 164 | defaultContext: data 165 | }) 166 | 167 | fastify.get('/', (_req, reply) => { 168 | reply.view('./templates/index.twig', {}) 169 | }) 170 | 171 | await fastify.listen({ port: 0 }) 172 | 173 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 174 | 175 | const responseContent = await result.text() 176 | 177 | t.assert.strictEqual(result.status, 200) 178 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 179 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 180 | await new Promise(resolve => { 181 | Twig.renderFile('./templates/index.twig', data, (err, html) => { 182 | t.assert.ifError(err) 183 | t.assert.strictEqual(html, responseContent) 184 | resolve() 185 | }) 186 | }) 187 | 188 | await fastify.close() 189 | }) 190 | 191 | test('reply.view for twig engine without data-parameter and defaultContext but with reply.locals', async t => { 192 | t.plan(5) 193 | const fastify = Fastify() 194 | const Twig = require('twig') 195 | const localsData = { text: 'text from locals' } 196 | 197 | fastify.register(require('../index'), { 198 | engine: { 199 | twig: Twig 200 | } 201 | }) 202 | 203 | fastify.addHook('preHandler', function (_request, reply, done) { 204 | reply.locals = localsData 205 | done() 206 | }) 207 | 208 | fastify.get('/', (_req, reply) => { 209 | reply.view('./templates/index.twig') 210 | }) 211 | 212 | await fastify.listen({ port: 0 }) 213 | 214 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 215 | 216 | const responseContent = await result.text() 217 | 218 | t.assert.strictEqual(result.status, 200) 219 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 220 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 221 | await new Promise(resolve => { 222 | Twig.renderFile('./templates/index.twig', localsData, (err, html) => { 223 | t.assert.ifError(err) 224 | t.assert.strictEqual(html, responseContent) 225 | resolve() 226 | }) 227 | }) 228 | 229 | await fastify.close() 230 | }) 231 | 232 | test('reply.view for twig engine without defaultContext but with reply.locals and data-parameter', async t => { 233 | t.plan(5) 234 | const fastify = Fastify() 235 | const Twig = require('twig') 236 | const localsData = { text: 'text from locals' } 237 | const data = { text: 'text' } 238 | 239 | fastify.register(require('../index'), { 240 | engine: { 241 | twig: Twig 242 | } 243 | }) 244 | 245 | fastify.addHook('preHandler', function (_request, reply, done) { 246 | reply.locals = localsData 247 | done() 248 | }) 249 | 250 | fastify.get('/', (_req, reply) => { 251 | reply.view('./templates/index.twig', data) 252 | }) 253 | 254 | await fastify.listen({ port: 0 }) 255 | 256 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 257 | 258 | const responseContent = await result.text() 259 | 260 | t.assert.strictEqual(result.status, 200) 261 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 262 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 263 | await new Promise(resolve => { 264 | Twig.renderFile('./templates/index.twig', data, (err, html) => { 265 | t.assert.ifError(err) 266 | t.assert.strictEqual(html, responseContent) 267 | resolve() 268 | }) 269 | }) 270 | 271 | await fastify.close() 272 | }) 273 | 274 | test('reply.view for twig engine without data-parameter but with reply.locals and defaultContext', async t => { 275 | t.plan(5) 276 | const fastify = Fastify() 277 | const Twig = require('twig') 278 | const localsData = { text: 'text from locals' } 279 | const contextData = { text: 'text from context' } 280 | 281 | fastify.register(require('../index'), { 282 | engine: { 283 | twig: Twig 284 | }, 285 | defaultContext: contextData 286 | }) 287 | 288 | fastify.addHook('preHandler', function (_request, reply, done) { 289 | reply.locals = localsData 290 | done() 291 | }) 292 | 293 | fastify.get('/', (_req, reply) => { 294 | reply.view('./templates/index.twig') 295 | }) 296 | 297 | await fastify.listen({ port: 0 }) 298 | 299 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 300 | 301 | const responseContent = await result.text() 302 | 303 | t.assert.strictEqual(result.status, 200) 304 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 305 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 306 | await new Promise(resolve => { 307 | Twig.renderFile('./templates/index.twig', localsData, (err, html) => { 308 | t.assert.ifError(err) 309 | t.assert.strictEqual(html, responseContent) 310 | resolve() 311 | }) 312 | }) 313 | 314 | await fastify.close() 315 | }) 316 | 317 | test('reply.view for twig engine with data-parameter and reply.locals and defaultContext', async t => { 318 | t.plan(5) 319 | const fastify = Fastify() 320 | const Twig = require('twig') 321 | const localsData = { text: 'text from locals' } 322 | const contextData = { text: 'text from context' } 323 | const data = { text: 'text' } 324 | 325 | fastify.register(require('../index'), { 326 | engine: { 327 | twig: Twig 328 | }, 329 | defaultContext: contextData 330 | }) 331 | 332 | fastify.addHook('preHandler', function (_request, reply, done) { 333 | reply.locals = localsData 334 | done() 335 | }) 336 | 337 | fastify.get('/', (_req, reply) => { 338 | reply.view('./templates/index.twig', data) 339 | }) 340 | 341 | await fastify.listen({ port: 0 }) 342 | 343 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 344 | 345 | const responseContent = await result.text() 346 | 347 | t.assert.strictEqual(result.status, 200) 348 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 349 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 350 | await new Promise(resolve => { 351 | Twig.renderFile('./templates/index.twig', data, (err, html) => { 352 | t.assert.ifError(err) 353 | t.assert.strictEqual(html, responseContent) 354 | resolve() 355 | }) 356 | }) 357 | 358 | await fastify.close() 359 | }) 360 | 361 | test('reply.view with twig engine, will preserve content-type', async t => { 362 | t.plan(5) 363 | const fastify = Fastify() 364 | const Twig = require('twig') 365 | const data = { title: 'fastify', text: 'text' } 366 | 367 | fastify.register(require('../index'), { 368 | engine: { 369 | twig: Twig 370 | } 371 | }) 372 | 373 | fastify.get('/', (_req, reply) => { 374 | reply.header('Content-Type', 'text/xml') 375 | reply.view('./templates/index.twig', data) 376 | }) 377 | 378 | await fastify.listen({ port: 0 }) 379 | 380 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 381 | 382 | const responseContent = await result.text() 383 | 384 | t.assert.strictEqual(result.status, 200) 385 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 386 | t.assert.strictEqual(result.headers.get('content-type'), 'text/xml') 387 | await new Promise(resolve => { 388 | Twig.renderFile('./templates/index.twig', data, (err, html) => { 389 | t.assert.ifError(err) 390 | t.assert.strictEqual(html, responseContent) 391 | resolve() 392 | }) 393 | }) 394 | 395 | await fastify.close() 396 | }) 397 | 398 | test('fastify.view with twig engine, should throw page missing', (t, end) => { 399 | t.plan(3) 400 | const fastify = Fastify() 401 | const Twig = require('twig') 402 | 403 | fastify.register(require('../index'), { 404 | engine: { 405 | twig: Twig 406 | } 407 | }) 408 | 409 | fastify.ready(err => { 410 | t.assert.ifError(err) 411 | 412 | fastify.view(null, {}, err => { 413 | t.assert.ok(err instanceof Error) 414 | t.assert.strictEqual(err.message, 'Missing page') 415 | fastify.close() 416 | end() 417 | }) 418 | }) 419 | }) 420 | 421 | test('reply.view with twig engine should return 500 if renderFile fails', async t => { 422 | t.plan(2) 423 | const fastify = Fastify() 424 | const Twig = { 425 | renderFile: (_, __, callback) => { callback(Error('RenderFile Error')) } 426 | } 427 | 428 | fastify.register(require('../index'), { 429 | engine: { 430 | twig: Twig 431 | } 432 | }) 433 | 434 | fastify.get('/', (_req, reply) => { 435 | reply.view('./templates/index.twig') 436 | }) 437 | 438 | await fastify.listen({ port: 0 }) 439 | 440 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 441 | 442 | const responseContent = await result.text() 443 | 444 | t.assert.strictEqual(result.status, 500) 445 | t.assert.strictEqual(JSON.parse(responseContent).message, 'RenderFile Error') 446 | 447 | await fastify.close() 448 | }) 449 | 450 | test('reply.view with twig engine and raw template', async t => { 451 | t.plan(5) 452 | const fastify = Fastify() 453 | const Twig = require('twig') 454 | const data = { title: 'fastify', text: 'text' } 455 | 456 | fastify.register(require('../index'), { 457 | engine: { 458 | twig: Twig 459 | } 460 | }) 461 | 462 | fastify.get('/', (_req, reply) => { 463 | reply.view({ raw: fs.readFileSync('./templates/index.twig') }, data) 464 | }) 465 | 466 | await fastify.listen({ port: 0 }) 467 | 468 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 469 | 470 | const responseContent = await result.text() 471 | 472 | t.assert.strictEqual(result.status, 200) 473 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 474 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 475 | await new Promise(resolve => { 476 | Twig.renderFile('./templates/index.twig', data, (err, html) => { 477 | t.assert.ifError(err) 478 | t.assert.strictEqual(html, responseContent) 479 | resolve() 480 | }) 481 | }) 482 | 483 | await fastify.close() 484 | }) 485 | 486 | test('reply.view with twig engine and function template', async t => { 487 | t.plan(5) 488 | const fastify = Fastify() 489 | const Twig = require('twig') 490 | const data = { title: 'fastify', text: 'text' } 491 | 492 | fastify.register(require('../index'), { 493 | engine: { 494 | twig: Twig 495 | } 496 | }) 497 | 498 | fastify.get('/', (_req, reply) => { 499 | reply.view(Twig.twig({ data: fs.readFileSync('./templates/index.twig').toString() }), data) 500 | }) 501 | 502 | await fastify.listen({ port: 0 }) 503 | 504 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 505 | 506 | const responseContent = await result.text() 507 | 508 | t.assert.strictEqual(result.status, 200) 509 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 510 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 511 | await new Promise(resolve => { 512 | Twig.renderFile('./templates/index.twig', data, (err, html) => { 513 | t.assert.ifError(err) 514 | t.assert.strictEqual(html, responseContent) 515 | resolve() 516 | }) 517 | }) 518 | 519 | await fastify.close() 520 | }) 521 | 522 | test('reply.view with twig engine and unknown template type', async t => { 523 | t.plan(1) 524 | const fastify = Fastify() 525 | const Twig = require('twig') 526 | const data = { title: 'fastify', text: 'text' } 527 | 528 | fastify.register(require('../index'), { 529 | engine: { 530 | twig: Twig 531 | } 532 | }) 533 | 534 | fastify.get('/', (_req, reply) => { 535 | reply.view({ }, data) 536 | }) 537 | 538 | await fastify.listen({ port: 0 }) 539 | 540 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 541 | 542 | t.assert.strictEqual(result.status, 500) 543 | 544 | await fastify.close() 545 | }) 546 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const os = require('node:os') 4 | const { test } = require('node:test') 5 | const path = require('node:path') 6 | const fs = require('node:fs') 7 | const Fastify = require('fastify') 8 | 9 | test('fastify.view exist', async t => { 10 | t.plan(1) 11 | const fastify = Fastify() 12 | 13 | fastify.register(require('../index'), { 14 | engine: { 15 | ejs: require('ejs') 16 | } 17 | }) 18 | 19 | await fastify.ready() 20 | 21 | t.assert.ok(fastify.view) 22 | 23 | await fastify.close() 24 | }) 25 | 26 | test('fastify.view.clearCache exist', async t => { 27 | t.plan(1) 28 | const fastify = Fastify() 29 | 30 | fastify.register(require('../index'), { 31 | engine: { 32 | ejs: require('ejs') 33 | } 34 | }) 35 | 36 | await fastify.ready() 37 | 38 | t.assert.ok(fastify.view.clearCache) 39 | 40 | await fastify.close() 41 | }) 42 | 43 | test('fastify.view.clearCache clears cache', async t => { 44 | t.plan(9) 45 | const templatesFolder = path.join(os.tmpdir(), 'fastify') 46 | try { 47 | fs.mkdirSync(templatesFolder) 48 | } catch {} 49 | fs.writeFileSync(path.join(templatesFolder, 'cache_clear_test.ejs'), '123', { mode: 0o600 }) 50 | const fastify = Fastify() 51 | 52 | fastify.register(require('../index'), { 53 | engine: { 54 | ejs: require('ejs') 55 | }, 56 | includeViewExtension: true, 57 | templates: templatesFolder, 58 | production: true 59 | }) 60 | 61 | fastify.get('/view-cache-test', (_req, reply) => { 62 | reply.type('text/html; charset=utf-8').view('cache_clear_test') 63 | }) 64 | 65 | await fastify.listen({ port: 0 }) 66 | 67 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/view-cache-test') 68 | 69 | const responseContent = await result.text() 70 | 71 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 72 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 73 | fs.writeFileSync(path.join(templatesFolder, 'cache_clear_test.ejs'), '456', { mode: 0o600 }) 74 | 75 | const result2 = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/view-cache-test') 76 | 77 | const responseContent2 = await result2.text() 78 | 79 | t.assert.strictEqual(result2.headers.get('content-length'), '' + responseContent2.length) 80 | t.assert.strictEqual(result2.headers.get('content-type'), 'text/html; charset=utf-8') 81 | t.assert.strictEqual(responseContent, responseContent2) 82 | 83 | fastify.view.clearCache() 84 | 85 | const result3 = await fetch('http://127.0.0.1:' + fastify.server.address().port + '/view-cache-test') 86 | 87 | const responseContent3 = await result3.text() 88 | 89 | t.assert.strictEqual(result3.headers.get('content-length'), '' + responseContent3.length) 90 | t.assert.strictEqual(result3.headers.get('content-type'), 'text/html; charset=utf-8') 91 | 92 | t.assert.notStrictEqual(responseContent, responseContent3) 93 | t.assert.ok(responseContent3.includes('456')) 94 | 95 | await fastify.close() 96 | }) 97 | 98 | test('reply.view exist', async t => { 99 | t.plan(4) 100 | const fastify = Fastify() 101 | 102 | fastify.register(require('../index'), { 103 | engine: { 104 | ejs: require('ejs') 105 | } 106 | }) 107 | 108 | fastify.get('/', (_req, reply) => { 109 | t.assert.ok(reply.view) 110 | reply.send({ hello: 'world' }) 111 | }) 112 | 113 | await fastify.listen({ port: 0 }) 114 | 115 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 116 | const responseContent = await result.text() 117 | 118 | t.assert.strictEqual(result.status, 200) 119 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 120 | t.assert.deepStrictEqual(JSON.parse(responseContent), { hello: 'world' }) 121 | 122 | await fastify.close() 123 | }) 124 | 125 | test('reply.locals exist', async t => { 126 | t.plan(4) 127 | const fastify = Fastify() 128 | 129 | fastify.register(require('../index'), { 130 | engine: { 131 | ejs: require('ejs') 132 | } 133 | }) 134 | 135 | fastify.get('/', (_req, reply) => { 136 | t.assert.ok(reply.locals) 137 | reply.send({ hello: 'world' }) 138 | }) 139 | 140 | await fastify.listen({ port: 0 }) 141 | 142 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 143 | const responseContent = await result.text() 144 | 145 | t.assert.strictEqual(result.status, 200) 146 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 147 | t.assert.deepStrictEqual(JSON.parse(responseContent), { hello: 'world' }) 148 | 149 | await fastify.close() 150 | }) 151 | 152 | test('reply.view can be returned from async function to indicate response processing finished', async t => { 153 | t.plan(4) 154 | const fastify = Fastify() 155 | const ejs = require('ejs') 156 | const data = { text: 'text' } 157 | 158 | fastify.register(require('../index'), { 159 | engine: { 160 | ejs 161 | }, 162 | root: path.join(__dirname, '../templates'), 163 | layout: 'layout.html' 164 | }) 165 | 166 | fastify.get('/', async (_req, reply) => { 167 | return reply.view('index-for-layout.ejs', data) 168 | }) 169 | 170 | await fastify.listen({ port: 0 }) 171 | 172 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 173 | const responseContent = await result.text() 174 | 175 | t.assert.strictEqual(result.status, 200) 176 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 177 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 178 | t.assert.strictEqual(ejs.render(fs.readFileSync('./templates/index.ejs', 'utf8'), data), responseContent) 179 | 180 | await fastify.close() 181 | }) 182 | 183 | test('Possibility to access res.locals variable across all views', async t => { 184 | t.plan(4) 185 | const fastify = Fastify() 186 | 187 | fastify.register(require('../index'), { 188 | engine: { 189 | ejs: require('ejs') 190 | }, 191 | root: path.join(__dirname, '../templates'), 192 | layout: 'index-layout-body', 193 | viewExt: 'ejs' 194 | }) 195 | 196 | fastify.addHook('preHandler', async function (_req, reply) { 197 | reply.locals = { 198 | content: 'ok' 199 | } 200 | }) 201 | 202 | fastify.get('/', async (_req, reply) => { 203 | return reply.view('index-layout-content') 204 | }) 205 | 206 | await fastify.listen({ port: 0 }) 207 | 208 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 209 | const responseContent = await result.text() 210 | 211 | t.assert.strictEqual(result.status, 200) 212 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 213 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 214 | t.assert.strictEqual('ok', responseContent.trim()) 215 | 216 | await fastify.close() 217 | }) 218 | 219 | test('Default extension for ejs', async t => { 220 | t.plan(4) 221 | const fastify = Fastify() 222 | 223 | fastify.register(require('../index'), { 224 | engine: { 225 | ejs: require('ejs') 226 | }, 227 | root: path.join(__dirname, '../templates'), 228 | viewExt: 'html' 229 | }) 230 | 231 | fastify.get('/', async (_req, reply) => { 232 | return reply.view('index-with-includes-without-ext') 233 | }) 234 | 235 | await fastify.listen({ port: 0 }) 236 | 237 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 238 | const responseContent = await result.text() 239 | 240 | t.assert.strictEqual(result.status, 200) 241 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 242 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 243 | t.assert.strictEqual('ok', responseContent.trim()) 244 | 245 | await fastify.close() 246 | }) 247 | 248 | test('reply.view with ejs engine and custom propertyName', async t => { 249 | t.plan(8) 250 | const fastify = Fastify() 251 | const ejs = require('ejs') 252 | 253 | fastify.register(require('../index'), { 254 | engine: { 255 | ejs 256 | }, 257 | root: path.join(__dirname, '../templates'), 258 | layout: 'layout.html', 259 | propertyName: 'mobile' 260 | }) 261 | fastify.register(require('../index'), { 262 | engine: { 263 | ejs 264 | }, 265 | root: path.join(__dirname, '../templates'), 266 | layout: 'layout.html', 267 | propertyName: 'desktop' 268 | }) 269 | 270 | fastify.get('/', async (req, reply) => { 271 | const text = req.headers['user-agent'] 272 | return reply[text]('index-for-layout.ejs', { text }) 273 | }) 274 | 275 | await fastify.listen({ port: 0 }) 276 | 277 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port, { 278 | headers: { 279 | 'user-agent': 'mobile' 280 | } 281 | }) 282 | const responseContent = await result.text() 283 | 284 | t.assert.strictEqual(result.status, 200) 285 | t.assert.strictEqual(result.headers.get('content-length'), '' + responseContent.length) 286 | t.assert.strictEqual(result.headers.get('content-type'), 'text/html; charset=utf-8') 287 | t.assert.strictEqual(ejs.render(fs.readFileSync('./templates/index.ejs', 'utf8'), { text: 'mobile' }), responseContent) 288 | 289 | const result2 = await fetch('http://127.0.0.1:' + fastify.server.address().port, { 290 | headers: { 291 | 'user-agent': 'desktop' 292 | } 293 | }) 294 | const responseContent2 = await result2.text() 295 | 296 | t.assert.strictEqual(result2.status, 200) 297 | t.assert.strictEqual(result2.headers.get('content-length'), '' + responseContent2.length) 298 | t.assert.strictEqual(result2.headers.get('content-type'), 'text/html; charset=utf-8') 299 | t.assert.strictEqual(ejs.render(fs.readFileSync('./templates/index.ejs', 'utf8'), { text: 'desktop' }), responseContent2) 300 | 301 | await fastify.close() 302 | }) 303 | 304 | test('reply.view should return 500 if page is missing', async t => { 305 | t.plan(1) 306 | const fastify = Fastify() 307 | 308 | fastify.register(require('../index'), { 309 | engine: { 310 | ejs: require('ejs') 311 | } 312 | }) 313 | 314 | fastify.get('/', (_req, reply) => { 315 | reply.view() 316 | }) 317 | 318 | await fastify.listen({ port: 0 }) 319 | 320 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 321 | 322 | t.assert.strictEqual(result.status, 500) 323 | 324 | await fastify.close() 325 | }) 326 | 327 | test('reply.view should return 500 if layout is set globally and provided on render', async t => { 328 | t.plan(1) 329 | const fastify = Fastify() 330 | const data = { text: 'text' } 331 | fastify.register(require('../index'), { 332 | engine: { 333 | ejs: require('ejs'), 334 | layout: 'layout.html' 335 | } 336 | }) 337 | 338 | fastify.get('/', (_req, reply) => { 339 | reply.view('index-for-layout.ejs', data, { layout: 'layout.html' }) 340 | }) 341 | 342 | await fastify.listen({ port: 0 }) 343 | 344 | const result = await fetch('http://127.0.0.1:' + fastify.server.address().port) 345 | 346 | t.assert.strictEqual(result.status, 500) 347 | 348 | await fastify.close() 349 | }) 350 | 351 | test('register callback should throw if the engine is missing', async t => { 352 | t.plan(1) 353 | const fastify = Fastify() 354 | 355 | fastify.register(require('../index')) 356 | 357 | await t.assert.rejects(fastify.ready(), undefined, 'Missing engine') 358 | }) 359 | 360 | test('register callback should throw if the engine is not supported', async t => { 361 | t.plan(1) 362 | const fastify = Fastify() 363 | 364 | const register = fastify.register(require('../index'), { 365 | engine: { 366 | notSupported: null 367 | } 368 | }) 369 | 370 | await t.assert.rejects(register.ready(), undefined, '\'notSupported\' not yet supported, PR? :)') 371 | }) 372 | 373 | test('register callback with handlebars engine should throw if layout file does not exist', async t => { 374 | t.plan(1) 375 | const fastify = Fastify() 376 | 377 | const register = fastify.register(require('../index'), { 378 | engine: { 379 | handlebars: require('handlebars') 380 | }, 381 | layout: './templates/does-not-exist.hbs' 382 | }) 383 | 384 | await t.assert.rejects(register.ready(), undefined, 'unable to access template "./templates/does-not-exist.hbs"') 385 | }) 386 | 387 | test('register callback should throw if layout option provided with wrong engine', async t => { 388 | t.plan(1) 389 | const fastify = Fastify() 390 | 391 | const register = fastify.register(require('../index'), { 392 | engine: { 393 | pug: require('pug') 394 | }, 395 | layout: 'template' 396 | }) 397 | 398 | await t.assert.rejects(register.ready(), undefined, 'Only Dot, Handlebars, EJS, and Eta support the "layout" option') 399 | }) 400 | 401 | test('register callback should throw if templates option provided as array with wrong engine', async t => { 402 | t.plan(1) 403 | const fastify = Fastify() 404 | 405 | const register = fastify.register(require('../index'), { 406 | engine: { 407 | pug: require('pug') 408 | }, 409 | templates: ['layouts', 'pages'] 410 | }) 411 | 412 | await t.assert.rejects(register.ready(), undefined, 'Only Nunjucks supports the "templates" option as an array') 413 | }) 414 | 415 | test('plugin is registered with "point-of-view" name', async t => { 416 | t.plan(1) 417 | const fastify = Fastify() 418 | 419 | fastify.register(require('../index'), { 420 | engine: { 421 | ejs: require('ejs') 422 | } 423 | }) 424 | 425 | await fastify.ready() 426 | 427 | const kRegistedPlugins = Symbol.for('registered-plugin') 428 | const registeredPlugins = fastify[kRegistedPlugins] 429 | t.assert.ok(registeredPlugins.find(name => name === '@fastify/view')) 430 | 431 | await fastify.close() 432 | }) 433 | -------------------------------------------------------------------------------- /types/index-global-layout.test-d.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import fastifyView, { FastifyViewOptions } from '..' 3 | import { expectAssignable } from 'tsd' 4 | import * as path from 'node:path' 5 | 6 | interface Locals { 7 | appVersion: string, 8 | } 9 | 10 | declare module 'fastify' { 11 | interface FastifyReply { 12 | locals: Partial | undefined 13 | } 14 | } 15 | const app = fastify() 16 | 17 | app.register(fastifyView, { 18 | engine: { 19 | ejs: require('ejs'), 20 | }, 21 | templates: 'templates', 22 | includeViewExtension: true, 23 | defaultContext: { 24 | dev: true, 25 | }, 26 | options: {}, 27 | charset: 'utf-8', 28 | layout: 'layout-ts', 29 | maxCache: 100, 30 | production: false, 31 | root: path.resolve(__dirname, '../templates'), 32 | viewExt: 'ejs', 33 | }) 34 | 35 | app.get('/', (_request, reply) => { 36 | reply.view('/layout-ts-content-no-data') 37 | }) 38 | 39 | app.get('/data', (_request, reply) => { 40 | if (!reply.locals) { 41 | reply.locals = {} 42 | } 43 | 44 | // reply.locals.appVersion = 1 // not a valid type 45 | reply.locals.appVersion = '4.14.0' 46 | reply.view('/layout-ts-content-with-data', { text: 'Sample data' }) 47 | }) 48 | 49 | app.get('/dataTyped', (_request, reply) => { 50 | if (!reply.locals) { 51 | reply.locals = {} 52 | } 53 | 54 | // reply.locals.appVersion = 1 // not a valid type 55 | reply.locals.appVersion = '4.14.0' 56 | reply.view<{ text: string; }>('/layout-ts-content-with-data', { text: 'Sample data' }) 57 | }) 58 | 59 | app.listen({ port: 3000 }, (err, address) => { 60 | if (err) throw err 61 | console.log(`server listening on ${address} ...`) 62 | }) 63 | 64 | expectAssignable({ engine: { twig: require('twig') }, propertyName: 'mobile' }) 65 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FastifyPluginAsync } from 'fastify' 2 | 3 | declare module 'fastify' { 4 | 5 | interface RouteSpecificOptions { 6 | layout?: string; 7 | } 8 | 9 | interface FastifyReply { 10 | view(page: string, data: T, opts?: RouteSpecificOptions): FastifyReply; 11 | view(page: string, data?: object, opts?: RouteSpecificOptions): FastifyReply; 12 | viewAsync(page: string, data: T, opts?: RouteSpecificOptions): Promise; 13 | viewAsync(page: string, data?: object, opts?: RouteSpecificOptions): Promise; 14 | } 15 | 16 | interface FastifyInstance { 17 | view(page: string, data: T, opts?: RouteSpecificOptions): Promise; 18 | view(page: string, data?: object, opts?: RouteSpecificOptions): Promise; 19 | } 20 | } 21 | 22 | type FastifyView = FastifyPluginAsync 23 | 24 | declare namespace fastifyView { 25 | export interface FastifyViewOptions { 26 | engine: { 27 | ejs?: any; 28 | eta?: any; 29 | nunjucks?: any; 30 | pug?: any; 31 | handlebars?: any; 32 | mustache?: any; 33 | twig?: any; 34 | liquid?: any; 35 | dot?: any; 36 | }; 37 | templates?: string | string[]; 38 | includeViewExtension?: boolean; 39 | options?: object; 40 | charset?: string; 41 | maxCache?: number; 42 | production?: boolean; 43 | defaultContext?: object; 44 | layout?: string; 45 | root?: string; 46 | viewExt?: string; 47 | propertyName?: string; 48 | asyncPropertyName?: string; 49 | } 50 | 51 | /** 52 | * @deprecated Use FastifyViewOptions 53 | */ 54 | export type PointOfViewOptions = FastifyViewOptions 55 | 56 | export const fastifyView: FastifyView 57 | export { fastifyView as default } 58 | export const fastifyViewCache: Symbol 59 | } 60 | 61 | declare function fastifyView (...params: Parameters): ReturnType 62 | export = fastifyView 63 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import fastifyView, { PointOfViewOptions, FastifyViewOptions } from '..' 3 | import { expectAssignable, expectNotAssignable, expectDeprecated, expectType } from 'tsd' 4 | import * as path from 'node:path' 5 | 6 | interface Locals { 7 | appVersion: string, 8 | } 9 | 10 | declare module 'fastify' { 11 | interface FastifyReply { 12 | locals: Partial | undefined 13 | } 14 | } 15 | const app = fastify() 16 | 17 | app.register(fastifyView, { 18 | engine: { 19 | ejs: require('ejs'), 20 | }, 21 | templates: 'templates', 22 | includeViewExtension: true, 23 | defaultContext: { 24 | dev: true, 25 | }, 26 | options: {}, 27 | charset: 'utf-8', 28 | maxCache: 100, 29 | production: false, 30 | root: path.resolve(__dirname, '../templates'), 31 | viewExt: 'ejs', 32 | }) 33 | 34 | app.get('/', (_request, reply) => { 35 | reply.view('/index-with-no-data') 36 | }) 37 | 38 | app.get('/data', (_request, reply) => { 39 | if (!reply.locals) { 40 | reply.locals = {} 41 | } 42 | 43 | // reply.locals.appVersion = 1 // not a valid type 44 | expectNotAssignable['appVersion']>(1) 45 | 46 | reply.locals.appVersion = '4.14.0' 47 | reply.view('/index', { text: 'Sample data' }) 48 | }) 49 | 50 | app.get('/dataTyped', (_request, reply) => { 51 | if (!reply.locals) { 52 | reply.locals = {} 53 | } 54 | 55 | // reply.locals.appVersion = 1 // not a valid type 56 | expectNotAssignable['appVersion']>(1) 57 | 58 | reply.locals.appVersion = '4.14.0' 59 | reply.view<{ text: string; }>('/index', { text: 'Sample data' }) 60 | }) 61 | 62 | app.get('/use-layout', (_request, reply) => { 63 | reply.view('/layout-ts-content-with-data', { text: 'Using a layout' }, { layout: '/layout-ts' }) 64 | }) 65 | 66 | app.get('/view-async', async (_request, reply) => { 67 | expectAssignable['appVersion']>('4.14.0') 68 | expectNotAssignable['appVersion']>(1) 69 | 70 | type ViewAsyncDataParamType = Parameters[1] 71 | expectAssignable({ text: 'Sample data' }) 72 | expectAssignable({ notText: 'Sample data ' }) 73 | 74 | const html = await reply.viewAsync('/index', { text: 'Sample data' }) 75 | expectType(html) 76 | return html 77 | }) 78 | 79 | app.get('/view-async-generic-provided', async (_request, reply) => { 80 | type ViewAsyncDataParamType = Parameters>[1] 81 | expectAssignable({ text: 'Sample data' }) 82 | expectNotAssignable({ notText: 'Sample data ' }) 83 | 84 | const html = reply.viewAsync<{ text: string; }>('/index', { text: 'Sample data' }) 85 | expectType>(html) 86 | return html 87 | }) 88 | 89 | app.listen({ port: 3000 }, (err, address) => { 90 | if (err) throw err 91 | console.log(`server listening on ${address} ...`) 92 | }) 93 | 94 | expectType>(app.view('/index', {}, { layout: '/layout-ts' })) 95 | 96 | expectAssignable({ engine: { twig: require('twig') }, propertyName: 'mobile' }) 97 | 98 | expectDeprecated({} as PointOfViewOptions) 99 | 100 | const nunjucksApp = fastify() 101 | 102 | nunjucksApp.register(fastifyView, { 103 | engine: { 104 | nunjucks: require('nunjucks'), 105 | }, 106 | templates: [ 107 | 'templates/nunjucks-layout', 108 | 'templates/nunjucks-template' 109 | ], 110 | }) 111 | 112 | nunjucksApp.get('/', (_request, reply) => { 113 | reply.view('index.njk', { text: 'Sample data' }) 114 | }) 115 | 116 | expectType>(nunjucksApp.view('/', { text: 'Hello world' })) 117 | 118 | expectAssignable({ engine: { nunjucks: require('nunjucks') }, templates: 'templates' }) 119 | 120 | expectAssignable({ engine: { nunjucks: require('nunjucks') }, templates: ['templates/nunjucks-layout', 'templates/nunjucks-template'] }) 121 | --------------------------------------------------------------------------------