├── .babelrc ├── .editorconfig ├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── coffeelint.json ├── package-lock.json ├── package.json ├── src ├── cache.js ├── index.js └── render.js └── test ├── config └── paths.coffee ├── fixtures ├── bad-locals.ejs ├── bad-locals.js ├── bad-syntax.ejs ├── bad-syntax.js ├── context.ejs ├── context.js ├── delimiter.ejs ├── delimiter.js ├── include-partial.ejs ├── include.ejs ├── include.js ├── resource-query.ejs ├── resource-query.js ├── simple.ejs └── simple.js ├── helpers ├── compile.coffee ├── config.coffee └── error.coffee ├── index.coffee └── mocha.opts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", {"targts": {"node": "current"}}] 4 | ], 5 | "plugins": [ 6 | "add-module-exports" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: eslint:recommended 2 | parserOptions: 3 | ecmaVersion: 6 4 | sourceType: module 5 | env: 6 | node: true 7 | mocha: true 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /lib/ 3 | /node_modules/ 4 | /npm-debug.* 5 | .tmp 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /src/ 3 | /test/ 4 | /.babelrc 5 | /.editorconfig 6 | /.eslintrc.yml 7 | /.gitignore 8 | /.travis.yml 9 | /coffeelint.json 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | after_success: 5 | - npm run coveralls 6 | before_deploy: 7 | - npm run build 8 | deploy: 9 | provider: npm 10 | email: 11 | secure: E1X0Di6u1dhOXAvEsGXTTMZzz0Cx7RqNS9O4U5Tgdqlqaq9maDoHz8rZNHwqcx97m6DkgH4/go+D22DARLPr7Y+5XJNBrFcDKWPbU34lBHsTjwhxnuoKS8h7VZ/lGvQqPp1ircyYvsG5rd97J4z0hI27gF3Sj9tk8W3bn3ZTc8JxIoEw00aiEC7KNNnDpOosWkHv+SiVASo5Fk9Iwdt+k8FRtmrfmjVb/K1qp8e4lkjN8sXtxLoAYqzuOt5TUpTFDzGjlY4KS42apJxq0cKTvPXsU38Wn4QE848E4urJAIHKLR8N45wapD54FxlvS4FKjWUCpscVf8vDAGPXPbFK0uireqlI1RdHZYhEKeyR0vnIC62lQbYd9GKrh26q9TVeGniKXfIpNC/j5gp6dexYkKqNjmWE5K1CgTplgLq5ufmZArkeS+hKrSGcdGYcfib5hGM3gEfisK0Mvm0/rRposChu3UwUh9IlEblKhGbsynrs8Ee8zSerrTPnuaFuJycDO7m+SpxBe5ujf7W4/Dhh8etCsNIG0f9xxlVSfwgeeLONs1z1i94g7gGHR4n1JTW4/xLhyJuE4jsvEm23y4M1nDwEOCiPw1oJvCASXxxKN5/ydQ0wJsV6t7R0RFwDQSbOA4FYlYsWQQzmqgLJ8BES2JgAEuFhCEHpT/K4lVZl9oA= 12 | api_key: 13 | secure: YGkGJVV8Wx86+4fy8k88vLBku/JU7402q+fFS7VFLa1Lt017N9qsIasJgSiUWBsmRbp6L8FqT77yv7BZsyn3swdeFk/Uz8Le3lKQgANFAy7JQj/4MaceZsGxvuOU1+PHsFMqZDlIQpTzkaiPN6bVCj/nrxspPfTEiaDThsNPy/BNXjoPMnwvg8PuRgotJSHEx1vWlemnLql69RnfoWO0dqO1RY8To8wNjPJVQtMsOGgdDbg1Gi6FAdp3YltVbCIl9XSn0ibjo+sEPfHEhToPszlbyExm4GelocK3qCFowTE6YMO/PHNXq12oPI/mOB0VESyNeawnT6MYQYI+ClgWS8KXGaZVUuOJ3s9nkPODMjTBmx7TElPWBmNNtutOI12VxTmXKs+gKynJuEf7T9xIgunIuzDOFZR/OflT1qySQoVGeYBJCjYeG2IA0hIVcnVHmYBybVuw6ut5A1N5fORxVS+yToJRfR8IrU6dmuhd61nffoAPJ81TzOvcGIldKuN0g6wfc40EaJQr+QZgUvBPp72jffajP0DviQ3RBx0y3ktikqvetm0TuXc9FzLEkjBKJY4r2BKahTcqv9YyPI4Ui9vNvjaD2VG4Qa7HtsUNi342/uaVnRMf24/VI/UOkhgI8e2DDKH35RucLK0sxrEEUfTwqNM2jhBuNQHA+BDpyAg= 14 | skip_cleanup: true 15 | on: 16 | repo: mcmath/ejs-html-loader 17 | node: "6" 18 | tags: true 19 | after_deploy: 20 | - npm run unbuild 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2017 Akim McMath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ejs-html-loader 2 | 3 | [![Version][version-badge]][npm] 4 | [![Build][build-badge]][travis] 5 | [![Coverage][coverage-badge]][coveralls] 6 | 7 | [Webpack][webpack] loader for rendering plain HTML from [EJS][ejs] 8 | template files 9 | 10 | ## Install 11 | 12 | Install with [npm][npm]. Ensure [EJS][ejs] and [Webpack][webpack] are installed 13 | as well, as these are peer dependencies. 14 | 15 | ```sh 16 | npm install --save-dev webpack ejs ejs-html-loader 17 | ``` 18 | 19 | ## Usage 20 | 21 | In your Webpack [configuration][webpack-configuration], pass data to your 22 | templates through either an `'options'` object or as query parameters. 23 | 24 | ```js 25 | module.exports = { 26 | // ... 27 | module: { 28 | rules: [{ 29 | test: /\.ejs$/, 30 | loader: 'ejs-html-loader', 31 | options: { 32 | title: 'The Ant: An Introduction', 33 | season: 1, 34 | episode: 9, 35 | production: process.env.ENV === 'production' 36 | } 37 | }] 38 | } 39 | }; 40 | ``` 41 | 42 | Data may also be passed through a resource query. These data take precedence 43 | over any options with the same name. 44 | 45 | ```js 46 | import "./index.ejs?page=home"; 47 | ``` 48 | 49 | ## Options 50 | 51 | All properties passed as loader options will be available to your 52 | templates as local variables. In addition, the following [EJS][ejs] 53 | options may be set: 54 | 55 | * **context** : `object`
56 | The value of `this` in your templates. If specified, its properties will be 57 | available in your templates, e.g. `<%= this.somePropery %>`. 58 | 59 | * **delimiter** : `string='%'`
60 | Character used inside of angle brackets marking opening/closing tags. 61 | Defaults to `'%'`, as in `<%= some.variable %>`. 62 | 63 | For example: 64 | 65 | ```js 66 | { 67 | // ... 68 | options: { 69 | delimiter: '$', 70 | title: 'The Naked Ant', 71 | season: 1, 72 | episode: 12 73 | } 74 | } 75 | ``` 76 | 77 | ## Includes 78 | 79 | The EJS `filename` option is set automatically, so you may include partials 80 | relative to your template files. If you want your included files to 81 | automatically recompile in watch mode, be sure to use the following syntax: 82 | 83 | ``` 84 | <% include some/file %> 85 | ``` 86 | 87 | ## License 88 | 89 | Copyright © 2016–2019 Akim McMath. Licensed under the [MIT License][license]. 90 | 91 | [version-badge]: https://img.shields.io/npm/v/ejs-html-loader.svg?style=flat-square 92 | [build-badge]: https://img.shields.io/travis/mcmath/ejs-html-loader/master.svg?style=flat-square 93 | [coverage-badge]: https://img.shields.io/coveralls/mcmath/ejs-html-loader/master.svg?style=flat-square&service=github 94 | [dependencies-badge]: https://img.shields.io/gemnasium/mcmath/ejs-html-loader.svg?style=flat-square 95 | 96 | [npm]: https://www.npmjs.com/package/ejs-html-loader 97 | [license]: LICENSE 98 | [travis]: https://travis-ci.org/mcmath/ejs-html-loader 99 | [coveralls]: https://coveralls.io/github/mcmath/ejs-html-loader?branch=master 100 | [gemnasium]: https://gemnasium.com/mcmath/ejs-html-loader 101 | [webpack]: https://webpack.js.org/ 102 | [webpack-configuration]: https://webpack.js.org/configuration/ 103 | [ejs]: http://ejs.co/ 104 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@mcmath/coffeelint-config" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ejs-html-loader", 3 | "version": "4.0.1", 4 | "description": "Webpack loader for rendering HTML from EJS templates", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "npm run unbuild && babel src -d lib", 8 | "unbuild": "rimraf lib", 9 | "test:lint": "eslint src test && coffeelint -q test", 10 | "test:unit": "istanbul cover _mocha -- test/index.coffee", 11 | "test:report": "npm run test:unit && open coverage/lcov-report/index.html", 12 | "test": "npm run test:lint && npm run test:unit", 13 | "coveralls": "cat coverage/lcov.info | coveralls" 14 | }, 15 | "engines": { 16 | "node": ">=6" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/mcmath/ejs-html-loader.git" 21 | }, 22 | "keywords": [ 23 | "webpack", 24 | "loader", 25 | "webpack-loader", 26 | "ejs", 27 | "html", 28 | "template" 29 | ], 30 | "author": "Akim McMath (http://www.mcmath.io/)", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/mcmath/ejs-html-loader/issues" 34 | }, 35 | "homepage": "https://github.com/mcmath/ejs-html-loader#readme", 36 | "peerDependencies": { 37 | "webpack": "2.x - 4.x", 38 | "ejs": "2.x" 39 | }, 40 | "dependencies": { 41 | "loader-utils": "^1.2.3" 42 | }, 43 | "devDependencies": { 44 | "@mcmath/coffeelint-config": "^1.0.1", 45 | "babel-cli": "^6.26.0", 46 | "babel-plugin-add-module-exports": "^1.0.0", 47 | "babel-preset-env": "^1.7.0", 48 | "babel-register": "^6.26.0", 49 | "chai": "^4.2.0", 50 | "coffeelint": "^2.1.0", 51 | "coffeescript": "^2.3.2", 52 | "coveralls": "^3.0.2", 53 | "ejs": "^2.6.1", 54 | "eslint": "^5.12.1", 55 | "file-loader": "^3.0.1", 56 | "istanbul": "1.1.0-alpha.1", 57 | "mocha": "^5.2.0", 58 | "relative-path-map": "^1.0.1", 59 | "rimraf": "^2.6.3", 60 | "webpack": "^4.29.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/cache.js: -------------------------------------------------------------------------------- 1 | const cache = {}; 2 | 3 | export function addDependencies(ctx) { 4 | add(ctx, get(ctx)); 5 | } 6 | 7 | export function saveDependencies(ctx, deps) { 8 | add(ctx, save(ctx, deps)); 9 | } 10 | 11 | function get(ctx) { 12 | return cache[ctx.resourcePath] || []; 13 | } 14 | 15 | function save(ctx, deps=[]) { 16 | cache[ctx.resourcePath] = deps; 17 | return get(ctx); 18 | } 19 | 20 | function add(ctx, deps) { 21 | deps.forEach(name => { 22 | ctx.addDependency(name); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {getOptions, parseQuery} from 'loader-utils'; 2 | import {addDependencies, saveDependencies} from './cache'; 3 | import {render} from './render'; 4 | 5 | export default function ejsHtmlLoader(src) { 6 | this.cacheable(); 7 | let rendered = ''; 8 | 9 | try { 10 | rendered = renderTemplate(this, src); 11 | } catch (e) { 12 | emitError(this, e.message); 13 | } 14 | 15 | return rendered; 16 | } 17 | 18 | function renderTemplate(ctx, src) { 19 | let data = getData(ctx); 20 | let {rendered, deps} = render(ctx, src, data); 21 | 22 | saveDependencies(ctx, deps); 23 | 24 | return rendered; 25 | } 26 | 27 | function getData(ctx) { 28 | return Object.assign({}, getOptions(ctx), getResourceQuery(ctx)); 29 | } 30 | 31 | function getResourceQuery(ctx) { 32 | return parseQuery(ctx.resourceQuery || "?"); 33 | } 34 | 35 | function emitError(ctx, msg) { 36 | addDependencies(ctx); 37 | ctx.emitError(`ejs-html-loader\n${msg}`); 38 | } 39 | -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | import {readFileSync} from 'fs'; 2 | import {compile} from 'ejs'; 3 | 4 | export function render(ctx, src, data) { 5 | let filename = ctx.resourcePath; 6 | let delimiter = data.delimiter; 7 | let context = data.context; 8 | 9 | let tpl = compile(src, {filename, delimiter, context}); 10 | 11 | return { 12 | rendered: tpl(data), 13 | deps: getDeps(tpl.dependencies, {delimiter, context}) 14 | }; 15 | } 16 | 17 | function getDeps(deps, opts, result=[]) { 18 | deps.forEach(name => { 19 | result.push(name); 20 | getDeps(getOwnDeps(name, opts), opts, result); 21 | }); 22 | 23 | return result; 24 | } 25 | 26 | function getOwnDeps(filename, {delimiter, context}) { 27 | let src = readFileSync(filename, 'utf8'); 28 | let tpl = compile(src, {filename, delimiter, context}); 29 | return tpl.dependencies; 30 | } 31 | -------------------------------------------------------------------------------- /test/config/paths.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | pathMap = require 'relative-path-map' 3 | 4 | module.exports = pathMap( 5 | root: path.resolve __dirname, '../..' 6 | src: '[root]/src/index.js' 7 | test: '[root]/test' 8 | fixt: '[test]/fixtures' 9 | out: '[test]/.tmp' 10 | outFile: '[out]/index.html' 11 | ) 12 | -------------------------------------------------------------------------------- /test/fixtures/bad-locals.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple 6 | 7 | 8 |

