Nourishing Plushie Monster

    gulp-ejs-monster

    3.2.3 • Public • Published

    gulp-ejs-monster

    npm es2015 license Build Status

    🇺🇸 English | 🇷🇺 Русский язык

    Gulp plugin for ejs with steroids. The project is inspired by ejs-locals

    js happiness style


    Table of contents

    1. Thanks
    2. Why this plugin was created?
    3. Example of using the plugin
    4. gulpEjsMonster()
    5. locals API
    6. Project Info

    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:

    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 parameter compileDebug

    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 file
      • content - the content of the file
      • toString() - own method of casting to a string that returns this.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');

    Project Info

    Install

    npm i gulp-ejs-monster

    DownloadsWeekly Downloads

    51

    Version

    3.2.3

    License

    MIT

    Unpacked Size

    59.6 kB

    Total Files

    17

    Last publish

    Collaborators

    • dutchenkooleg