diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..d7dbf9a7
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+quote_type = single
+
+[*.md]
+trim_trailing_whitespace = false
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000..7d03cee4
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,3 @@
+{
+ "extends": "standard"
+}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..3c27edcd
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+ Set the default behavior, in case people don't have core.autocrlf set
+* text=auto
+
+# Require Unix line endings
+* text eol=lf
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..f59ec20a
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+*
\ No newline at end of file
diff --git a/README.md b/README.md
index 95564a63..390cc5f4 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,7 @@ Templates rendering plugin support for Fastify.
`point-of-view` decorates the reply interface with the `view` method for managing view engines, which can be used to render templates responses.
Currently supports the following templates engines:
+
- [`ejs`](https://ejs.co/)
- [`nunjucks`](https://mozilla.github.io/nunjucks/)
- [`pug`](https://pugjs.org/api/getting-started.html)
@@ -24,13 +25,15 @@ Currently supports the following templates engines:
In `production` mode, `point-of-view` will heavily cache the templates file and functions, while in `development` will reload every time the template file and function.
-*Note that at least Fastify `v2.0.0` is needed.*
+_Note that at least Fastify `v2.0.0` is needed._
-*Note: [`ejs-mate`](https://github.com/JacksonTian/ejs-mate) support [has been dropped](https://github.com/fastify/point-of-view/pull/157).*
+_Note: [`ejs-mate`](https://github.com/JacksonTian/ejs-mate) support [has been dropped](https://github.com/fastify/point-of-view/pull/157)._
#### Benchmarks
+
The benchmark were run with the files in the `benchmark` folder with the `ejs` engine.
The data has been taken with: `autocannon -c 100 -d 5 -p 10 localhost:3000`
+
- Express: 8.8k req/sec
- **Fastify**: 15.6k req/sec
@@ -40,120 +43,234 @@ The data has been taken with: `autocannon -c 100 -d 5 -p 10 localhost:3000`
npm install point-of-view --save
```
-
-## Usage
+
+
+## Quick start
+
+`fastify.register` is used to register point-of-view. By default, It will decorate the `reply` object with a `view` method that takes at least two arguments:
+
+- the template to be rendered
+- the data that should be available to the template during rendering
+
+This example will render the template and provide a variable `text` to be used inside the template:
+
```js
-const fastify = require('fastify')()
+const fastify = require("fastify")();
-fastify.register(require('point-of-view'), {
+fastify.register(require("point-of-view"), {
engine: {
- ejs: require('ejs')
- }
-})
+ ejs: require("ejs"),
+ },
+});
-fastify.get('/', (req, reply) => {
- reply.view('/templates/index.ejs', { text: 'text' })
-})
+fastify.get("/", (req, reply) => {
+ reply.view("/templates/index.ejs", { text: "text" });
+});
-// With async handler be sure to return the result of reply.view
-fastify.get('/', async (req, reply) => {
- const t = await something()
- return reply.view('/templates/index.ejs', { text: 'text' })
-})
+fastify.listen(3000, (err) => {
+ if (err) throw err;
+ console.log(`server listening on ${fastify.server.address().port}`);
+});
+```
-fastify.listen(3000, err => {
- if (err) throw err
- console.log(`server listening on ${fastify.server.address().port}`)
-})
+If your handler function is asynchronous, make sure to return the result - otherwise this will result in an `FST_ERR_PROMISE_NOT_FULFILLED` error:
+
+```js
+// This is an async function
+fastify.get("/", async (req, reply) => {
+ // We are awaiting a functioon result
+ const t = await something();
+
+ // Note the return statement
+ return reply.view("/templates/index.ejs", { text: "text" });
+});
```
-Or render a template directly with the `fastify.view()` decorator:
+## Configuration
+
+`fastify.register(, )` accepts an options object.
+
+### Options
+
+- `engine`: The template engine object - pass in the return value of `require('')`. This option is mandatory.
+- `layout`: Point-of-view supports layouts for **EJS**, **Handlebars**, **Eta** and **doT**. This option lets you specify a global layout file to be used when rendering your templates. Settings like `root` or `viewExt` apply as for any other template file. Example: `./templates/layouts/main.hbs`
+- `propertyName`: The property that should be used to decorate `reply` and `fastify` - E.g. `reply.view()` and `fastify.view()` where `"view"` is the property name. Default: `"view"`.
+- `root`: The root path of your templates folder. The template name or path passed to the render function will be resolved relative to this path. Default: `"./"`.
+- `includeViewExtension`: Setting this to `true` will automatically append the default extension for the used template engine **if ommited from the template name** . So instead of `template.hbs`, just `template` can be used. Default: `false`.
+- `viewExt`: Let's you override the default extension for a given template engine. This has precedence over `includeViewExtension` and will lead to the same behavior, just with a custom extension. Default `""`. Example: `"handlebars"`.
+- `defaultContext`: The template variables defined here will be available to all views. Variables provided on render have precendence and will **override** this if they have the same name. Default: `{}`. Example: `{ siteName: "MyAwesomeSite" }`.
+
+Example:
+
```js
-// With a promise
-const html = await fastify.view('/templates/index.ejs', { text: 'text' })
+fastify.register(require("point-of-view"), {
+ engine: {
+ handlebars: require("handlebars"),
+ },
+ root: path.join(__dirname, "views"), // Points to `./views` relative to the current file
+ layout: "./templates/template", // Sets the layout to use to `./views/templates/layout.handlebars` relative to the current file.
+ viewExt: "handlebars", // Sets the default extension to `.handlebars`
+ propertyName: "render", // The template can now be rendered via `reply.render()` and `fastify.render()`
+ defaultContext: {
+ dev: process.env.NODE_ENV === "development", // Inside your templates, `dev` will be `true` if the expression evaluates to true
+ },
+ options: {}, // No options passed to handlebars
+});
+```
-// or with a callback
-fastify.view('/templates/index.ejs', { text: 'text' }, (err, html) => {
- // ...
-})
+## Rendering the template into a variable
+
+The `fastify` object is decorated the same way as `reply` and allows you to just render a view into a variable instead of sending the result back to the browser:
+
+```js
+// Promise based, using async/await
+const html = await fastify.view("/templates/index.ejs", { text: "text" });
+
+// Callback based
+fastify.view("/templates/index.ejs", { text: "text" }, (err, html) => {
+ // Handle error
+ // Do something with `html`
+});
```
-Like having 2 different declarations with different propertynames calling different partials:
+## Registering multiple engines
+
+Registering multiple engines with different configurations is supported. They are dinguished via their `propertyName`:
+
```js
-fastify.register(require('../index'), {
+fastify.register(require("point-of-view"), {
engine: { ejs: ejs },
- layout: './templates/layout-mobile.ejs'
- propertyName: 'mobile'
-})
-fastify.register(require('../index'), {
+ layout: "./templates/layout-mobile.ejs",
+ propertyName: "mobile",
+});
+
+fastify.register(require("point-of-view"), {
engine: { ejs: ejs },
- layout: './templates/layout-desktop.ejs'
- propertyName: 'desktop'
-})
+ layout: "./templates/layout-desktop.ejs",
+ propertyName: "desktop",
+});
+
+fastify.get("/mobile", (req, reply) => {
+ // Render using the `mobile` render function
+ return reply.mobile("/templates/index.ejs", { text: "text" });
+});
+
+fastify.get("/desktop", (req, reply) => {
+ // Render using the `desktop` render function
+ return reply.desktop("/templates/index.ejs", { text: "text" });
+});
```
-If you want to set a fixed templates folder, or pass some options to the template engines:
+## Providing a layout on render
+
+Point-of-view supports layouts for **EJS**, **Handlebars**, **Eta** and **doT**.
+These engines also support providing a layout on render.
+
+**Please note:** Global layouts and provding layouts on render are mutually exclusive. They can not be mixed.
+
```js
-fastify.register(require('point-of-view'), {
- engine: {
- ejs: require('ejs')
- },
- root: path.join(__dirname, 'view'),
- layout: 'template',
- viewExt: 'html', // it will add the extension to all the views
- options: {}
+fastify.get('/', (req, reply) => {
+ reply.view('index-for-layout.ejs', data, { layout: 'layout.html' })
})
```
-If you want to set a default context that the variable can be using in each view:
+## Setting request-global variables
+Sometimes, several templates should have access to the same request-sceific variables. E.g. when setting the current username.
+
+If you want to provide data, which will be depended on by a request and available in all views, you have to add property `locals` to `reply` object, like in the example below:
+
```js
-fastify.register(require('point-of-view'), {
- engine: {
- ejs: require('ejs')
- },
- defaultContext: {
- dev: process.env.NODE_ENV === 'development'
+fastify.addHook("preHandler", function (request, reply, done) {
+ reply.locals = {
+ text: getTextFromRequest(request), // it will be available in all views
+ };
+
+ done();
+});
+```
+
+Properties from `reply.locals` will override those from `defaultContext`, but not from `data` parameter provided to `reply.view(template, data)` function.
+
+## Minifying HTML on render
+
+To utilize [`html-minifier`](https://www.npmjs.com/package/html-minifier) in the rendering process, you can add the option `useHtmlMinifier` with a reference to `html-minifier`,
+and the optional `htmlMinifierOptions` option is used to specify the `html-minifier` options:
+
+```js
+// get a reference to html-minifier
+const minifier = require('html-minifier')
+// optionally defined the html-minifier options
+const minifierOpts = {
+ removeComments: true,
+ removeCommentsFromCDATA: true,
+ collapseWhitespace: true,
+ collapseBooleanAttributes: true,
+ removeAttributeQuotes: true,
+ removeEmptyAttributes: true
+}
+// in template engine options configure the use of html-minifier
+ options: {
+ useHtmlMinifier: minifier,
+ htmlMinifierOptions: minifierOpts
}
-})
```
-and in the template files like pug can use the variable like:
-```html
-link(src=dev?"link-to-dev.css":"link-to-pro.css")
+
+To utilize [`html-minify-stream`](https://www.npmjs.com/package/html-minify-stream) in the rendering process with template engines that support streams,
+you can add the option `useHtmlMinifyStream` with a reference to `html-minify-stream`, and the optional `htmlMinifierOptions` option is used to specify the options just like `html-minifier`:
+
+```js
+// get a reference to html-minify-stream
+const htmlMinifyStream = require('html-minify-stream')
+// optionally defined the html-minifier options that are used by html-minify-stream
+const minifierOpts = {
+ removeComments: true,
+ removeCommentsFromCDATA: true,
+ collapseWhitespace: true,
+ collapseBooleanAttributes: true,
+ removeAttributeQuotes: true,
+ removeEmptyAttributes: true
+}
+// in template engine options configure the use of html-minify-stream
+ options: {
+ useHtmlMinifyStream: htmlMinifyStream,
+ htmlMinifierOptions: minifierOpts
+ }
```
-Note that the data passing to the template will **override** the defaultContext
-If you want to omit view extension, you can add `includeViewExtension` property as following:
-```javascript
-fastify.register(require('point-of-view'), {
- engine: {
- ejs: require('ejs')
- },
- includeViewExtension: true
-});
-fastify.get('/', (req, reply) => {
- reply.view('/templates/index', { text: 'text' })
-})
-```
-Note that to use include files with ejs you also need:
+## Engine-specific settings
+
+
+
+### Mustache
To use partials in mustache you will need to pass the names and paths in the options parameter:
+
```js
options: {
partials: {
@@ -163,7 +280,10 @@ To use partials in mustache you will need to pass the names and paths in the opt
}
```
+### Handlebars
+
To use partials in handlebars you will need to pass the names and paths in the options parameter:
+
```js
options: {
partials: {
@@ -174,204 +294,162 @@ To use partials in handlebars you will need to pass the names and paths in the o
```
To use layouts in handlebars you will need to pass the `layout` parameter:
+
```js
-fastify.register(require('point-of-view'), {
+fastify.register(require("point-of-view"), {
engine: {
- handlebars: require('handlebars')
+ handlebars: require("handlebars"),
},
- layout: './templates/layout.hbs'
+ layout: "./templates/layout.hbs",
});
-fastify.get('/', (req, reply) => {
- reply.view('./templates/index.hbs', { text: 'text' })
-})
+fastify.get("/", (req, reply) => {
+ reply.view("./templates/index.hbs", { text: "text" });
+});
```
+### Nunjucks
+
To configure nunjucks environment after initialisation, you can pass callback function to options:
-```js
- options: {
- onConfigure: (env) => {
- // do whatever you want on nunjucks env
- }
- }
+```js
+options: {
+ onConfigure: (env) => {
+ // do whatever you want on nunjucks env
+ };
+}
```
+### Liquid
+
To configure liquid you need to pass the engine instance as engine option:
+
```js
-const { Liquid } = require('liquidjs')
-const path = require('path')
+const { Liquid } = require("liquidjs");
+const path = require("path");
const engine = new Liquid({
- root: path.join(__dirname, 'templates'),
- extname: '.liquid'
-})
+ root: path.join(__dirname, "templates"),
+ extname: ".liquid",
+});
-fastify.register(require('point-of-view'), {
+fastify.register(require("point-of-view"), {
engine: {
- liquid: engine
- }
+ liquid: engine,
+ },
});
-fastify.get('/', (req, reply) => {
- reply.view('./templates/index.liquid', { text: 'text' })
-})
+fastify.get("/", (req, reply) => {
+ reply.view("./templates/index.liquid", { text: "text" });
+});
```
+### doT
+
When using [doT](https://github.com/olado/doT) the plugin compiles all templates when the application starts, this way all `.def` files are loaded and
both `.jst` and `.dot` files are loaded as in-memory functions.
This behaviour is recommended by the doT team [here](https://github.com/olado/doT#security-considerations).
To make it possible it is necessary to provide a `root` or `templates` option with the path to the template directory.
+
```js
-const path = require('path')
+const path = require("path");
-fastify.register(require('point-of-view'), {
+fastify.register(require("point-of-view"), {
engine: {
- dot: require('dot')
+ dot: require("dot"),
},
- root: 'templates',
+ root: "templates",
options: {
- destination: 'dot-compiled' // path where compiled .jst files are placed (default = 'out')
- }
+ destination: "dot-compiled", // path where compiled .jst files are placed (default = 'out')
+ },
});
-fastify.get('/', (req, reply) => {
+fastify.get("/", (req, reply) => {
// this works both for .jst and .dot files
- reply.view('index', { text: 'text' })
-})
-```
-
-To utilize [`html-minifier`](https://www.npmjs.com/package/html-minifier) in the rendering process, you can add the option `useHtmlMinifier` with a reference to `html-minifier`,
- and the optional `htmlMinifierOptions` option is used to specify the `html-minifier` options:
-```js
-// get a reference to html-minifier
-const minifier = require('html-minifier')
-// optionally defined the html-minifier options
-const minifierOpts = {
- removeComments: true,
- removeCommentsFromCDATA: true,
- collapseWhitespace: true,
- collapseBooleanAttributes: true,
- removeAttributeQuotes: true,
- removeEmptyAttributes: true
-}
-// in template engine options configure the use of html-minifier
- options: {
- useHtmlMinifier: minifier,
- htmlMinifierOptions: minifierOpts
- }
-```
-To utilize [`html-minify-stream`](https://www.npmjs.com/package/html-minify-stream) in the rendering process with template engines that support streams,
- you can add the option `useHtmlMinifyStream` with a reference to `html-minify-stream`, and the optional `htmlMinifierOptions` option is used to specify the options just like `html-minifier`:
-```js
-// get a reference to html-minify-stream
-const htmlMinifyStream = require('html-minify-stream')
-// optionally defined the html-minifier options that are used by html-minify-stream
-const minifierOpts = {
- removeComments: true,
- removeCommentsFromCDATA: true,
- collapseWhitespace: true,
- collapseBooleanAttributes: true,
- removeAttributeQuotes: true,
- removeEmptyAttributes: true
-}
-// in template engine options configure the use of html-minify-stream
- options: {
- useHtmlMinifyStream: htmlMinifyStream,
- htmlMinifierOptions: minifierOpts
- }
-```
-
-The optional boolean property `production` will override environment variable `NODE_ENV` and force `point-of-view` into `production` or `development` mode:
-```js
- options: {
- // force production mode
- production: true
- }
+ reply.view("index", { text: "text" });
+});
```
-If you want to provide data, which will be depended on by a request and available in all views, you have to add property `locals` to `reply` object, like in the example below:
-```js
-fastify.addHook('preHandler', function (request, reply, done) {
- reply.locals = {
- text: getTextFromRequest(request) // it will be available in all views
- }
-
- done()
-})
-```
-Properties from `reply.locals` will override those from `defaultContext`, but not from `data` parameter provided to `reply.view(template, data)` function.
+
+
+### Miscellaneous
+
+## Using point-of-view as a dependency in a fastify-plugin
To require `point-of-view` as a dependency to a [fastify-plugin](https://github.com/fastify/fastify-plugin), add the name `point-of-view` to the dependencies array in the [plugin's opts](https://github.com/fastify/fastify-plugin#dependencies).
```js
-fastify.register(myViewRendererPlugin, {
- dependencies: ['point-of-view']
-})
+fastify.register(myViewRendererPlugin, {
+ dependencies: ["point-of-view"],
+});
```
+## Forcing a cache-flush
+
To forcefully clear cache when in production mode, call the `view.clearCache()` function.
```js
-fastify.view.clearCache()
+fastify.view.clearCache();
```
+
## Note
By default views are served with the mime type 'text/html; charset=utf-8',
but you can specify a different value using the type function of reply, or by specifying the desired charset in the property 'charset' in the opts object given to the plugin.
-
## Acknowledgements
This project is kindly sponsored by:
+
- [nearForm](https://nearform.com)
- [LetzDoIt](https://www.letzdoitapp.com/)
## License
Licensed under [MIT](./LICENSE).
-
diff --git a/index.js b/index.js
index 3d5d5839..af400fbd 100644
--- a/index.js
+++ b/index.js
@@ -27,38 +27,47 @@ function fastifyView (fastify, opts, next) {
const charset = opts.charset || 'utf-8'
const propertyName = opts.propertyName || 'view'
const engine = opts.engine[type]
- const options = opts.options || {}
+ const globalOptions = opts.options || {}
const templatesDir = opts.root || resolve(opts.templates || './')
const lru = HLRU(opts.maxCache || 100)
const includeViewExtension = opts.includeViewExtension || false
const viewExt = opts.viewExt || ''
const prod = typeof opts.production === 'boolean' ? opts.production : process.env.NODE_ENV === 'production'
const defaultCtx = opts.defaultContext || {}
- const layoutFileName = opts.layout
+ const globalLayoutFileName = opts.layout
- if (layoutFileName && type !== 'dot' && type !== 'handlebars' && type !== 'ejs' && type !== 'eta') {
- next(new Error('Only Dot, Handlebars, EJS, and Eta support the "layout" option'))
- return
+ function layoutIsValid (_layoutFileName) {
+ if (type !== 'dot' && type !== 'handlebars' && type !== 'ejs' && type !== 'eta') {
+ throw new Error('Only Dot, Handlebars, EJS, and Eta support the "layout" option')
+ }
+
+ if (!hasAccessToLayoutFile(_layoutFileName, getDefaultExtension(type))) {
+ throw new Error(`unable to access template "${_layoutFileName}"`)
+ }
}
- if (layoutFileName && !hasAccessToLayoutFile(layoutFileName, getDefaultExtension(type))) {
- next(new Error(`unable to access template "${layoutFileName}"`))
- return
+ if (globalLayoutFileName) {
+ try {
+ layoutIsValid(globalLayoutFileName)
+ } catch (error) {
+ next(error)
+ return
+ }
}
- const dotRender = type === 'dot' ? viewDot.call(fastify, preProcessDot.call(fastify, templatesDir, options)) : null
+ const dotRender = type === 'dot' ? viewDot.call(fastify, preProcessDot.call(fastify, templatesDir, globalOptions)) : null
const renders = {
marko: viewMarko,
- ejs: withLayout(viewEjs),
- handlebars: withLayout(viewHandlebars),
+ ejs: withLayout(viewEjs, globalLayoutFileName),
+ handlebars: withLayout(viewHandlebars, globalLayoutFileName),
mustache: viewMustache,
nunjucks: viewNunjucks,
'art-template': viewArtTemplate,
twig: viewTwig,
liquid: viewLiquid,
- dot: withLayout(dotRender),
- eta: withLayout(viewEta),
+ dot: withLayout(dotRender, globalLayoutFileName),
+ eta: withLayout(viewEta, globalLayoutFileName),
_default: view
}
@@ -153,8 +162,8 @@ function fastifyView (fastify, opts, next) {
callback(err, null)
return
}
- if (options.useHtmlMinifier && (typeof options.useHtmlMinifier.minify === 'function')) {
- data = options.useHtmlMinifier.minify(data, options.htmlMinifierOptions || {})
+ if (globalOptions.useHtmlMinifier && (typeof globalOptions.useHtmlMinifier.minify === 'function')) {
+ data = globalOptions.useHtmlMinifier.minify(data, globalOptions.htmlMinifierOptions || {})
}
if (type === 'handlebars') {
data = engine.compile(data)
@@ -186,8 +195,8 @@ function fastifyView (fastify, opts, next) {
if (err) {
error = err
}
- if (options.useHtmlMinifier && (typeof options.useHtmlMinifier.minify === 'function')) {
- data = options.useHtmlMinifier.minify(data, options.htmlMinifierOptions || {})
+ if (globalOptions.useHtmlMinifier && (typeof globalOptions.useHtmlMinifier.minify === 'function')) {
+ data = globalOptions.useHtmlMinifier.minify(data, globalOptions.htmlMinifierOptions || {})
}
partialsHtml[key] = data
@@ -209,15 +218,15 @@ function fastifyView (fastify, opts, next) {
let compiledPage
try {
- if ((type === 'ejs') && viewExt && !options.includer) {
- options.includer = (originalPath, parsedPath) => {
+ if ((type === 'ejs') && viewExt && !globalOptions.includer) {
+ globalOptions.includer = (originalPath, parsedPath) => {
return {
filename: parsedPath || join(templatesDir, originalPath + '.' + viewExt)
}
}
}
- options.filename = join(templatesDir, page)
- compiledPage = engine.compile(html, options)
+ globalOptions.filename = join(templatesDir, page)
+ compiledPage = engine.compile(html, globalOptions)
} catch (error) {
that.send(error)
return
@@ -233,8 +242,8 @@ function fastifyView (fastify, opts, next) {
} catch (error) {
cachedPage = error
}
- if (options.useHtmlMinifier && (typeof options.useHtmlMinifier.minify === 'function')) {
- cachedPage = options.useHtmlMinifier.minify(cachedPage, options.htmlMinifierOptions || {})
+ if (globalOptions.useHtmlMinifier && (typeof globalOptions.useHtmlMinifier.minify === 'function')) {
+ cachedPage = globalOptions.useHtmlMinifier.minify(cachedPage, globalOptions.htmlMinifierOptions || {})
}
that.send(cachedPage)
}
@@ -291,7 +300,18 @@ function fastifyView (fastify, opts, next) {
readFile(join(templatesDir, page), 'utf8', readCallback(this, page, data))
}
- function viewEjs (page, data) {
+ function viewEjs (page, data, opts) {
+ if (opts && opts.layout) {
+ try {
+ layoutIsValid(opts.layout)
+ const that = this
+ return withLayout(viewEjs, opts.layout).call(that, page, data)
+ } catch (error) {
+ this.send(error)
+ return
+ }
+ }
+
if (!page) {
this.send(new Error('Missing page'))
return
@@ -331,7 +351,7 @@ function fastifyView (fastify, opts, next) {
}
// merge engine options
- const confs = Object.assign({}, defaultSetting, options)
+ const confs = Object.assign({}, defaultSetting, globalOptions)
function render (filename, data) {
confs.filename = join(templatesDir, filename)
@@ -355,17 +375,17 @@ function fastifyView (fastify, opts, next) {
this.send(new Error('Missing page'))
return
}
- const env = engine.configure(templatesDir, options)
- if (typeof options.onConfigure === 'function') {
- options.onConfigure(env)
+ const env = engine.configure(templatesDir, globalOptions)
+ if (typeof globalOptions.onConfigure === 'function') {
+ globalOptions.onConfigure(env)
}
data = Object.assign({}, defaultCtx, this.locals, data)
// Append view extension.
page = getPage(page, 'njk')
env.render(join(templatesDir, page), data, (err, html) => {
if (err) return this.send(err)
- if (options.useHtmlMinifier && (typeof options.useHtmlMinifier.minify === 'function')) {
- html = options.useHtmlMinifier.minify(html, options.htmlMinifierOptions || {})
+ if (globalOptions.useHtmlMinifier && (typeof globalOptions.useHtmlMinifier.minify === 'function')) {
+ html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})
}
this.header('Content-Type', 'text/html; charset=' + charset)
this.send(html)
@@ -389,8 +409,8 @@ function fastifyView (fastify, opts, next) {
const template = opts && opts.templateSrc ? engine.load(join(templatesDir, page), opts.templateSrc) : engine.load(join(templatesDir, page))
if (opts && opts.stream) {
- if (typeof options.useHtmlMinifyStream === 'function') {
- this.send(template.stream(data).pipe(options.useHtmlMinifyStream(options.htmlMinifierOptions || {})))
+ if (typeof globalOptions.useHtmlMinifyStream === 'function') {
+ this.send(template.stream(data).pipe(globalOptions.useHtmlMinifyStream(globalOptions.htmlMinifierOptions || {})))
} else {
this.send(template.stream(data))
}
@@ -401,8 +421,8 @@ function fastifyView (fastify, opts, next) {
function send (that) {
return function _send (err, html) {
if (err) return that.send(err)
- if (options.useHtmlMinifier && (typeof options.useHtmlMinifier.minify === 'function')) {
- html = options.useHtmlMinifier.minify(html, options.htmlMinifierOptions || {})
+ if (globalOptions.useHtmlMinifier && (typeof globalOptions.useHtmlMinifier.minify === 'function')) {
+ html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})
}
that.header('Content-Type', 'text/html; charset=' + charset)
that.send(html)
@@ -410,13 +430,24 @@ function fastifyView (fastify, opts, next) {
}
}
- function viewHandlebars (page, data) {
+ function viewHandlebars (page, data, opts) {
+ if (opts && opts.layout) {
+ try {
+ layoutIsValid(opts.layout)
+ const that = this
+ return withLayout(viewHandlebars, opts.layout).call(that, page, data)
+ } catch (error) {
+ this.send(error)
+ return
+ }
+ }
+
if (!page) {
this.send(new Error('Missing page'))
return
}
- const options = Object.assign({}, opts.options)
+ const options = Object.assign({}, globalOptions)
data = Object.assign({}, defaultCtx, this.locals, data)
// append view extension
page = getPage(page, 'hbs')
@@ -498,7 +529,7 @@ function fastifyView (fastify, opts, next) {
return
}
- data = Object.assign({}, defaultCtx, options, this.locals, data)
+ data = Object.assign({}, defaultCtx, globalOptions, this.locals, data)
// Append view extension.
page = getPage(page, 'twig')
engine.renderFile(join(templatesDir, page), data, (err, html) => {
@@ -506,8 +537,8 @@ function fastifyView (fastify, opts, next) {
return this.send(err)
}
- if (options.useHtmlMinifier && (typeof options.useHtmlMinifier.minify === 'function')) {
- html = options.useHtmlMinifier.minify(html, options.htmlMinifierOptions || {})
+ if (globalOptions.useHtmlMinifier && (typeof globalOptions.useHtmlMinifier.minify === 'function')) {
+ html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})
}
if (!this.getHeader('content-type')) {
this.header('Content-Type', 'text/html; charset=' + charset)
@@ -528,8 +559,8 @@ function fastifyView (fastify, opts, next) {
engine.renderFile(join(templatesDir, page), data, opts)
.then((html) => {
- if (options.useHtmlMinifier && (typeof options.useHtmlMinifier.minify === 'function')) {
- html = options.useHtmlMinifier.minify(html, options.htmlMinifierOptions || {})
+ if (globalOptions.useHtmlMinifier && (typeof globalOptions.useHtmlMinifier.minify === 'function')) {
+ html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})
}
if (!this.getHeader('content-type')) {
this.header('Content-Type', 'text/html; charset=' + charset)
@@ -543,14 +574,24 @@ function fastifyView (fastify, opts, next) {
function viewDot (renderModule) {
return function _viewDot (page, data, opts) {
+ if (opts && opts.layout) {
+ try {
+ layoutIsValid(opts.layout)
+ const that = this
+ return withLayout(dotRender, opts.layout).call(that, page, data)
+ } catch (error) {
+ this.send(error)
+ return
+ }
+ }
if (!page) {
this.send(new Error('Missing page'))
return
}
data = Object.assign({}, defaultCtx, this.locals, data)
let html = renderModule[page](data)
- if (options.useHtmlMinifier && (typeof options.useHtmlMinifier.minify === 'function')) {
- html = options.useHtmlMinifier.minify(html, options.htmlMinifierOptions || {})
+ if (globalOptions.useHtmlMinifier && (typeof globalOptions.useHtmlMinifier.minify === 'function')) {
+ html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})
}
if (!this.getHeader('content-type')) {
this.header('Content-Type', 'text/html; charset=' + charset)
@@ -559,7 +600,18 @@ function fastifyView (fastify, opts, next) {
}
}
- function viewEta (page, data) {
+ function viewEta (page, data, opts) {
+ if (opts && opts.layout) {
+ try {
+ layoutIsValid(opts.layout)
+ const that = this
+ return withLayout(viewEta, opts.layout).call(that, page, data)
+ } catch (error) {
+ this.send(error)
+ return
+ }
+ }
+
if (!page) {
this.send(new Error('Missing page'))
return
@@ -567,13 +619,13 @@ function fastifyView (fastify, opts, next) {
lru.define = lru.set
engine.configure({
- templates: options.templates ? options.templates : lru
+ templates: globalOptions.templates ? globalOptions.templates : lru
})
const config = Object.assign({
cache: prod,
views: templatesDir
- }, options)
+ }, globalOptions)
data = Object.assign({}, defaultCtx, this.locals, data)
// Append view extension (Eta will append '.eta' by default,
@@ -595,8 +647,8 @@ function fastifyView (fastify, opts, next) {
})
}
- if (prod && type === 'handlebars' && options.partials) {
- getPartials(type, options.partials, (err, partialsObject) => {
+ if (prod && type === 'handlebars' && globalOptions.partials) {
+ getPartials(type, globalOptions.partials, (err, partialsObject) => {
if (err) {
next(err)
return
@@ -610,13 +662,12 @@ function fastifyView (fastify, opts, next) {
next()
}
- function withLayout (render) {
- if (layoutFileName) {
+ function withLayout (render, layout) {
+ if (layout) {
return function (page, data, opts) {
+ if (opts && opts.layout) throw new Error('A layout can either be set globally or on render, not both.')
const that = this
-
data = Object.assign({}, defaultCtx, this.locals, data)
-
render.call({
getHeader: () => { },
header: () => { },
@@ -626,13 +677,11 @@ function fastifyView (fastify, opts, next) {
}
data = Object.assign((data || {}), { body: result })
-
- render.call(that, layoutFileName, data, opts)
+ render.call(that, layout, data, opts)
}
}, page, data, opts)
}
}
-
return render
}
diff --git a/package.json b/package.json
index ab803fa5..e6625a6a 100644
--- a/package.json
+++ b/package.json
@@ -39,11 +39,20 @@
"hashlru": "^2.3.0"
},
"devDependencies": {
- "@types/node": "^16.0.0",
+ "@types/node": "^16.11.10",
+ "@typescript-eslint/eslint-plugin": "^5.4.0",
+ "@typescript-eslint/parser": "^5.4.0",
"art-template": "^4.13.2",
"cross-env": "^7.0.2",
"dot": "^1.1.3",
"ejs": "^3.1.2",
+ "eslint": "^7.32.0",
+ "eslint-config-standard": "^16.0.3",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-plugin-import": "^2.25.3",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-promise": "^5.1.1",
+ "eslint-plugin-standard": "^5.0.0",
"eta": "^1.11.0",
"express": "^4.17.1",
"fastify": "^3.0.0",
diff --git a/test/test-dot.js b/test/test-dot.js
index 316adec5..352e8985 100644
--- a/test/test-dot.js
+++ b/test/test-dot.js
@@ -482,3 +482,101 @@ test('reply.view with dot engine with layout option', t => {
})
})
})
+
+test('reply.view with dot engine with layout option on render', t => {
+ t.plan(6)
+ const fastify = Fastify()
+ const engine = require('dot')
+ const data = { text: 'text' }
+
+ fastify.register(require('../index'), {
+ engine: {
+ dot: engine
+ },
+ root: 'templates'
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('testdot', data, { layout: 'layout' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, body) => {
+ t.error(err)
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-length'], '' + body.length)
+ t.equal(response.headers['content-type'], 'text/html; charset=utf-8')
+ t.equal('header: textfoo text1 foo
footer', body.toString())
+ fastify.close()
+ })
+ })
+})
+
+test('reply.view with dot engine with layout option on render', t => {
+ t.plan(6)
+ const fastify = Fastify()
+ const engine = require('dot')
+ const data = { text: 'text' }
+
+ fastify.register(require('../index'), {
+ engine: {
+ dot: engine
+ },
+ root: 'templates'
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('testdot', data, { layout: 'layout' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, body) => {
+ t.error(err)
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-length'], '' + body.length)
+ t.equal(response.headers['content-type'], 'text/html; charset=utf-8')
+ t.equal('header: textfoo text1 foo
footer', body.toString())
+ fastify.close()
+ })
+ })
+})
+
+test('reply.view should return 500 if layout is missing on render', t => {
+ t.plan(3)
+ const fastify = Fastify()
+ const engine = require('dot')
+ const data = { text: 'text' }
+
+ fastify.register(require('../index'), {
+ engine: {
+ dot: engine
+ },
+ root: 'templates'
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('testdot', data, { layout: 'non-existing-layout' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, body) => {
+ t.error(err)
+ t.equal(response.statusCode, 500)
+ fastify.close()
+ })
+ })
+})
diff --git a/test/test-ejs.js b/test/test-ejs.js
index 5af0cb6c..22729e66 100644
--- a/test/test-ejs.js
+++ b/test/test-ejs.js
@@ -85,6 +85,69 @@ test('reply.view with ejs engine with layout option', t => {
})
})
+test('reply.view with ejs engine with layout option on render', t => {
+ t.plan(6)
+ const fastify = Fastify()
+ const ejs = require('ejs')
+ const data = { text: 'text' }
+
+ fastify.register(require('../index'), {
+ engine: {
+ ejs: ejs
+ },
+ root: path.join(__dirname, '../templates')
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('index-for-layout.ejs', data, { layout: 'layout.html' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, body) => {
+ t.error(err)
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-length'], '' + body.length)
+ t.equal(response.headers['content-type'], 'text/html; charset=utf-8')
+ t.equal(ejs.render(fs.readFileSync('./templates/index.ejs', 'utf8'), data), body.toString())
+ fastify.close()
+ })
+ })
+})
+
+test('reply.view should return 500 if layout is missing on render', t => {
+ t.plan(3)
+ const fastify = Fastify()
+ const ejs = require('ejs')
+ const data = { text: 'text' }
+ fastify.register(require('../index'), {
+ engine: {
+ ejs
+ },
+ root: path.join(__dirname, '../templates')
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('index-for-layout.ejs', data, { layout: 'non-existing-layout.html' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, body) => {
+ t.error(err)
+ t.equal(response.statusCode, 500)
+ fastify.close()
+ })
+ })
+})
+
test('reply.view with ejs engine and custom ext', t => {
t.plan(6)
const fastify = Fastify()
@@ -599,6 +662,51 @@ test('*** reply.view with ejs engine with layout option, includeViewExtension pr
})
})
+test('*** reply.view with ejs engine with layout option on render, includeViewExtension property as true ***', t => {
+ t.plan(6)
+ const fastify = Fastify()
+ const ejs = require('ejs')
+ const data = { text: 'text' }
+ const header = ''
+ const footer = ''
+
+ fastify.register(require('../index'), {
+ engine: {
+ ejs: ejs
+ },
+ defaultContext: {
+ header,
+ footer
+ },
+ includeViewExtension: true,
+ root: path.join(__dirname, '../templates')
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('index-for-layout.ejs', data, { layout: 'layout-with-includes' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, body) => {
+ t.error(err)
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-length'], '' + body.length)
+ t.equal(response.headers['content-type'], 'text/html; charset=utf-8')
+ t.equal(ejs.render(fs.readFileSync('./templates/index.ejs', 'utf8'), {
+ ...data,
+ header,
+ footer
+ }), body.toString())
+ fastify.close()
+ })
+ })
+})
+
test('reply.view with ejs engine, template folder specified, include files (ejs and html) used in template, includeViewExtension property as true', t => {
t.plan(7)
const fastify = Fastify()
diff --git a/test/test-eta.js b/test/test-eta.js
index 8da6be9d..b7ab9204 100644
--- a/test/test-eta.js
+++ b/test/test-eta.js
@@ -82,6 +82,71 @@ test('reply.view with eta engine with layout option', t => {
})
})
+test('reply.view with eta engine with layout option on render', t => {
+ t.plan(6)
+ const fastify = Fastify()
+
+ const data = { text: 'text' }
+
+ fastify.register(pointOfView, {
+ engine: {
+ eta: eta
+ },
+ root: path.join(__dirname, '../templates')
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('index-for-layout.eta', data, { layout: 'layout-eta.html' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, body) => {
+ t.error(err)
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-length'], '' + body.length)
+ t.equal(response.headers['content-type'], 'text/html; charset=utf-8')
+ t.equal(eta.render(fs.readFileSync('./templates/index.eta', 'utf8'), data), body.toString())
+ fastify.close()
+ })
+ })
+})
+
+test('reply.view should return 500 if layout is missing on render', t => {
+ t.plan(3)
+ const fastify = Fastify()
+
+ const data = { text: 'text' }
+
+ fastify.register(pointOfView, {
+ engine: {
+ eta: eta
+ },
+ root: path.join(__dirname, '../templates')
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('index-for-layout.eta', data, { layout: 'non-existing-layout-eta.html' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, body) => {
+ t.error(err)
+ t.equal(response.statusCode, 500)
+ fastify.close()
+ })
+ })
+})
+
test('reply.view with eta engine and custom ext', t => {
t.plan(6)
const fastify = Fastify()
diff --git a/test/test-handlebars.js b/test/test-handlebars.js
index aad1813b..671dd2c6 100644
--- a/test/test-handlebars.js
+++ b/test/test-handlebars.js
@@ -307,6 +307,52 @@ test('fastify.view with handlebars engine with layout option', t => {
})
})
+test('fastify.view with handlebars engine with layout option on render', t => {
+ t.plan(3)
+
+ const fastify = Fastify()
+ const handlebars = require('handlebars')
+ const data = { text: 'it works!' }
+
+ fastify.register(require('../index'), {
+ engine: {
+ handlebars
+ }
+ })
+
+ fastify.ready(err => {
+ t.error(err)
+
+ fastify.view('./templates/index-for-layout.hbs', data, { layout: './templates/layout.hbs' }, (err, compiled) => {
+ t.error(err)
+ t.equal(handlebars.compile(fs.readFileSync('./templates/index.hbs', 'utf8'))(data), compiled)
+ fastify.close()
+ })
+ })
+})
+
+test('fastify.view with handlebars engine with invalid layout option on render should throw', t => {
+ t.plan(3)
+
+ const fastify = Fastify()
+ const handlebars = require('handlebars')
+ const data = { text: 'it works!' }
+
+ fastify.register(require('../index'), {
+ engine: {
+ handlebars
+ }
+ })
+
+ fastify.ready(err => {
+ t.error(err)
+ fastify.view('./templates/index-for-layout.hbs', data, { layout: './templates/invalid-layout.hbs' }, (err, compiled) => {
+ t.ok(err instanceof Error)
+ t.equal(err.message, 'unable to access template "./templates/invalid-layout.hbs"')
+ })
+ })
+})
+
test('reply.view with handlebars engine', t => {
t.plan(6)
const fastify = Fastify()
@@ -758,6 +804,65 @@ test('reply.view with handlebars engine with layout option', t => {
})
})
+test('reply.view with handlebars engine with layout option on render', t => {
+ t.plan(6)
+ const fastify = Fastify()
+ const handlebars = require('handlebars')
+
+ fastify.register(require('../index'), {
+ engine: {
+ handlebars: handlebars
+ }
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('./templates/index-for-layout.hbs', {}, { layout: './templates/layout.hbs' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, replyBody) => {
+ t.error(err)
+ t.equal(response.statusCode, 200)
+ t.equal(response.headers['content-length'], '' + replyBody.length)
+ t.equal(response.headers['content-type'], 'text/html; charset=utf-8')
+ t.equal(handlebars.compile(fs.readFileSync('./templates/index.hbs', 'utf8'))({}), replyBody.toString())
+ fastify.close()
+ })
+ })
+})
+
+test('reply.view should return 500 if layout is missing on render', t => {
+ t.plan(3)
+ const fastify = Fastify()
+ const handlebars = require('handlebars')
+
+ fastify.register(require('../index'), {
+ engine: {
+ handlebars: handlebars
+ }
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('./templates/index-for-layout.hbs', {}, { layout: './templates/missing-layout.hbs' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, replyBody) => {
+ t.error(err)
+ t.equal(response.statusCode, 500)
+ fastify.close()
+ })
+ })
+})
+
test('fastify.view with handlebars engine, missing template file', t => {
t.plan(3)
const fastify = Fastify()
diff --git a/test/test.js b/test/test.js
index 6133f70a..a8f48b5c 100644
--- a/test/test.js
+++ b/test/test.js
@@ -325,6 +325,35 @@ test('reply.view should return 500 if page is missing', t => {
})
})
+test('reply.view should return 500 if layout is set globally and provided on render', t => {
+ t.plan(3)
+ const fastify = Fastify()
+ const data = { text: 'text' }
+ fastify.register(require('../index'), {
+ engine: {
+ ejs: require('ejs'),
+ layout: 'layout.html'
+ }
+ })
+
+ fastify.get('/', (req, reply) => {
+ reply.view('index-for-layout.ejs', data, { layout: 'layout.html' })
+ })
+
+ fastify.listen(0, err => {
+ t.error(err)
+
+ sget({
+ method: 'GET',
+ url: 'http://localhost:' + fastify.server.address().port
+ }, (err, response, body) => {
+ t.error(err)
+ t.equal(response.statusCode, 500)
+ fastify.close()
+ })
+ })
+})
+
test('register callback should throw if the engine is missing', t => {
t.plan(2)
const fastify = Fastify()