<%= headingheadingheading %>

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/bad-locals.js: -------------------------------------------------------------------------------- 1 | require('./bad-locals.ejs'); 2 | -------------------------------------------------------------------------------- /test/fixtures/bad-syntax.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 |

<%= heading =>

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/bad-syntax.js: -------------------------------------------------------------------------------- 1 | require('./bad-syntax.ejs'); 2 | -------------------------------------------------------------------------------- /test/fixtures/context.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple 6 | 7 | 8 |

<%= this.heading %>

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/context.js: -------------------------------------------------------------------------------- 1 | require('./context.ejs'); 2 | -------------------------------------------------------------------------------- /test/fixtures/delimiter.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple 6 | 7 | 8 |

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/delimiter.js: -------------------------------------------------------------------------------- 1 | require('./delimiter.ejs'); 2 | -------------------------------------------------------------------------------- /test/fixtures/include-partial.ejs: -------------------------------------------------------------------------------- 1 |

<%= two %>

2 | -------------------------------------------------------------------------------- /test/fixtures/include.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple 6 | 7 | 8 |

<%= one %>

9 | <% include include-partial %> 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/include.js: -------------------------------------------------------------------------------- 1 | require('./include.ejs'); 2 | -------------------------------------------------------------------------------- /test/fixtures/resource-query.ejs: -------------------------------------------------------------------------------- 1 | <%- one %><%- two %><%- three %> 2 | -------------------------------------------------------------------------------- /test/fixtures/resource-query.js: -------------------------------------------------------------------------------- 1 | require("./resource-query.ejs?one=abc&two=xyz"); 2 | -------------------------------------------------------------------------------- /test/fixtures/simple.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple 6 | 7 | 8 |

