├── .eslintrc ├── .gitignore ├── .npmrc ├── .travis.yml ├── LICENSE ├── README.md ├── __specs__ ├── index.spec.js ├── mock.js └── templates │ ├── embed-base.twig │ ├── embed-ignore-missing.twig │ ├── embed-include.twig │ ├── embed-simple.twig │ ├── error-compile.twig │ ├── extendee.twig │ ├── extender.twig │ ├── from.twig │ ├── import.twig │ ├── include.twig │ ├── macro-self.twig │ ├── macro-wrapped.twig │ ├── macro.twig │ └── test.twig ├── examples ├── express │ ├── .gitignore │ ├── .npmrc │ ├── package.json │ ├── src │ │ ├── index.js │ │ └── views │ │ │ ├── index.twig │ │ │ └── layout.twig │ └── webpack.config.js ├── frontend │ ├── .gitignore │ ├── .npmrc │ ├── package.json │ ├── src │ │ ├── index.html │ │ └── js │ │ │ ├── index.js │ │ │ └── views │ │ │ ├── index.twig │ │ │ └── layout.twig │ └── webpack.config.js ├── namespaces │ ├── .gitignore │ ├── .npmrc │ ├── package.json │ ├── src │ │ ├── index.js │ │ └── views │ │ │ ├── include │ │ │ ├── head.twig │ │ │ └── layout.twig │ │ │ └── index.twig │ └── webpack.config.js └── typescript │ ├── .gitignore │ ├── .npmrc │ ├── package.json │ ├── src │ ├── index.ts │ └── views │ │ ├── index.twig │ │ └── layout.twig │ ├── tsconfig.json │ └── webpack.config.js ├── index.js ├── package.json └── typings ├── express.d.ts ├── index.d.ts └── twig.d.ts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base/legacy", 3 | 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "ecmaFeatures": { 7 | "experimentalObjectRestSpread": true 8 | } 9 | }, 10 | 11 | "env": { 12 | "node": true, 13 | "jasmine": true, 14 | "es6": true 15 | }, 16 | 17 | "rules": { 18 | "class-methods-use-this": 0, 19 | "comma-dangle": ["error", { 20 | "arrays": "always-multiline", 21 | "objects": "always-multiline", 22 | "imports": "always-multiline", 23 | "exports": "always-multiline", 24 | "functions": "never" 25 | }], 26 | "default-case": 0, 27 | "function-paren-newline": [0, "consistent"], 28 | "key-spacing": [2, { "mode": "minimum" }], 29 | "max-len": [1, 80], 30 | "no-await-in-loop": 0, 31 | "no-console": 0, 32 | "no-multi-spaces": [2, { "exceptions": { 33 | "Property": true, 34 | "VariableDeclarator": true, 35 | "AssignmentExpression": true, 36 | "ObjectExpression": true, 37 | "ClassProperty": true 38 | }}], 39 | "no-param-reassign": 0, 40 | "no-plusplus": 0, 41 | "no-restricted-syntax": [ 42 | "error", 43 | "LabeledStatement", 44 | "WithStatement" 45 | ], 46 | "no-underscore-dangle": 0, 47 | "no-unused-expressions": 0, 48 | "no-use-before-define": 0, 49 | "object-curly-newline": 0, 50 | "spaced-comment": [2, "always", { 51 | "line": { "markers": ["*package", "!", ",", "noinspection"] }, 52 | "block": { 53 | "balanced": true, 54 | "markers": ["*package", "!", ",", "noinspection"], 55 | "exceptions": ["*"] } 56 | }] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "node" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Alexey Prokhorov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twigjs-loader 2 | [![Build Status](https://travis-ci.org/megahertz/twigjs-loader.svg?branch=master)](https://travis-ci.org/megahertz/twigjs-loader) 3 | [![npm version](https://badge.fury.io/js/twigjs-loader.svg)](https://badge.fury.io/js/twigjs-loader) 4 | [![Dependencies status](https://img.shields.io/david/megahertz/twigjs-loader)](https://david-dm.org/megahertz/twigjs-loader) 5 | 6 | ## Description 7 | 8 | twig.js loader for Webpack 9 | 10 | 11 | ## Installation 12 | 13 | This package requires node.js 8 at least. 14 | 15 | Install with [npm](https://npmjs.org/package/twigjs-loader): 16 | 17 | $ npm install -D twigjs-loader 18 | 19 | ## Usage 20 | 21 | ```js 22 | const indexView = require('./index.twig'); 23 | console.log(indexView({ variable1: 'value' })); 24 | ``` 25 | 26 | **webpack.config.js** 27 | 28 | ```js 29 | module.exports = { 30 | //... 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.twig$/, 35 | use: 'twigjs-loader', 36 | }, 37 | //... 38 | ], 39 | }, 40 | //... 41 | } 42 | 43 | ``` 44 | 45 | ### With Express 46 | 47 | - [example](examples/express) 48 | - [typescript example](examples/typescript) 49 | 50 | `$ npm install twigjs-loader` 51 | 52 | **index.js:** 53 | ```js 54 | import * as express from 'express'; 55 | import { ExpressView } from 'twigjs-loader'; 56 | import indexView from './views/index.twig'; 57 | 58 | const app = express(); 59 | app.set('view', ExpressView); 60 | 61 | app.get('/', (req, res) => { 62 | res.render(indexView, { 63 | url: req.originalUrl, 64 | }) 65 | }); 66 | 67 | app.listen(8080); 68 | ``` 69 | 70 | ### On frontend 71 | 72 | - [example](examples/frontend) 73 | 74 | ```js 75 | import indexView from './views/index.twig'; 76 | 77 | document.body.innerHTML = indexView({ 78 | url: location.href, 79 | }) 80 | ``` 81 | 82 | ## Configure 83 | 84 | You can configure how a template is compiled by webpack using the 85 | `renderTemplate` option. For example: 86 | 87 | **webpack.config.js** 88 | 89 | ```js 90 | module.exports = { 91 | //... 92 | module: { 93 | rules: [ 94 | { 95 | test: /\.twig$/, 96 | use: { 97 | loader: 'twigjs-loader', 98 | options: { 99 | /** 100 | * @param {object} twigData Data passed to the Twig.twig function 101 | * @param {string} twigData.id Template id (relative path) 102 | * @param {object} twigData.tokens Parsed AST of a template 103 | * @param {string} dependencies Code which requires related templates 104 | * @param {boolean} isHot Is Hot Module Replacement enabled 105 | * @return {string} 106 | */ 107 | renderTemplate(twigData, dependencies, isHot) { 108 | return ` 109 | ${dependencies} 110 | var twig = require("twig").twig; 111 | var tpl = twig(${JSON.stringify(twigData)}); 112 | module.exports = function(context) { return tpl.render(context); }; 113 | `; 114 | }, 115 | }, 116 | }, 117 | }, 118 | //... 119 | ], 120 | }, 121 | //... 122 | } 123 | 124 | ``` 125 | 126 | ## Path resolving 127 | 128 | The module uses webpack for resolving template path, so it doesn't resolve 129 | path by itself. If you need custom file path resolution (eg namespaces), 130 | check [the example](examples/namespaces). 131 | 132 | ## Credits 133 | 134 | Based on [zimmo-be/twig-loader](https://github.com/zimmo-be/twig-loader) 135 | -------------------------------------------------------------------------------- /__specs__/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { describe, expect, expectAsync, it } = require('humile'); 4 | const { compile, renderOutput } = require('./mock'); 5 | 6 | describe('twigjs_loader', () => { 7 | it('should load embed', async () => { 8 | const out = await compile('templates/embed-simple.twig'); 9 | expect(out).toContain('require("embed-base.twig");'); 10 | expect(out).toContain('require("embed-include.twig");'); 11 | expect(out).toContain('"value":"__specs__/templates/embed-base.twig"'); 12 | 13 | expect(renderOutput(out).trim()).toEqual([ 14 | 'START', 'A', 'new header', 'base footer', 'B', '', 'A', 'base header', 15 | 'base footer', 'extended', 'B', '', 'A', 'base header', 'extended', 16 | 'base footer', 'extended', 'B', '', 'A', 'Super cool new header', 17 | 'Cool footer', 'B', 'END', 18 | ].join('\n')); 19 | }); 20 | 21 | it('should extend the parent template', async () => { 22 | const out = await compile('templates/extender.twig'); 23 | expect(out).toContain('require("extendee.twig");'); 24 | expect(out).toContain('"value":"__specs__/templates/extendee.twig"'); 25 | 26 | expect(renderOutput(out).trim()).toBe('ok!'); 27 | }); 28 | 29 | it('should import macro', async () => { 30 | const out = await compile('templates/import.twig'); 31 | expect(out).toContain('require("macro.twig");'); 32 | expect(out).toContain('"value":"__specs__/templates/macro.twig"'); 33 | 34 | expect(renderOutput(out).trim()).toBe('Hello World'); 35 | }); 36 | 37 | it('should import selected macros from template', async () => { 38 | const out = await compile('templates/from.twig'); 39 | expect(out).toContain('require("macro-wrapped.twig");'); 40 | expect(out).toContain('"value":"__specs__/templates/macro-wrapped.twig"'); 41 | 42 | expect(renderOutput(out).trim()).toEqual([ 43 | 'Hello Twig.js', 44 | '
' 45 | + '' 46 | + '
' 47 | + '
' 48 | + '' 49 | + '
', 50 | ].join('')); 51 | }); 52 | 53 | it('should load an included template with no context', async () => { 54 | const out = await compile('templates/include.twig'); 55 | expect(out).toContain('require("test.twig");'); 56 | expect(out).toContain('"value":"__specs__/templates/test.twig"'); 57 | 58 | expect(renderOutput(out, { test: 'tst' }).trim()) 59 | .toBe('BeforeTest template = tst\n\nAfter'); 60 | }); 61 | 62 | it('should handle twig exceptions', async () => { 63 | return expectAsync(compile('templates/error-compile.twig')).toBeRejected(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /__specs__/mock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { readFileSync } = require('fs'); 4 | const path = require('path'); 5 | const Twig = require('twig'); 6 | const twigLoader = require('../index'); 7 | 8 | class LoaderMock { 9 | constructor(template) { 10 | this.resource = template; 11 | this.resourcePath = path.resolve(__dirname, template); 12 | this.content = readFileSync(this.resourcePath, 'utf8'); 13 | this.context = path.dirname(this.resourcePath); 14 | } 15 | 16 | addDependency() {} 17 | 18 | async exec() { 19 | return new Promise((resolve, reject) => { 20 | this.async = () => (err, result) => err ? reject(err) : resolve(result); 21 | twigLoader.call(this, this.content); 22 | }); 23 | } 24 | 25 | resolve(context, request, callback) { 26 | callback(null, path.resolve(context, request)); 27 | } 28 | } 29 | 30 | async function compile(templateText) { 31 | const mock = new LoaderMock(templateText); 32 | return mock.exec(); 33 | } 34 | 35 | function renderOutput(output, context = {}) { 36 | const text = output.match(/tpl = twig\((.*)\);/); 37 | 38 | if (!text || !text[1]) { 39 | throw new Error('renderOutput: Couldn\'t extract data'); 40 | } 41 | 42 | const ast = JSON.parse(text[1]); 43 | ast.path = './'; 44 | 45 | const tpl = Twig.twig(ast); 46 | return tpl.render(context); 47 | } 48 | 49 | module.exports.compile = compile; 50 | module.exports.LoaderMock = LoaderMock; 51 | module.exports.renderOutput = renderOutput; 52 | -------------------------------------------------------------------------------- /__specs__/templates/embed-base.twig: -------------------------------------------------------------------------------- 1 | A 2 | {% block header %} 3 | base header 4 | {% endblock %} 5 | {% block footer %} 6 | base footer 7 | {% endblock %} 8 | B 9 | -------------------------------------------------------------------------------- /__specs__/templates/embed-ignore-missing.twig: -------------------------------------------------------------------------------- 1 | ignore-{% embed "embed-not-there.twig" ignore missing %}{% endembed %}missing -------------------------------------------------------------------------------- /__specs__/templates/embed-include.twig: -------------------------------------------------------------------------------- 1 | {% block header %} 2 | Cool header 3 | {% endblock %} 4 | -------------------------------------------------------------------------------- /__specs__/templates/embed-simple.twig: -------------------------------------------------------------------------------- 1 | START 2 | {% embed "embed-base.twig" %} 3 | {% block header %} 4 | new header 5 | {% endblock %} 6 | {% endembed %} 7 | 8 | {% embed "embed-base.twig" %} 9 | {% block footer %} 10 | {{ parent() }}extended 11 | {% endblock %} 12 | {% endembed %} 13 | 14 | {% embed "embed-base.twig" %} 15 | {% block header %} 16 | {{ parent() }}extended 17 | {% endblock %} 18 | 19 | {% block footer %} 20 | {{ parent() }}extended 21 | {% endblock %} 22 | {% endembed %} 23 | 24 | {% embed "embed-base.twig" %} 25 | {% block header %} 26 | {% embed "embed-include.twig" %} 27 | {% block header %} 28 | Super cool new header 29 | {% endblock %} 30 | {% endembed %} 31 | {% endblock %} 32 | {% block footer%} 33 | Cool footer 34 | {% endblock %} 35 | {% endembed %} 36 | END 37 | -------------------------------------------------------------------------------- /__specs__/templates/error-compile.twig: -------------------------------------------------------------------------------- 1 | Should throw a compile error 2 | {{ "foo" bar }} 3 | -------------------------------------------------------------------------------- /__specs__/templates/extendee.twig: -------------------------------------------------------------------------------- 1 | {% macro macro() %}ok!{% endmacro %} 2 | {% import _self as my %} 3 | 4 | {% block content %} 5 |

