├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── index.js ├── package.json └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs 2 | # http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | 8 | # change these settings to your own preference 9 | indent_style = space 10 | indent_size = 2 11 | 12 | # we recommend you to keep these unchanged 13 | end_of_line = lf 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # These attributes affect how the contents stored in the repository are copied 2 | # to the working tree files when commands such as git checkout and git merge run. 3 | # http://git-scm.com/docs/gitattributes 4 | 5 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Git uses this file to determine which files and directories to ignore 2 | # https://help.github.com/articles/ignoring-files 3 | 4 | # Node.js 5 | node_modules 6 | npm-debug.log 7 | tmp 8 | 9 | # NPM 10 | *.tgz 11 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "immed": true, 8 | "newcap": true, 9 | "noarg": true, 10 | "undef": true, 11 | "unused": "vars", 12 | "strict": true, 13 | "indent": 2 14 | } 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.tgz 2 | .idea 3 | .editorconfig 4 | .gitattributes 5 | .jshintrc 6 | .npmignore 7 | .travis.yml 8 | gulpfile.js 9 | test.js 10 | SampleComponent.jsx 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Konstantin Tarkus (@koistya) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [gulp](http://gulpjs.com)-render   [![Build Status](http://img.shields.io/travis/koistya/gulp-render/master.svg?style=flat)](http://travis-ci.org/koistya/gulp-render) [![Dependency Status](https://david-dm.org/koistya/gulp-render.svg?style=flat)](https://david-dm.org/koistya/gulp-render) [![Tips](http://img.shields.io/gratipay/koistya.svg?style=flat)](https://gratipay.com/koistya) [![Gitter](http://img.shields.io/badge/chat-online-brightgreen.svg?style=flat)](https://gitter.im/kriasoft/react-starter-kit) 2 | 3 | > Pre-render [React](https://facebook.github.io/react/) components at compile time. 4 | 5 | ## How to Install 6 | 7 | [![NPM](https://nodei.co/npm/gulp-render.png?compact=true)](https://www.npmjs.org/package/gulp-render) 8 | 9 | ```sh 10 | $ npm install gulp-render --save-dev 11 | ``` 12 | 13 | ## How to Use 14 | 15 | #### Example 1: 16 | 17 | ```javascript 18 | var gulp = require('gulp'); 19 | var render = require('gulp-render'); 20 | 21 | gulp.task('default', function() { 22 | return gulp.src('src/pages/**/*.jsx') 23 | .pipe(render({template: 'src/pages/_template.html'})) 24 | .pipe(gulp.dest('build')); 25 | }); 26 | ``` 27 | 28 | #### Example 2: 29 | 30 | ```javascript 31 | var gulp = require('gulp'); 32 | var render = require('gulp-render'); 33 | 34 | gulp.task('default', function() { 35 | return gulp.src('src/pages/**/*.jsx') 36 | .pipe(render({ 37 | template: 38 | '' + 39 | '<%=title%>' + 40 | '<%=body%>', 41 | harmony: false, 42 | data: {title: 'Page Title'} 43 | })) 44 | .pipe(gulp.dest('build')); 45 | }); 46 | ``` 47 | 48 | #### React Component Sample (`src/pages/SomePage.jsx`) 49 | 50 | ```javascript 51 | var React = require('react'); 52 | var DefaultLayout = require('../layouts/DefaultLayout.jsx'); 53 | 54 | var SomePage = React.createClass({ 55 | statics: { 56 | layout: DefaultLayout 57 | }, 58 | render() { 59 | return ( 60 |
61 |

React Component Sample

62 |

Lorem ipsum dolor sit amet.