<%= heading %>

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/simple.js: -------------------------------------------------------------------------------- 1 | require('./simple.ejs'); 2 | -------------------------------------------------------------------------------- /test/helpers/compile.coffee: -------------------------------------------------------------------------------- 1 | {readFile} = require 'fs' 2 | {expect} = require 'chai' 3 | webpack = require 'webpack' 4 | paths = require '../config/paths' 5 | config = require './config' 6 | 7 | module.exports = (match, opts) -> 8 | new Promise((resolve) -> 9 | webpack config(opts), (err) -> 10 | expect(err).not.to.exist 11 | 12 | readFile paths.outFile, 'utf8', (err, content) -> 13 | expect(err).not.to.exist 14 | expect(content).to.match match 15 | 16 | resolve() 17 | ) 18 | -------------------------------------------------------------------------------- /test/helpers/config.coffee: -------------------------------------------------------------------------------- 1 | {resolve} = require 'path' 2 | paths = require '../config/paths' 3 | 4 | module.exports = (opts) -> 5 | context: paths.fixt 6 | entry: resolve paths.fixt, opts.entry 7 | output: 8 | path: paths.out 9 | filename: 'bundle.js' 10 | module: 11 | rules: [ 12 | test: /\.ejs$/ 13 | use: 14 | loader: 'file-loader?name=index.html' 15 | , 16 | loader: if opts.query then "#{paths.src}?#{opts.query}" else paths.src, 17 | options: opts.ejsHtml 18 | ] 19 | -------------------------------------------------------------------------------- /test/helpers/error.coffee: -------------------------------------------------------------------------------- 1 | {expect} = require 'chai' 2 | webpack = require 'webpack' 3 | config = require './config' 4 | 5 | module.exports = (match, opts, done) -> 6 | new Promise((resolve) -> 7 | webpack config(opts), (err, stats) -> 8 | expect(err).not.to.exist 9 | expect(stats.toString()).to.match match 10 | 11 | resolve() 12 | ) 13 | -------------------------------------------------------------------------------- /test/index.coffee: -------------------------------------------------------------------------------- 1 | chai = require 'chai' 2 | rimraf = require 'rimraf' 3 | ejsHtmlLoader = require '../src' 4 | paths = require './config/paths' 5 | compile = require './helpers/compile' 6 | error = require './helpers/error' 7 | 8 | before -> 9 | chai.should() 10 | 11 | describe 'ejs-html-loader', -> 12 | 13 | afterEach (done) -> 14 | rimraf paths.out, done 15 | 16 | it 'exports a function', -> 17 | ejsHtmlLoader.should.be.a 'function' 18 | 19 | context 'options passed as ejsHtml property', -> 20 | 21 | it 'renders template', -> 22 | compile /tuvalu/, 23 | entry: 'simple.js' 24 | ejsHtml: 25 | heading: 'tuvalu' 26 | 27 | context 'options passed as query string', -> 28 | 29 | it 'renders template', -> 30 | compile /togo/, 31 | entry: 'simple.js' 32 | query: 'heading=togo' 33 | 34 | context 'options passed through resource query', -> 35 | 36 | it 'renders template', -> 37 | compile /abcxyz123/, 38 | entry: 'resource-query.js' 39 | ejsHtml: 40 | two: '987' 41 | three: '123' 42 | 43 | context 'with include statement', -> 44 | 45 | it 'includes partial', -> 46 | compile /trinidad[\s\S]*tobago/, 47 | entry: 'include.js' 48 | ejsHtml: 49 | one: 'trinidad' 50 | two: 'tobago' 51 | 52 | context 'option: delimiter', -> 53 | 54 | it 'sets EJS delimiter option', -> 55 | compile /tajikistan/, 56 | entry: 'delimiter.js' 57 | ejsHtml: 58 | heading: 'tajikistan' 59 | delimiter: '?' 60 | 61 | context 'option: context', -> 62 | 63 | it 'sets EJS context option', -> 64 | compile /tibet/, 65 | entry: 'context.js' 66 | ejsHtml: 67 | context: 68 | heading: 'tibet' 69 | 70 | context 'error: undelcared locals', -> 71 | 72 | it 'emits error', -> 73 | error /ejs-html-loader/, 74 | entry: 'bad-locals.js' 75 | ejsHtml: 76 | heading: 'tunisia' 77 | 78 | context 'error: invalaid syntax', -> 79 | 80 | it 'emits error', -> 81 | error /ejs-html-loader/, 82 | entry: 'bad-syntax.js' 83 | ejsHtml: 84 | heading: 'turkey' 85 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --reporter spec 3 | --require babel-core/register 4 | --require coffeescript/register 5 | --------------------------------------------------------------------------------