Sub: content

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /__specs__/templates/extender.twig: -------------------------------------------------------------------------------- 1 | {% extends 'extendee.twig' %} 2 | 3 | {% block content %} 4 | {{ my.macro({}) }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /__specs__/templates/from.twig: -------------------------------------------------------------------------------- 1 | {% from "macro.twig" import echo %}{{ echo("Twig.js") }}{% from "macro-wrapped.twig" import wrapped_input as input, red_input %}{{ input('text') }}{{ red_input('password') }} 2 | -------------------------------------------------------------------------------- /__specs__/templates/import.twig: -------------------------------------------------------------------------------- 1 | {% import "macro.twig" as say %}{{ say.echo('World') }} 2 | -------------------------------------------------------------------------------- /__specs__/templates/include.twig: -------------------------------------------------------------------------------- 1 | Before{% include "test.twig" %}After -------------------------------------------------------------------------------- /__specs__/templates/macro-self.twig: -------------------------------------------------------------------------------- 1 | {% macro input(name, value, type, size) %}{% endmacro %} 2 | {% import _self as forms %}

{{ forms.input('username') }}

3 | -------------------------------------------------------------------------------- /__specs__/templates/macro-wrapped.twig: -------------------------------------------------------------------------------- 1 | {% macro wrapped_input(name, value, type, size) %}{% import "macro-self.twig" as forms %}
{{ forms.input(name, value, type, size) }}
{% endmacro %} 2 | {% macro red_input(name, value, type, size) %}{% import "macro-self.twig" as forms %}
{{ forms.input(name, value, type, size) }}
{% endmacro %} 3 | {% import _self as forms %}

{{ forms.wrapped_input('username') }}

4 | -------------------------------------------------------------------------------- /__specs__/templates/macro.twig: -------------------------------------------------------------------------------- 1 | {% macro echo(name) %}Hello {{ name }}{% endmacro %} 2 | {% macro whitespace_echo( name ) %}Hello {{ name }}{% endmacro %} 3 | -------------------------------------------------------------------------------- /__specs__/templates/test.twig: -------------------------------------------------------------------------------- 1 | Test template = {{ test }} 2 | 3 | {% if flag %}Flag set!{% endif %} -------------------------------------------------------------------------------- /examples/express/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | -------------------------------------------------------------------------------- /examples/express/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twigjs-loader-example-express", 3 | "version": "1.0.0", 4 | "description": "twigjs-loader in express project", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack --mode=development", 8 | "start": "node dist/main.js" 9 | }, 10 | "devDependencies": { 11 | "webpack": "^4.41.2", 12 | "webpack-cli": "^3.3.9", 13 | "webpack-node-externals": "^1.7.2" 14 | }, 15 | "dependencies": { 16 | "express": "^4.17.1", 17 | "twig": "^1.13.3", 18 | "twigjs-loader": "1.x" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/express/src/index.js: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { ExpressView } from "twigjs-loader"; 3 | import indexView from "./views/index.twig"; 4 | 5 | const app = express(); 6 | app.set("view", ExpressView); 7 | 8 | app.get("/", (req, res) => { 9 | res.render(indexView, { 10 | url: `${req.protocol}://${req.get("host")}${req.originalUrl}`, 11 | }) 12 | }); 13 | 14 | const port = process.env.NODE_PORT || 8080; 15 | app.listen(port, () => { 16 | console.log(`Example app listening on port ${port}.`); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/express/src/views/index.twig: -------------------------------------------------------------------------------- 1 | {% extends './layout.twig' %} 2 | 3 | {% block content %} 4 |

Example page

5 |

The current url is: {{ url }}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /examples/express/src/views/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | blah 6 | 7 | 8 | {% block content %}{% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/express/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | devtool: 'source-map', 7 | 8 | externals: [ 9 | nodeExternals(), 10 | ], 11 | 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.twig$/, 16 | use: 'twigjs-loader', 17 | }, 18 | ], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /examples/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | -------------------------------------------------------------------------------- /examples/frontend/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /examples/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twigjs-loader-example-frontend", 3 | "version": "1.0.0", 4 | "description": "twigjs-loader in frontend project", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack --mode=development", 8 | "start": "webpack-dev-server --open" 9 | }, 10 | "devDependencies": { 11 | "twigjs-loader": "1.x", 12 | "webpack": "^4.41.2", 13 | "webpack-cli": "^3.3.9", 14 | "webpack-dev-server": "^3.9.0" 15 | }, 16 | "dependencies": { 17 | "twig": "^1.13.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TwigJS 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/frontend/src/js/index.js: -------------------------------------------------------------------------------- 1 | import indexView from './views/index.twig'; 2 | 3 | document.body.innerHTML = indexView({ 4 | url: location.href, 5 | }) 6 | -------------------------------------------------------------------------------- /examples/frontend/src/js/views/index.twig: -------------------------------------------------------------------------------- 1 | {% extends './layout.twig' %} 2 | 3 | {% block content %} 4 |

Example page

5 |

The current url is: {{ url }}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /examples/frontend/src/js/views/layout.twig: -------------------------------------------------------------------------------- 1 |
2 | {% block content %}{% endblock %} 3 |
4 | -------------------------------------------------------------------------------- /examples/frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | devServer: { 7 | contentBase: path.join(__dirname, 'src'), 8 | }, 9 | 10 | devtool: 'source-map', 11 | 12 | entry: './src/js/index.js', 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.twig$/, 18 | use: 'twigjs-loader', 19 | }, 20 | ], 21 | }, 22 | 23 | resolve: { 24 | extensions: ['.js', '.twig'], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /examples/namespaces/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | -------------------------------------------------------------------------------- /examples/namespaces/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /examples/namespaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twigjs-loader-example-express", 3 | "version": "1.0.0", 4 | "description": "twigjs-loader in express project", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack --mode=development", 8 | "start": "node dist/main.js" 9 | }, 10 | "devDependencies": { 11 | "webpack": "^4.41.2", 12 | "webpack-cli": "^3.3.9", 13 | "webpack-node-externals": "^1.7.2" 14 | }, 15 | "dependencies": { 16 | "express": "^4.17.1", 17 | "twig": "^1.13.3", 18 | "twigjs-loader": "1.x" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/namespaces/src/index.js: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { ExpressView } from '../../../index'; 3 | import indexView from './views/index.twig'; 4 | 5 | const app = express(); 6 | app.set('view', ExpressView); 7 | 8 | app.get('/', (req, res) => { 9 | res.render(indexView, { 10 | url: `${req.protocol}://${req.get('host')}${req.originalUrl}`, 11 | }) 12 | }); 13 | 14 | const port = process.env.NODE_PORT || 8080; 15 | app.listen(port, () => { 16 | console.log(`Example app listening on port ${port}.`); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/namespaces/src/views/include/head.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | blah 4 | 5 | -------------------------------------------------------------------------------- /examples/namespaces/src/views/include/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include 'views::head.twig' %} 5 | 6 | 7 | {% block content %}{% endblock %} 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/namespaces/src/views/index.twig: -------------------------------------------------------------------------------- 1 | {% extends '@views/layout.twig' %} 2 | 3 | {% block content %} 4 |

Example page

5 |

The current url is: {{ url }}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /examples/namespaces/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const nodeExternals = require('webpack-node-externals'); 5 | 6 | const LEGACY_REGEXP = /^(\w+)::/; 7 | 8 | /** 9 | * Transforms legacy namespace::template/path to @namespoace/template/path 10 | */ 11 | class LegacyNsResolverPlugin { 12 | apply(resolver) { 13 | const target = resolver.ensureHook('resolve'); 14 | resolver 15 | .getHook('resolve') 16 | .tapAsync('LegacyNsResolverPlugin', (request, resolveContext, callback) => { 17 | const requestPath = request.request; 18 | if (!requestPath.match(LEGACY_REGEXP)) { 19 | callback(); 20 | return; 21 | } 22 | 23 | const newRequest = { 24 | ...request, 25 | request: requestPath.replace(LEGACY_REGEXP, '@$1/'), 26 | }; 27 | 28 | resolver.doResolve(target, newRequest, null, resolveContext, callback); 29 | }); 30 | } 31 | } 32 | 33 | module.exports = { 34 | devtool: 'source-map', 35 | 36 | externals: [ 37 | nodeExternals(), 38 | ], 39 | 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.twig$/, 44 | use: '../../index', 45 | }, 46 | ], 47 | }, 48 | 49 | resolve: { 50 | alias: { 51 | '@views': path.resolve(__dirname, 'src/views/include'), 52 | }, 53 | plugins: [new LegacyNsResolverPlugin()], 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /examples/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | -------------------------------------------------------------------------------- /examples/typescript/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twigjs-loader-example-typescript", 3 | "version": "1.0.0", 4 | "description": "twigjs-loader in typescript project", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack --mode=development", 8 | "start": "node dist/main.js" 9 | }, 10 | "devDependencies": { 11 | "@types/express": "^4.17.1", 12 | "@types/node": "^9.6.6", 13 | "awesome-typescript-loader": "^5.2.1", 14 | "typescript": "^3.7.0-dev.20191016", 15 | "webpack": "^4.41.2", 16 | "webpack-cli": "^3.3.9", 17 | "webpack-node-externals": "^1.7.2" 18 | }, 19 | "dependencies": { 20 | "express": "^4.17.1", 21 | "twig": "^1.13.3", 22 | "twigjs-loader": "1.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { Request, Response } from "express"; 3 | import { ExpressView } from "twigjs-loader"; 4 | import indexView from "./views/index.twig"; 5 | 6 | const app = express(); 7 | 8 | app.set("view", ExpressView); 9 | 10 | app.get("/", (req: Request, res: Response) => { 11 | res.render(indexView, { 12 | url: `${req.protocol}://${req.get("host")}${req.originalUrl}`, 13 | }) 14 | }); 15 | 16 | const port = process.env.NODE_PORT || 8080; 17 | app.listen(port, () => { 18 | console.log(`Example app listening on port ${port}.`); 19 | }); 20 | -------------------------------------------------------------------------------- /examples/typescript/src/views/index.twig: -------------------------------------------------------------------------------- 1 | {% extends './layout.twig' %} 2 | 3 | {% block content %} 4 |

Example page

5 |

The current url is: {{ url }}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /examples/typescript/src/views/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | blah 6 | 7 | 8 | {% block content %}{% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "lib": [ 6 | "es6" 7 | ] 8 | }, 9 | "include": [ 10 | "src/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/typescript/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | devtool: 'source-map', 7 | 8 | entry: './src/index.ts', 9 | 10 | externals: [ 11 | nodeExternals(), 12 | ], 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.twig$/, 18 | use: 'twigjs-loader', 19 | }, 20 | { 21 | test: /\.ts$/, 22 | use: 'awesome-typescript-loader', 23 | }, 24 | ], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const Twig = require('twig'); 5 | 6 | module.exports = twigLoader; 7 | module.exports.ExpressView = ExpressView; 8 | module.exports.default = module.exports; 9 | 10 | Twig.cache(false); 11 | 12 | function twigLoader(source) { 13 | const callback = this.async(); 14 | 15 | const template = Twig.twig({ 16 | allowInlineIncludes: true, 17 | data: source, 18 | id: makeTemplateId(this, this.resourcePath), 19 | path: this.resourcePath, 20 | rethrow: true, 21 | }); 22 | 23 | compile(this, template) 24 | .then(output => callback(null, output)) 25 | .catch(err => callback(err)); 26 | } 27 | 28 | async function compile(loaderApi, template) { 29 | let dependencies = []; 30 | await each(template.tokens, processToken); 31 | 32 | const twigData = { 33 | allowInlineIncludes: true, 34 | data: template.tokens, 35 | id: template.id, 36 | rethrow: true, 37 | }; 38 | 39 | const dependenciesString = unique(dependencies) 40 | .sort() 41 | .map(d => `require(${JSON.stringify(d)});`) 42 | .join('\n'); 43 | 44 | let renderer = loaderApi.query && loaderApi.query.renderTemplate; 45 | if (typeof renderer !== 'function') { 46 | renderer = renderTemplate; 47 | } 48 | 49 | return renderer(twigData, dependenciesString, loaderApi.hot); 50 | 51 | async function processDependency(token) { 52 | const absolutePath = await resolveModule(loaderApi, token.value); 53 | dependencies.push(token.value); 54 | token.value = makeTemplateId(loaderApi, absolutePath); 55 | loaderApi.addDependency(absolutePath); 56 | } 57 | 58 | async function processToken(token) { 59 | if (token.type !== 'logic' || !token.token.type) { 60 | return; 61 | } 62 | 63 | switch (token.token.type) { 64 | case 'Twig.logic.type.block': 65 | case 'Twig.logic.type.if': 66 | case 'Twig.logic.type.elseif': 67 | case 'Twig.logic.type.else': 68 | case 'Twig.logic.type.for': 69 | case 'Twig.logic.type.spaceless': 70 | case 'Twig.logic.type.macro': 71 | case 'Twig.logic.type.apply': 72 | case 'Twig.logic.type.setcapture': { 73 | await each(token.token.output, processToken); 74 | break; 75 | } 76 | 77 | case 'Twig.logic.type.extends': 78 | case 'Twig.logic.type.include': { 79 | await each(token.token.stack, processDependency); 80 | break; 81 | } 82 | 83 | case 'Twig.logic.type.embed': { 84 | await each(token.token.output, processToken); 85 | await each(token.token.stack, processDependency); 86 | break; 87 | } 88 | 89 | case 'Twig.logic.type.import': 90 | case 'Twig.logic.type.from': 91 | if (token.token.expression !== '_self') { 92 | await each(token.token.stack, processDependency); 93 | } 94 | break; 95 | } 96 | } 97 | } 98 | 99 | async function each(arr, callback) { 100 | if (!Array.isArray(arr)) { 101 | return Promise.resolve(); 102 | } 103 | 104 | return Promise.all(arr.map(callback)); 105 | } 106 | 107 | function makeTemplateId(loaderApi, absolutePath) { 108 | const root = loaderApi.rootContext || process.cwd(); 109 | return path.relative(root, absolutePath); 110 | } 111 | 112 | /** 113 | * @param {object} twigData Data passed to the Twig.twig function 114 | * @param {string} twigData.id Template id (relative path) 115 | * @param {object} twigData.tokens Parsed AST of a template 116 | * @param {string} dependencies Code which requires related templates 117 | * @param {boolean} isHot Is Hot Module Replacement enabled 118 | * @return {string} 119 | */ 120 | function renderTemplate(twigData, dependencies, isHot) { 121 | const hmrFix = isHot ? '\n require("twig").cache(false);' : ''; 122 | 123 | return ` 124 | ${dependencies} ${hmrFix} 125 | var twig = require("twig").twig; 126 | var tpl = twig(${JSON.stringify(twigData)}); 127 | module.exports = function(context) { return tpl.render(context); }; 128 | module.exports.id = ${JSON.stringify(twigData.id)}; 129 | module.exports.default = module.exports; 130 | `.replace(/^\s+/gm, ''); 131 | } 132 | 133 | async function resolveModule(loaderApi, modulePath) { 134 | return new Promise((resolve, reject) => { 135 | loaderApi.resolve(loaderApi.context, modulePath, (err, result) => { 136 | err ? reject(err) : resolve(result); 137 | }); 138 | }); 139 | } 140 | 141 | function unique(arr) { 142 | return arr.filter((val, i, self) => self.indexOf(val) === i); 143 | } 144 | 145 | /** 146 | * Render compiled twig template 147 | */ 148 | function ExpressView(view) { 149 | this.render = (options, callback) => { 150 | const variables = { ...options._locals, ...options }; 151 | callback(null, view(variables)); 152 | }; 153 | this.path = view.id; 154 | } 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twigjs-loader", 3 | "version": "1.0.3", 4 | "description": "twig.js loader for Webpack", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run humile && npm run lint", 8 | "humile": "humile", 9 | "lint": "eslint index.js '__specs__/**/*.js'", 10 | "postversion": "npm test && git push && git push --tags", 11 | "prepack": "npm test" 12 | }, 13 | "files": [ 14 | "index.js", 15 | "typings/*" 16 | ], 17 | "typings": "typings/index.d.ts", 18 | "keywords": [ 19 | "twig", 20 | "twig.js", 21 | "template", 22 | "webpack", 23 | "loader" 24 | ], 25 | "repository": "megahertz/twigjs-loader", 26 | "author": "Alexey Prokhorov", 27 | "license": "MIT", 28 | "bugs": "https://github.com/megahertz/twigjs-loader/issues", 29 | "homepage": "https://github.com/megahertz/twigjs-loader#readme", 30 | "engines": { 31 | "node": ">=8.0" 32 | }, 33 | "peerDependencies": { 34 | "twig": "1.x" 35 | }, 36 | "devDependencies": { 37 | "@types/express": "*", 38 | "eslint": "^8.22.0", 39 | "eslint-config-airbnb-base": "^15.0.0", 40 | "eslint-plugin-import": "^2.26.0", 41 | "express": "*", 42 | "humile": "^0.5.0", 43 | "twig": "*" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /typings/express.d.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | declare module "express" { 4 | interface Response { 5 | render( 6 | view: any, 7 | options?: object, 8 | callback?: (err: Error, html: string) => void, 9 | ): void; 10 | render(view: any, callback?: (err: Error, html: string) => void): void; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | import "./express"; 2 | import "./twig"; 3 | 4 | export declare function ExpressView(view: any): any; 5 | -------------------------------------------------------------------------------- /typings/twig.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.twig" { 2 | const template: (variables?: object) => string; 3 | export default template; 4 | } 5 | --------------------------------------------------------------------------------