gulp-ejs-monster
v3.2.3
Published
Gulp plugin for ejs with steroids
Downloads
351
Maintainers
Readme
gulp-ejs-monster
:us: English | :ru: Русский язык
Gulp plugin for ejs with steroids. The project is inspired by ejs-locals
Table of contents
Thanks
First of all, we want to express our gratitude to the people who led us to use the template engine ejs
and create on its basis gulp-ejs-monster
:
- Matthew Eernisse (mde) - for creating and supporting
ejs
(http://ejs.co), and the community ofejs
, which helps him in this - Tom Carden (RandomEtc) - for creating a project
ejs-locals
, from which we took the idea of realizationgulp-ejs-monster
- Ryan Zimmerman (RyanZim) -
EJS-Lint
- Ariya Hidayat (ariya) -
jquery/esprima
- Corey Hart (codenothing) -
jsonlint
Why this plugin was created?
ejs
(http://ejs.co) - is a universal template engine that allows you to create any markup of any complexity. The better your knowledge of JavaScript - the more opportunities you have with ejs
.
There are already many other plugins for ejs
. But we also decided to create own, as an add-on to the ejs
+ pumping it with a small set of "steroids" ))).
Also, the main focus of the plugin gulp-ejs-monster
- was made on optimization and rendering speed.
By default, ejs
uses the JavaScript construction with (expression)
to add scope - this gives its advantages for working with the template engine, but has its own price - the speed of searching for variables increases - which affects the rendering speed of the pages. This is especially noticeable on large projects.
Therefore gulp-ejs-monster
forcibly turns off the native ejs
parameters in order to work in strict mode. That gives a significant gain to the rendering speed.
List of constant values from gulp-ejs-monster
for ejs
:
{
"strict": true,
"_with": false,
"debug": false,
"rmWhitespace": false,
"client": false
}
This approach also has its price - now only one global object is available for you, without any "proxy" properties (which the with
design used to imitate).
If this approach to working with template engine
ejs
does not suit you, you may no read further and do not create PRs, since we do not intend to change it)))
Example of using the plugin
Installation
npm i --save-dev gulp-ejs-monster
# or yarn cli
yarn add --dev gulp-ejs-monster
Gulp task
const gulp = require('gulp');
const gulpEjsMonster = require('gulp-ejs-monster');
gulp.task('ejs', function() {
return gulp.src('./src/ejs/*.ejs')
.pipe(gulpEjsMonster({/* plugin options */}))
.pipe(gulp.dest('./dist/'));
});
EJS markup
Example of a project structure
ejs/
layouts/
base.ejs
widgets/
news-list.ejs
includes/
critical.css
requires/
news-list.json
index.ejs
news.ejs
Layouts
layouts/base.ejs
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><%- locals.blocks.title %></title>
<style><%- locals.include('includes/critical.css') %></style>
</head>
<body>
<%- locals.blocks.header %>
<%- locals.body %>
</body>
</html>
Render views
index.ejs
<% locals.setLayout('layouts/base.ejs') -%>
<% locals.block('title', 'Index view') -%>
<h1>Index view</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<hr>
news.ejs
<% locals.setLayout('layouts/base.ejs') -%>
<% locals.block('title', 'Last News') -%>
<% let newsList = locals.require('requires/news-list.json') -%>
<h1>Last News</h1>
<%- locals.widget('widgets/news-list.ejs', {list: newsList}) %>
<hr>
Executables files
requires/news-list.json
[
{
"title": "News title 1",
"description": "Lorem ipsum dolor sit ....",
"href": "news-page.html"
}, {
"title": "News title 2",
"description": "Lorem ipsum dolor sit ....",
"href": "news-page.html"
}, {
"title": "News title 3",
"description": "Lorem ipsum dolor sit ....",
"href": "news-page.html"
}
]
Text files
includes/critical.css
html{font-family:sans-serif}
body{margin:0}
h1{color:red}
Widgets
widgets/news-list.ejs
<%
let {
list = []
} = locals.entry;
if (!list.length) {
return 'No news yet :((';
}
-%>
<ul class="news-list">
<% list.forEach(item => { -%>
<li class="news-list__item">
<div class="news-item">
<div class="news-item__title"><%- item.title %></div>
<div class="news-item__description">
<p><%- item.description %></p>
<p><a href="<%- item.href %>">Read more</a></p>
</div>
</div>
</li>
<% }); -%>
</ul>
gulpEjsMonster
Plugin properties
gulpEjsMonster.pluginName
Plugin name.
Plugin methods
gulpEjsMonster.preventCrash()
The method which, on error, calls the end
event to prevent the current process gulp
from falling out of the task.
Example of use
const gulp = require('gulp');
const gulpEjsMonster = require('gulp-ejs-monster');
gulp.task('ejs', function() {
return gulp.src('./src/ejs/*.ejs')
.pipe(gulpEjsMonster({/* plugin options */}).on('error', gulpEjsMonster.preventCrash))
.pipe(gulp.dest('./dist/'));
});
Plugin options
A little bit of advice - in order to speed up the processing and preparation of the plugin's parameters - use the created object with saving to a variable, which you can then specify when you call.
In this case, by storing references to an external object, the parameters will not be re-prepared. And also it is possible to save the received data (in the object locals
) from the previous page of the render to the next.
Example of use
const gulp = require('gulp');
const gulpEjsMonster = require('gulp-ejs-monster');
const options = {/* plugin options */}; // save as variable
gulp.task('ejs', function() {
return gulp.src('./src/ejs/*.ejs')
.pipe(gulpEjsMonster(options).on('error', gulpEjsMonster.preventCrash))
.pipe(gulp.dest('./dist/'));
});
Then you can see a list of all available options.
layouts
data type string
|
default process.cwd()
The relative path from the current working directory to the directory with layouts.
widgets
data type string
|
default process.cwd()
The relative path from the current working directory to the directory with widgets.
requires
data type string
|
default process.cwd()
Relative path from the current working directory to the directory with js/json files, which you can connect as executable files, using the CommonJS export modules.
includes
data type string
|
default process.cwd()
The relative path from the current working directory to the directory c with any text files from which you can connect the content as it is.
extname
data type string
|
default '.html'
Extend the resulting render files.
It is allowed not to specify. (dot) at the beginning of the value, example 'php' => '.php'
delimiter
data type string
|
default '%'
|
допустимые значения ['%', '&', '$', '?']
Symbol for use with angle brackets for opening / closing.
If the specified property does not match the valid value, the value default will be set!
localsName
data type string
|
default 'locals'
The name that will be used for the object that holds the local variables. You can replace this value with your own and later use it inside the template.
The corresponding value must have a valid JavaScript variable name!
locals
data type Object
|
default {}
Sending your own values to an object that stores local variables that will be available to you inside the template in the locals
object (or under the name you could specify in the localsName
property)
It is important to know that the plug-in already has a certain set of properties and methods that will be added to this object. So that there are no conflicts and overwriting - check out the locals API.
compileDebug
data type boolean
|
default false
When disabled, debugging tools are not compiled, which allows you to speed up the render process a little. The specified value will be converted to Boolean.
It is important to know that if an error occurs, the plugin will not be able to give an explanation of the failure if the value is false
. Therefore, in case of an error, the plug-in will render the current page render again with the parameter enabled, in order to find out what went wrong and to output the maximum report on the errors found.
If you use the watch task for the render - after correcting the error, the parameter will again have the same value.
In very specific situations, the re-renderer may not correctly detect errors,
because of a repeated pass, in which, for example, some value can be overridden, and so on.
If this happens - run the task immediately with the enabled parametercompileDebug
showHistory
data type boolean
|
default false
Displays the render history after completing work with each page.
showHistoryOnCrash
data type boolean
|
default false
Displays the render history on error.
escape
data type function
|
default undefined
Options
Name | Type | Description
--- | --- | ---
markup
| string
| Markup inside the structure
Your own escaping function used with the <% =
construct, which must return a string.
afterRender
data type function
|
default undefined
The method that will be called after the page renderer with its layouts.
Options
Name | Type | Description
--- | --- | ---
markup
| string
| Final page markup
file
| Object
| Current renderer file
sources
| Array.<string>
| The paths of all the connected files during the rendering of the current file, including the path to the current page (the first one in the list)
Using the afterRender
method, you can change the markup, for example, format with js-beautify
and return a new result using return
or use the method to set watches on dependent files for each page separately.
Example of formatting markup
const gulp = require('gulp');
const gulpEjsMonster = require('gulp-ejs-monster');
const jsBeautify = require('js-beautify').html;
const options = {
afterRender (markup) {
return jsBeautify.html(markup, /* jsBeautify options */);
}
};
gulp.task('ejs', function() {
return gulp.src('./src/ejs/*.ejs')
.pipe(gulpEjsMonster(options).on('error', gulpEjsMonster.preventCrash))
.pipe(gulp.dest('./dist/'));
});
Example of setting watches on dependent files
const gulp = require('gulp');
const gulpEjsMonster = require('gulp-ejs-monster');
const gulpWatchAndTouch = require('gulp-watch-and-touch');
const ejsFileWatcher = gulpWatchAndTouch(gulp);
const watchTask = true;
const options = {
afterRender (markup, file, sources) {
if (watchTask) {
let filePath = sources.shift(); // remove path of current view
let newImports = ejsFileWatcher(filePath, filePath, sources);
if (newImports) {
console.log(`${file.stem} has new imports`);
}
}
}
};
gulp.task('ejs', function() {
return gulp.src('./src/ejs/*.ejs')
.pipe(gulpEjsMonster(options).on('error', gulpEjsMonster.preventCrash))
.pipe(gulp.dest('./dist/'));
});
gulp.task('ejs-watch', function() {
gulp.watch('./src/ejs/*.ejs', gulp.series('ejs')); // gulp#4.x
});
Rendering error reports
We also emphasized the output of the maximum error reports that can occur when rendering pages so you can understand what went wrong.
If you fail, you will receive a report group:
render history
The rendering history of the current page, with the help of which you can track the sequence of plug-in actions
Note! since 3.1.0
The history is displayed only when the showHistoryOnCrash parameter is turned on
Render history:
Start
render view - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\index.ejs
> set layout - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\_layouts\base.ejs
> render widget - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\_widgets\demo.ejs
caching new file content
√ file changed
! render file content
> render widget - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\_widgets\demo.ejs
getting file content from cache
! file not changed
! render file content
> require node module "lodash"
caching new file content
> require file - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\_requires\data.js
√ file changed
caching new file content
> require file - C:\Wezom\NodeModules\gulp-ejs-monster\examples\src\_requires\component.js
→ CRASH...
ejs report
The native ejs
report on the error found
The availability of the following reports will depend on the error itself and the file in which it occurred
fs report
If the file you are looking for was not found.
ejs-lint report
If the error is in the * .ejs
file - it will lint file by EJS-Lint
, to detect possible errors.
esprima report
If the error is in the * .js
file - it will test file by esprima
, to detect possible errors.
json-lint report
If the error is in the * .json
file - it will lint file by json-lint
, to detect possible errors.
locals API
locals
- is a single global object that contains local values, which will be available inside ejs
.
Properties
locals.body
data type string
Content of the current page, for insertion inside the layouts.
Accordingly, the property is available only within the layouts!
Example of use
<!-- view index.ejs -->
<% locals.setLayout('base.ejs') %>
<h1>Index view</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<!-- layout base.ejs -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<%- locals.body %>
</body>
</html>
locals.blocks
data type Object
List of assembled blocks that are created using the method locals.block()
Example of use
<!-- view index.ejs -->
<% locals.setLayout('base.ejs') %>
<% locals.block('title', 'Index view') %>
<% locals.block('header', '<h1>Index view header</h1>') %>
<!-- view news.ejs -->
<% locals.setLayout('base.ejs') %>
<% locals.block('title', 'Last News') %>
<% locals.block('header', '<h1>News view header</h1>') %>
<!-- layout base.ejs -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><%- locals.blocks.title %></title>
</head>
<body>
<%- locals.blocks.header %>
<%- locals.body %>
</body>
</html>
locals.entry
data type Object
Incoming properties for the current widget.
Accordingly, the property is available only inside the widgets!
Example of use
<!-- view index.ejs -->
<%- locals.widget('test.ejs') %>
<%- locals.widget('test.ejs', {title: 'Custom title'}) %>
<!-- widget test.ejs -->
<% let title = locals.entry.title || 'Default title'; %>
<h1><%- title) %></h1>
Example with the use of es6 destructuring
<!-- widget test.ejs -->
<% let {title = 'Default title'} = locals.entry; %>
<h1><%- title) %></h1>
locals.viewName
data type string
The name of the current rendering page (without the extension).
Regardless of the current widget, the include, layout and so on.
locals.viewPath
data type string
Absolute path to the current render page in your file system.
Regardless of the current widget, the include, layout and so on.
locals.fileChanged
data type boolean
Flag, whether the file changed after the last access to it.
The property is available inside the widgets. On the main render pages and their layouts, the property is also available, but for them it will always be true
.
Methods
locals.setLayout (filePath)
Sets the path to the layout for the current page.
Options
Name | Type | Description
--- | --- | ---
filePath
| string
| The path to the file (with the extension) relative to the directory specified in the parameterlayouts
locals.widget (filePath [, relativeFolderPath] [, entry] [, cacheRenderResult]) → string
Connecting the markup widget.
Options
Name | Type | Attributes | Default | Description
--- | --- | --- | --- | ---
filePath
| string
| | | The path to the file (with the extension) relative to the directory specified in the parameter widgets
relativeFolderPath
| string
| <optional> | | The relative path from which to connect the specified file, ignoring the widgets, If the parameter is not equal to a string, then it is perceived as entry
entry
| Object
| <optional> | {}
| Incoming data that is passed to the widget, If the parameter relativeFolderPath
is not equal to a string and the third parameter is equal to the logical value, then it is perceived as cacheRenderResult
cacheRenderResult
| boolean
| <optional> | false
| Cache the result of the renderer.
Returns
- data type:
string
- description: rendering ejs markup
Inside the widget, you can accept incoming data from the locals.entry
.
Caching the result of the renderer will allow you to store the received string as ready static markup and insert it on subsequent calls on the page without compilation. To do this in the next widgets, you must also specify cacheRenderResult
. Otherwise, the render will be performed again for the current call.
If you change the file of the widget itself (change the modification date), the cache will be reset.
This approach can also be used for several pages in the overall rendering task.
For example - the first page, index.ejs
, render the block of code, a news.ejs
, which goes after, will already take the cached result.
Example of use
<!-- cache at first render -->
<%- locals.widget('big-rendering-markup.ejs', {/*data*/}, true) %>
<!-- get cached render result from first render -->
<%- locals.widget('big-rendering-markup.ejs', {/*data*/}, true) %>
<!-- new render result -->
<%- locals.widget('big-rendering-markup.ejs', {/*data*/}) %>
<!-- get cached render result from first render -->
<%- locals.widget('big-rendering-markup.ejs', {/*data*/}, true) %>
locals.requireNodeModule (moduleName) → *
Connecting modules from installed node_modules
Options
Name | Type | Description
--- | --- | ---
moduleName
| string
| Module name
Returns
- data type:
*
- description: Connected module
Example of use
<%
let lodash = locals.requireNodeModule('lodash');
lodash.cloneDeep(options);
lodash.isPlainObject(data);
%>
locals.require (filePath [, relativeFolderPath]) → *
Connect your own executable js/json files with CommonJS support for export.
Options
Name | Type | Attributes | Default | Description
--- | --- | --- | --- | ---
filePath
| string
| | | The path to the file (with the extension) relative to the directory specified in the parameter requires
relativeFolderPath
| string
| <optional> | | The relative path from which to connect the specified file, ignoring the requires
Returns
- data type:
*
- description: Connected file
Inside such files, the locals
object is not available. You can transfer it to the file, for example, if you export a method:
Variant 1. Bind the context for the method
<%
let component = locals.require('component.js').bind(locals);
component('Hello');
%>
// component.js
function component (message) {
console.log(this);
console.log(message);
// require another components and files
let anotherComponent = this.require('another-component.js').bind(this);
let data = this.require('config.json');
// ...
}
module.exports = component;
Variant 2. Use currying
<%
let component = locals.require('component.js')(locals);
component('Hello');
%>
// component.js
// Function wrapper
function componentWrapper (locals) {
// component
function component (message) {
console.log(locals);
console.log(message);
// require another components and files
let anotherComponent = locals.require('another-component.js')(locals);
let data = this.require('config.json');
// ...
}
return component;
}
module.exports = componentWrapper;
locals.include (filePath [, relativeFolderPath]) → Object
Includes the text content of the file in your markup as is.
Options
Name | Type | Attributes | Default | Description
--- | --- | --- | --- | ---
filePath
| string
| | | The path to the file (with the extension) relative to the directory specified in the parameter includes
relativeFolderPath
| string
| <optional> | | The relative path from which to connect the specified file, ignoring the includes
Returns
- data type:
Object
- description: The object has a set of properties
changed
- flag, if the file is changed.mtime
- The date of the last modification of the filecontent
- the content of the filetoString()
- own method of casting to a string that returnsthis.content
, so if you execute the method in the context of the insertion in the markup - the result will immediately be the content of the file.
Example of use
<!-- include css file -->
<style><%- locals.include('critical.css') %></style>
Example of creating a component with conversion md to html
// requires/components/md2html.js
function createMd2HtmlComponent (locals) {
const marked = locals.requireNodeModule('marked');
const lodash = locals.requireNodeModule('lodash');
const defaultOptions = {
render: false,
gfm: true,
tables: true,
breaks: true,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: true
};
/**
* Convert md 2 html
* @param {string} filePath
* @param {Object} [options={}]
* @returns {string} converted html markup
*/
function md2html (filePath, options = {}) {
let mdFile = locals.include(filePath);
if (mdFile.changed) {
let markedOptions = lodash.merge({}, defaultOptions, options);
// rewrite cached file content until it not changed
mdFile.content = marked(mdFile.content, markedOptions);
}
return mdFile;
}
return md2html;
}
module.exports = createMd2HtmlComponent;
// requires/extend-locals.js
function extendLocals (locals) {
if (!locals.hasOwnProperty('com')) {
locals.com = {};
}
locals.com.md2html = locals.com.md2html || locals.require('components/md2html.js')(locals);
// set other components inside render
// ...
}
module.exports = extendLocals;
# icludes/about-us.md
[Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet)
# H1
## H2
### H3
#### H4
##### H5
###### H6
Alternatively, for H1 and H2, an underline-ish style:
Alt-H1
======
Alt-H2
------
<!-- view index.ejs -->
<% locals.setLayout('base.ejs'); -%>
<% locals.require('extend-locals.js')(locals); -%>
<div class="container">
<article class="wysiwyg">
<%- locals.com.md2html('about-us.md') %>
</article>
</div>
locals.block (blockName, markup [, mtd]) → Block
Specify the markup block that will be available in the block list.
Options
Name | Type | Attributes | Default | Description
--- | --- | --- | --- | ---
blockName
| string
| | | The name of the block that can be accessed in the block list
markup
| string
| | | Value of the block
mtd
| string
| <optional> | 'replace'
| Method for specifying a value for a block.
Returns
- data type:
string
- description: Value of the block
When specifying a value for a block, an array is formed, which, when printed, is joined to a string.
Methods for specifying a value for a block:
'replace'
- replace the previous value if it was. If not then just assign a new value.'append'
- add a new value to the end of the array.'prepend'
- add a new value to the beginning of the array.
Example of using addition methods
<% locals.block(headers, '<h2>Ipsum</h2>') %>
...
<% locals.block(headers, '<h3>Dolor</h3>', 'append') %>
...
<% locals.block(headers, '<h1>Lorem</h1>', 'prepend') %>
...
<%- locals.blocks.header %> // => ['<h1>Lorem</h1>', '<h2>Ipsum</h2>', '<h3>Dolor</h3>'].join('\n');