63 |
64 | ); 65 | } 66 | }); 67 | 68 | module.exports = SomePage; 69 | ``` 70 | 71 | ## API 72 | 73 | #### `render(options)` 74 | 75 | option | values | default 76 | ---------------|------------------------------------------------------------------------|-------- 77 | `template` | [Lo-Dash template](http://lodash.com/docs#template) string or filename | `null` 78 | `harmony` | `true`: enable ES6 features | `true` 79 | `stripTypes` | `true`: enable [Flow](http://flowtype.org) type annotations | `true` 80 | `hyphenate` | `true`: SomePage.jsx -> some-page.html | `true` 81 | `staticMarkup` | `true`: HTML output will not have `data-react-*` attributes | `false` 82 | `data ` | E.g. `{title: 'Hello'}` or `function(file) { ... }` | `object` or `function` 83 | 84 | ## Related Projects 85 | 86 | [React.js Starter Kit](https://github.com/kriasoft/react-starter-kit) - 87 | a skeleton for an isomorphic web application (SPA) 88 | 89 | ## License 90 | 91 | The MIT License (MIT) @ Konstantin Tarkus ([@koistya](https://twitter.com/koistya)) 92 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * gulp-render 3 | * https://github.com/koistya/gulp-render 4 | * 5 | * Copyright (c) 2014 Konstantin Tarkus 6 | * Licensed under the MIT license 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var through = require('through2'); 12 | var gutil = require('gulp-util'); 13 | var path = require('path'); 14 | var fs = require('fs'); 15 | var _ = require('lodash'); 16 | var React = require('react'); 17 | var ReactTools = require('react-tools'); 18 | var hyphenate = require('react/lib/hyphenate'); 19 | var template = _.template; 20 | var PluginError = gutil.PluginError; 21 | var Module = module.constructor; 22 | 23 | // Constants 24 | var PLUGIN_NAME = 'gulp-render'; 25 | 26 | /** 27 | * Append @jsx pragma to JSX files 28 | */ 29 | function appendJsxPragma(filename, contents) { 30 | return filename.match(/\.jsx$/) || filename.match(/[\-\.]react\.js$/) ? 31 | '/**@jsx React.DOM*/' + contents : contents; 32 | } 33 | 34 | /** 35 | * Check if Page component has a layout property; and if yes, wrap the page 36 | * into the specified layout, then render to a string. 37 | */ 38 | function renderToString(page) { 39 | var layout = null, child = null, props = {}; 40 | while ((layout = page.type.layout || (page.defaultProps && page.defaultProps.layout))) { 41 | child = React.createElement(page, props, child); 42 | _.extend(props, page.defaultProps); 43 | React.renderToString(React.createElement(page, props, child)); 44 | page = layout; 45 | } 46 | return React.renderToString(React.createElement(page, props, child)); 47 | } 48 | 49 | /** 50 | * Just produce static markup without data-react-* attributes 51 | * http://facebook.github.io/react/docs/top-level-api.html#react.rendertostaticmarkup 52 | */ 53 | function renderToStaticMarkup(page) { 54 | return React.renderToStaticMarkup(React.createElement(page)); 55 | } 56 | 57 | // Plugin level function (dealing with files) 58 | function Plugin(options) { 59 | 60 | options = options || {}; 61 | 62 | var reactOptions = { 63 | harmony: typeof options.harmony === 'undefined' ? true : options.harmony, 64 | stripTypes: typeof options.stripTypes === 'undefined' ? true : options.stripTypes 65 | }; 66 | 67 | if (options.template && options.template.indexOf('<') === -1) { 68 | options.template = fs.readFileSync(options.template, {encoding: 'utf8'}); 69 | } 70 | 71 | var originalJsTransform = require.extensions['.js']; 72 | 73 | var reactTransform = function(module, filename) { 74 | if (filename.indexOf('node_modules') === -1) { 75 | var src = fs.readFileSync(filename, {encoding: 'utf8'}); 76 | src = appendJsxPragma(filename, src); 77 | src = ReactTools.transform(src, reactOptions); 78 | module._compile(src, filename); 79 | } else { 80 | originalJsTransform(module, filename); 81 | } 82 | }; 83 | 84 | require.extensions['.js'] = reactTransform; 85 | require.extensions['.jsx'] = reactTransform; 86 | 87 | // Creates a stream through which each file will pass 88 | var stream = through.obj(function(file, enc, cb) { 89 | 90 | if (!file.isNull()) { 91 | 92 | if (file.isStream()) { 93 | this.emit('error', new PluginError(PLUGIN_NAME, 'Streams are not supported!')); 94 | return cb(); 95 | } 96 | 97 | if (file.isBuffer()) { 98 | 99 | try { 100 | var contents = file.contents.toString('utf8'); 101 | contents = appendJsxPragma(file.path, contents); 102 | contents = ReactTools.transform(contents, reactOptions); 103 | var m = new Module(); 104 | m.id = file.path; 105 | m.filename = file.path; 106 | m.paths = module.paths.slice(1); 107 | m._compile(contents, file.path); 108 | var Component = m.exports; 109 | var markup = options.staticMarkup ? renderToStaticMarkup(Component) : renderToString(Component); 110 | 111 | if (options.template) { 112 | var data = _.extend({}, (typeof(options.data) == 'function' ? options.data(file) : options.data)); 113 | data.body = markup; 114 | 115 | // Set default values to avoid null-reference exceptions 116 | data.title = data.title || ''; 117 | data.description = data.description || ''; 118 | data.keywords = data.keywords || ''; 119 | 120 | markup = template(options.template, data); 121 | } 122 | 123 | file.contents = new Buffer(markup); 124 | var filename = gutil.replaceExtension(file.path, '.html'); 125 | 126 | if (typeof options.hyphenate === 'undefined' || options.hyphenate) { 127 | filename = hyphenate(path.basename(filename)); 128 | filename = filename.lastIndexOf('-', 0) === 0 ? filename.substring(1) : filename; 129 | filename = path.join(path.dirname(file.path), filename); 130 | } 131 | 132 | file.path = filename; 133 | } catch (err) { 134 | this.emit('error', new PluginError(PLUGIN_NAME, err)); 135 | return cb(); 136 | } 137 | } 138 | } 139 | 140 | // Make sure the file goes through the next gulp plugin 141 | this.push(file); 142 | // Tell the stream engine that we are done with this file 143 | return cb(); 144 | }); 145 | 146 | // Return the file stream 147 | return stream; 148 | } 149 | 150 | module.exports = Plugin; 151 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-render", 3 | "description": "Pre-render React components at compile time. E.g. SomePage.jsx -> some-page.html", 4 | "version": "0.2.2", 5 | "homepage": "https://github.com/koistya/gulp-render", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/koistya/gulp-render.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/koistya/gulp-render/issues" 12 | }, 13 | "author": "Konstantin Tarkus (@koistya)", 14 | "licenses": { 15 | "type": "MIT", 16 | "url": "https://github.com/koistya/gulp-render/raw/master/LICENSE.txt" 17 | }, 18 | "keywords": [ 19 | "gulpplugin", 20 | "react", 21 | "jsx", 22 | "render", 23 | "rendering", 24 | "javascript", 25 | "es6", 26 | "harmony", 27 | "compiler", 28 | "transpiler" 29 | ], 30 | "main": "index.js", 31 | "dependencies": { 32 | "gulp-util": "^3.0.1", 33 | "lodash": "^2.4.1", 34 | "react": "^0.12.1", 35 | "react-tools": "^0.12.1", 36 | "through2": "^0.6.3" 37 | }, 38 | "devDependencies": { 39 | "jshint": "^2.5.10", 40 | "mocha": "^2.0.1" 41 | }, 42 | "scripts": { 43 | "test": "jshint index.js test.js && mocha" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * gulp-render 3 | * https://github.com/koistya/gulp-render 4 | * 5 | * Copyright (c) 2014 Konstantin Tarkus 6 | * Licensed under the MIT license 7 | */ 8 | 9 | /* global require, Buffer, it */ 10 | 11 | 'use strict'; 12 | 13 | var assert = require('assert'); 14 | var gutil = require('gulp-util'); 15 | var render = require('./'); 16 | 17 | it('Should render a simple React component', function(cb) { 18 | 19 | var stream = render(); 20 | 21 | stream.on('data', function(file) { 22 | var contents = file.contents.toString('utf8'); 23 | assert(contents.indexOf('Hello world!') != -1); 24 | cb(); 25 | }); 26 | 27 | stream.write(new gutil.File({ 28 | path: 'SampleComponent.jsx', 29 | cwd: __dirname, 30 | contents: new Buffer( 31 | 'var React = require("./node_modules/react"); ' + 32 | 'var HelloMessage = React.createClass({' + 33 | ' render: function() {return React.DOM.div(null, "Hello world!");}' + 34 | '}); '+ 35 | 'module.exports = HelloMessage;' 36 | ) 37 | })); 38 | 39 | }); 40 | 41 | it('Should render a simple React component with a template', function(cb) { 42 | 43 | var stream = render({ 44 | template: '<%=title%><%=body%>', 45 | data: { title: 'Title123' } 46 | }); 47 | 48 | stream.on('data', function(file) { 49 | var contents = file.contents.toString('utf8'); 50 | assert(contents.indexOf('Hello world!') != -1); 51 | assert(contents.indexOf('Title123') != -1); 52 | cb(); 53 | }); 54 | 55 | stream.write(new gutil.File({ 56 | path: 'SampleComponent.jsx', 57 | cwd: __dirname, 58 | contents: new Buffer( 59 | 'var React = require("./node_modules/react"); ' + 60 | 'var HelloMessage = React.createClass({' + 61 | ' render: function() {return React.DOM.div(null, "Hello world!");}' + 62 | '}); '+ 63 | 'module.exports = HelloMessage;' 64 | ) 65 | })); 66 | 67 | }); 68 | 69 | it('Should render a simple React component as static markup', function(cb) { 70 | 71 | var stream = render({ 72 | staticMarkup: true 73 | }); 74 | 75 | stream.on('data', function(file) { 76 | var contents = file.contents.toString('utf8'); 77 | assert(contents.indexOf('data-react') === -1); 78 | cb(); 79 | }); 80 | 81 | stream.write(new gutil.File({ 82 | path: 'SampleComponent.jsx', 83 | cwd: __dirname, 84 | contents: new Buffer( 85 | 'var React = require("./node_modules/react"); ' + 86 | 'var HelloMessage = React.createClass({' + 87 | ' render: function() {return React.DOM.div(null, "Hello world!");}' + 88 | '}); '+ 89 | 'module.exports = HelloMessage;' 90 | ) 91 | })); 92 | 93 | }); 94 | 95 | it('Should render a simple React component with a template and data function', function(cb) { 96 | 97 | var stream = render({ 98 | template: '<%=title%><%=body%>', 99 | data: function(file) { 100 | return { title: 'Test123' + file.path }; 101 | } 102 | }); 103 | 104 | stream.on('data', function(file) { 105 | var contents = file.contents.toString('utf8'); 106 | assert(contents.indexOf('Hello world!') != -1); 107 | assert(contents.indexOf('Test123SampleComponent.jsx') != -1); 108 | cb(); 109 | }); 110 | 111 | stream.write(new gutil.File({ 112 | path: 'SampleComponent.jsx', 113 | cwd: __dirname, 114 | contents: new Buffer( 115 | 'var React = require("./node_modules/react"); ' + 116 | 'var HelloMessage = React.createClass({' + 117 | ' render: function() {return React.DOM.div(null, "Hello world!");}' + 118 | '}); '+ 119 | 'module.exports = HelloMessage;' 120 | ) 121 | })); 122 | 123 | }); 124 | 125 | it('Should render a React component with a layout defined in default props', function(cb) { 126 | 127 | var stream = render(); 128 | 129 | stream.on('data', function(file) { 130 | var contents = file.contents.toString('utf8'); 131 | assert(contents.indexOf('Layout') != -1); 132 | assert(contents.indexOf('Test') != -1); 133 | assert(contents.indexOf('Hello world!') != -1); 134 | cb(); 135 | }); 136 | 137 | stream.write(new gutil.File({ 138 | path: 'SampleComponent.jsx', 139 | cwd: __dirname, 140 | contents: new Buffer( 141 | 'var React = require("./node_modules/react"); ' + 142 | 'var Layout = React.createClass({' + 143 | ' render: function() {return React.DOM.div(null, ["Layout", this.props.title, this.props.children]);}' + 144 | '}); '+ 145 | 'var HelloMessage = React.createClass({' + 146 | ' getDefaultProps() {' + 147 | ' return {' + 148 | ' "title": "Test",' + 149 | ' "layout": Layout' + 150 | ' }' + 151 | ' },' + 152 | ' render: function() {return React.DOM.div(null, "Hello world!");}' + 153 | '}); '+ 154 | 'module.exports = HelloMessage;' 155 | ) 156 | })); 157 | 158 | }); 159 | 160 | it('Should render a React component with a layout defined in statics', function(cb) { 161 | 162 | var stream = render(); 163 | 164 | stream.on('data', function(file) { 165 | var contents = file.contents.toString('utf8'); 166 | assert(contents.indexOf('Layout') != -1); 167 | assert(contents.indexOf('Hello world!') != -1); 168 | cb(); 169 | }); 170 | 171 | stream.write(new gutil.File({ 172 | path: 'SampleComponent.jsx', 173 | cwd: __dirname, 174 | contents: new Buffer( 175 | 'var React = require("./node_modules/react"); ' + 176 | 'var Layout = React.createClass({' + 177 | ' render: function() {return React.DOM.div(null, ["Layout", this.props.children]);}' + 178 | '}); '+ 179 | 'var HelloMessage = React.createClass({' + 180 | ' statics: {' + 181 | ' "layout": Layout' + 182 | ' },' + 183 | ' render: function() {return React.DOM.div(null, "Hello world!");}' + 184 | '}); '+ 185 | 'module.exports = HelloMessage;' 186 | ) 187 | })); 188 | 189 | }); 190 | --------------------------------------------------------------------------------