├── browserify.js ├── test ├── fixture_selfclosing.jsx ├── fixture_namespace.jsx ├── fixture_namespace.js ├── fixture_es6.jsx ├── fixture.jsx ├── fixture_spread_attrs.jsx ├── fixture.js ├── fixture_array_args.js ├── fixture_spread_attrs.js ├── fixture_spread_attrs_b.js ├── fixture_spread_attrs_c.js └── jsx.js ├── .gitignore ├── .travis.yml ├── lib ├── trimTrailingSpaces.js ├── README_template.md.hbs ├── jsx.js └── visitor.js ├── CHANGELOG.md ├── package.json ├── gulpfile.js └── README.md /browserify.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/jsx.js').browserifyTransform; 2 | -------------------------------------------------------------------------------- /test/fixture_selfclosing.jsx: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return ; 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | haters 2 | node_modules 3 | npm-debug.log 4 | **DS_Store 5 | coverage.html 6 | lib-cov 7 | -------------------------------------------------------------------------------- /test/fixture_namespace.jsx: -------------------------------------------------------------------------------- 1 | module.exports = () => ( 2 | 3 | ) 4 | -------------------------------------------------------------------------------- /test/fixture_namespace.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ( 2 | h('svg', null, [h('use', {'xlink:href': "#a"})]) 3 | ) 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '4' 5 | - '5' 6 | - 'node' 7 | notifications: 8 | email: false 9 | -------------------------------------------------------------------------------- /lib/trimTrailingSpaces.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Trim all trailing spaces. 3 | * 4 | * @param {String} val 5 | * @returns {String} 6 | * @private 7 | */ 8 | function trimTrailingSpaces(val) { 9 | return val.replace(/[^\S\r\n]+$/gm, ""); 10 | } 11 | 12 | module.exports = trimTrailingSpaces; 13 | -------------------------------------------------------------------------------- /test/fixture_es6.jsx: -------------------------------------------------------------------------------- 1 | export default View; 2 | 3 | function View() { 4 | var x = 1; 5 | var profile = {x = 2} 6 | 7 | if (x < 2) { 8 | return

One is less than two

; 9 | } else { 10 | return

elements can be nested

; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixture.jsx: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var x = 1; 3 | var profile = {x = 2} 4 | var h1 =

Hello {firstName + " " + lastName}

; 5 | 6 | if (x < 1) { 7 | return
; 8 | } else if (x < 2) { 9 | return

One is less than two

; 10 | } else { 11 | return

elements can be nested

; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixture_spread_attrs.jsx: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | 3 |
Test
4 |
Test
5 | 6 | 7 | 8 | 9 | 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixture.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var x = 1; 3 | var profile = Component(null, [x = 2]) 4 | var h1 = DOM('h1', {class: "header"}, ["Hello ", firstName + " " + lastName]); 5 | 6 | if (x < 1) { 7 | return DOM('div'); 8 | } else if (x < 2) { 9 | return DOM('h1', null, ["One is less than two"]); 10 | } else { 11 | return DOM('div', {class: "title"}, [DOM('h1', null, ["elements can be nested"])]); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixture_array_args.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var x = 1; 3 | var profile = Component(null, x = 2) 4 | var h1 = DOM('h1', {class: "header"}, "Hello ", firstName + " " + lastName); 5 | 6 | if (x < 1) { 7 | return DOM('div'); 8 | } else if (x < 2) { 9 | return DOM('h1', null, "One is less than two"); 10 | } else { 11 | return DOM('div', {class: "title"}, DOM('h1', null, "elements can be nested")); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | # 1.2.1 4 | 5 | - Fix undefined function call for browserify transform. 6 | 7 | # 1.2.0 8 | 9 | - Add option to replace unknown tags using a pattern. Makes supporting 10 | `Component.render()` trivial. 11 | 12 | # 1.1.0 13 | 14 | - Add option to specify spread function name. 15 | 16 | # 1.0.0 17 | 18 | - Changed API function names and options. 19 | - Added support for spread attributes. 20 | - Removed support for renaming attributes. 21 | -------------------------------------------------------------------------------- /test/fixture_spread_attrs.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | Component(Object.assign({}, firstSpread, secondSpread), 3 | DOM('div', {some: "prop", another: "prop"}, "Test"), 4 | DOM('div', Object.assign({}, thirdSpread), "Test"), 5 | Component(Object.assign({}, firstSpread, secondSpread, {foo: "baz"})), 6 | Component(Object.assign({}, state.nested, {foo: "bar"})), 7 | Component(Object.assign({}, state[0], {foo: "bar"})), 8 | Component(Object.assign({}, state[0][1], {foo: "bar"})) 9 | ) 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixture_spread_attrs_b.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | DOM(Component, Object.assign({}, firstSpread, secondSpread), 3 | DOM('div', {some: "prop", another: "prop"}, "Test"), 4 | DOM('div', Object.assign({}, thirdSpread), "Test"), 5 | DOM(Component, Object.assign({}, firstSpread, secondSpread, {foo: "baz"})), 6 | DOM(Component, Object.assign({}, state.nested, {foo: "bar"})), 7 | DOM(Component, Object.assign({}, state[0], {foo: "bar"})), 8 | DOM(Component, Object.assign({}, state[0][1], {foo: "bar"})) 9 | ) 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixture_spread_attrs_c.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | DOM('Component', Object.assign({}, firstSpread, secondSpread), 3 | DOM('div', {some: "prop", another: "prop"}, "Test"), 4 | DOM('div', Object.assign({}, thirdSpread), "Test"), 5 | DOM('Component', Object.assign({}, firstSpread, secondSpread, {foo: "baz"})), 6 | DOM('Component', Object.assign({}, state.nested, {foo: "bar"})), 7 | DOM('Component', Object.assign({}, state[0], {foo: "bar"})), 8 | DOM('Component', Object.assign({}, state[0][1], {foo: "bar"})) 9 | ) 10 | }; 11 | -------------------------------------------------------------------------------- /lib/README_template.md.hbs: -------------------------------------------------------------------------------- 1 | # jsx-transform [![Build Status](http://img.shields.io/travis/alexmingoia/jsx-transform.svg?style=flat)](http://travis-ci.org/alexmingoia/jsx-transform) [![NPM version](http://img.shields.io/npm/v/jsx-transform.svg?style=flat)](https://npmjs.org/package/jsx-transform) [![Dependency Status](http://img.shields.io/david/alexmingoia/jsx-transform.svg?style=flat)](http://david-dm.org/alexmingoia/jsx-transform) 2 | 3 | > JSX transpiler. Desugar JSX into JavaScript. 4 | 5 | {{#module name="jsx-transform"}}{{>body}}{{/module}}## Installation 6 | 7 | ```sh 8 | npm install jsx-transform 9 | ``` 10 | 11 | ## API 12 | {{#module name="jsx-transform"}}{{>docs}}{{/module}} 13 | 14 | ## BSD Licensed 15 | 16 | [0]: https://facebook.github.io/react/docs/jsx-in-depth.html 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx-transform", 3 | "version": "2.4.0", 4 | "description": "JSX transpiler. Desugar JSX into JavaScript. A standard and configurable implementation of JSX decoupled from React.", 5 | "files": [ 6 | "lib", 7 | "browserify.js" 8 | ], 9 | "main": "lib/jsx.js", 10 | "scripts": { 11 | "test": "gulp test" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/alexmingoia/jsx-transform.git" 16 | }, 17 | "keywords": [ 18 | "jsx", 19 | "virtual-hyperscript", 20 | "vtree", 21 | "virtual-dom", 22 | "html", 23 | "mercury", 24 | "hyperscript" 25 | ], 26 | "author": "Alex Mingoia ", 27 | "license": "0BSD", 28 | "bugs": { 29 | "url": "https://github.com/alexmingoia/jsx-transform/issues" 30 | }, 31 | "homepage": "https://github.com/alexmingoia/jsx-transform", 32 | "dependencies": { 33 | "esprima-fb": "^15001.1001.0-dev-harmony-fb", 34 | "jstransform": "^11.0.3", 35 | "through2": "^2.0.0" 36 | }, 37 | "devDependencies": { 38 | "browserify": "^16.1.1", 39 | "expect.js": "^0.3.1", 40 | "gulp": "^3.9.1", 41 | "gulp-instrument": "^0.1.0", 42 | "gulp-jsdoc-to-markdown": "^1.2.2", 43 | "gulp-jshint": "^2.1.0", 44 | "gulp-mocha": "^5.0.0", 45 | "gulp-rename": "^1.2.2", 46 | "jshint": "^2.9.5", 47 | "jshint-stylish": "^2.2.1", 48 | "vinyl-source-stream": "^2.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jsx-transform 3 | * https://github.com/alexmingoia/jsx-transform 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var gulp = require('gulp'); 9 | var env = process.env.NODE_ENV; 10 | var fs = require('fs'); 11 | var rename = require('gulp-rename'); 12 | var instrument = require('gulp-instrument'); 13 | var jsdoc2md = require('gulp-jsdoc-to-markdown'); 14 | var jshint = require('gulp-jshint'); 15 | var mocha = require('gulp-mocha'); 16 | var stylish = require('jshint-stylish'); 17 | var spawn = require('child_process').spawn; 18 | var source = require('vinyl-source-stream'); 19 | 20 | gulp.task('instrument', function() { 21 | return gulp.src('lib\/**.js') 22 | .pipe(instrument()) 23 | .pipe(gulp.dest('lib-cov')); 24 | }); 25 | 26 | gulp.task('coverage', ['instrument'], function() { 27 | process.env.JSCOV=1; 28 | 29 | return spawn('node_modules/gulp-mocha/node_modules/mocha/bin/mocha', [ 30 | 'test', '--reporter', 'html-cov' 31 | ]).stdout 32 | .pipe(source('coverage.html')) 33 | .pipe(gulp.dest('./')); 34 | }); 35 | 36 | gulp.task('docs', function(done) { 37 | return gulp.src('lib/jsx.js') 38 | .pipe(jsdoc2md({ 39 | template: fs.readFileSync('lib/README_template.md.hbs', 'utf8') 40 | })) 41 | .pipe(rename('README.md')) 42 | .pipe(gulp.dest('./')) 43 | }); 44 | 45 | gulp.task('test', function () { 46 | return gulp.src('test\/*.js') 47 | .pipe(mocha({ 48 | timeout: 6000, 49 | ignoreLeaks: ['replacements'], 50 | ui: 'bdd', 51 | reporter: 'spec' 52 | })); 53 | }); 54 | 55 | gulp.task('jshint', function () { 56 | return gulp.src(['lib/**/*.js', 'test/**/*.js']) 57 | .pipe(jshint()) 58 | .pipe(jshint.reporter(stylish)); 59 | }); 60 | 61 | gulp.task('watch', function () { 62 | return gulp.watch(['lib/*.js', 'test/*.js'], ['jshint', 'test']); 63 | }); 64 | 65 | gulp.task('default', [env === 'production' ? 'watch' : 'test']); 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsx-transform [![Build Status](http://img.shields.io/travis/alexmingoia/jsx-transform.svg?style=flat)](http://travis-ci.org/alexmingoia/jsx-transform) [![NPM version](http://img.shields.io/npm/v/jsx-transform.svg?style=flat)](https://npmjs.org/package/jsx-transform) [![Dependency Status](http://img.shields.io/david/alexmingoia/jsx-transform.svg?style=flat)](http://david-dm.org/alexmingoia/jsx-transform) 2 | 3 | > JSX transpiler. Desugar JSX into JavaScript. 4 | 5 | This module aims to be a standard and configurable implementation of JSX 6 | decoupled from [React](https://github.com/facebook/react) for use with 7 | [Mercury](https://github.com/Raynos/mercury) or other modules. 8 | 9 | JSX is a JavaScript syntax for composing virtual DOM elements. 10 | See React's [documentation][0] for an explanation. 11 | 12 | For linting files containing JSX see 13 | [JSXHint](https://github.com/STRML/JSXHint). 14 | 15 | ## Installation 16 | 17 | ```sh 18 | npm install jsx-transform 19 | ``` 20 | 21 | ## API 22 | 23 | ## jsx-transform 24 | This module aims to be a standard and configurable implementation of JSX 25 | decoupled from [React](https://github.com/facebook/react) for use with 26 | [Mercury](https://github.com/Raynos/mercury) or other modules. 27 | 28 | JSX is a JavaScript syntax for composing virtual DOM elements. 29 | See React's [documentation][0] for an explanation. 30 | 31 | For linting files containing JSX see 32 | [JSXHint](https://github.com/STRML/JSXHint). 33 | 34 | 35 | * [jsx-transform](#module_jsx-transform) 36 | * [~fromString(str, [options])](#module_jsx-transform..fromString) ⇒ String 37 | * [~fromFile(path, [options])](#module_jsx-transform..fromFile) ⇒ String 38 | * [~browserifyTransform([filename], [options])](#module_jsx-transform..browserifyTransform) ⇒ function 39 | 40 | 41 | ### jsx-transform~fromString(str, [options]) ⇒ String 42 | Desugar JSX and return transformed string. 43 | 44 | **Kind**: inner method of [jsx-transform](#module_jsx-transform) 45 | 46 | | Param | Type | Description | 47 | | --- | --- | --- | 48 | | str | String | | 49 | | [options] | Object | | 50 | | options.factory | String | Factory function name for element creation. | 51 | | [options.spreadFn] | String | Name of function for use with spread attributes (default: Object.assign). | 52 | | [options.unknownTagPattern] | String | uses given pattern for unknown tags where `{tag}` is replaced by the tag name. Useful for rending mercury components as `Component.render()` instead of `Component()`. | 53 | | [options.passUnknownTagsToFactory] | Boolean | Handle unknown tags like known tags, and pass them as an object to `options.factory`. If true, `createElement(Component)` instead of `Component()` (default: false). | 54 | | [options.unknownTagsAsString] | Boolean | Pass unknown tags as string to `options.factory` (default: false). | 55 | | [options.arrayChildren] | Boolean | Pass children as array instead of arguments (default: true). | 56 | 57 | **Example** 58 | ```javascript 59 | var jsx = require('jsx-transform'); 60 | 61 | jsx.fromString('

Hello World

', { 62 | factory: 'mercury.h' 63 | }); 64 | // => 'mercury.h("h1", null, ["Hello World"])' 65 | ``` 66 | 67 | ### jsx-transform~fromFile(path, [options]) ⇒ String 68 | **Kind**: inner method of [jsx-transform](#module_jsx-transform) 69 | 70 | | Param | Type | 71 | | --- | --- | 72 | | path | String | 73 | | [options] | Object | 74 | 75 | 76 | ### jsx-transform~browserifyTransform([filename], [options]) ⇒ function 77 | Make a browserify transform. 78 | 79 | **Kind**: inner method of [jsx-transform](#module_jsx-transform) 80 | **Returns**: function - browserify transform 81 | 82 | | Param | Type | Description | 83 | | --- | --- | --- | 84 | | [filename] | String | | 85 | | [options] | Object | | 86 | | [options.extensions] | String | Array of file extensions to run browserify transform on (default: `['.js', '.jsx', '.es', '.es6']`). | 87 | 88 | **Example** 89 | ```javascript 90 | var browserify = require('browserify'); 91 | var jsxify = require('jsx-transform').browserifyTransform; 92 | 93 | browserify() 94 | .transform(jsxify, options) 95 | .bundle() 96 | ``` 97 | 98 | Use `.configure(options)` to return a configured transform: 99 | 100 | ```javascript 101 | var browserify = require('browserify'); 102 | var jsxify = require('jsx-transform').browserifyTransform; 103 | 104 | browserify({ 105 | transforms: [jsxify.configure(options)] 106 | }).bundle() 107 | ``` 108 | 109 | Use in `package.json`: 110 | 111 | ```json 112 | "browserify": { 113 | "transform": [ 114 | ["jsx-transform/browserify", { "factory": "h" }] 115 | ] 116 | } 117 | ``` 118 | 119 | 120 | ## BSD Licensed 121 | 122 | [0]: https://facebook.github.io/react/docs/jsx-in-depth.html 123 | -------------------------------------------------------------------------------- /lib/jsx.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jsx-transform 3 | * https://github.com/alexmingoia/jsx-transform 4 | */ 5 | 6 | /** 7 | * This module aims to be a standard and configurable implementation of JSX 8 | * decoupled from {@link https://github.com/facebook/react|React} for use with 9 | * {@link https://github.com/Raynos/mercury|Mercury} or other modules. 10 | * 11 | * JSX is a JavaScript syntax for composing virtual DOM elements. 12 | * See React's [documentation][0] for an explanation. 13 | * 14 | * For linting files containing JSX see 15 | * {@link https://github.com/STRML/JSXHint|JSXHint}. 16 | * 17 | * @module jsx-transform 18 | */ 19 | 20 | 'use strict'; 21 | 22 | var fs = require('fs'); 23 | var getExtension = require('path').extname; 24 | var jstransform = require('jstransform').transform; 25 | var visitNode = require('./visitor'); 26 | var trimTrailingSpaces = require('./trimTrailingSpaces'); 27 | var through = require('through2'); 28 | 29 | module.exports = { 30 | fromString: fromString, 31 | fromFile: fromFile, 32 | browserifyTransform: browserifyTransform, 33 | visitor: visitNode 34 | }; 35 | 36 | /** 37 | * Desugar JSX and return transformed string. 38 | * 39 | * @example 40 | * 41 | * ```javascript 42 | * var jsx = require('jsx-transform'); 43 | * 44 | * jsx.fromString('

Hello World

', { 45 | * factory: 'mercury.h' 46 | * }); 47 | * // => 'mercury.h("h1", null, ["Hello World"])' 48 | * ``` 49 | * 50 | * @param {String} str 51 | * @param {Object=} options 52 | * @param {String} options.factory Factory function name for element creation. 53 | * @param {String=} options.spreadFn Name of function for use with spread 54 | * attributes (default: Object.assign). 55 | * @param {String=} options.unknownTagPattern uses given pattern for unknown 56 | * tags where `{tag}` is replaced by the tag name. Useful for rending mercury 57 | * components as `Component.render()` instead of `Component()`. 58 | * @param {Boolean=} options.passUnknownTagsToFactory Handle unknown tags 59 | * like known tags, and pass them as an object to `options.factory`. If 60 | * true, `createElement(Component)` instead of `Component()` (default: false). 61 | * @param {Boolean=} options.unknownTagsAsString Pass unknown tags as string 62 | * to `options.factory` (default: false). 63 | * @param {Boolean=} options.arrayChildren Pass children as array instead of 64 | * arguments (default: true). 65 | * @returns {String} 66 | */ 67 | function fromString(str, options) { 68 | options = processOptions(options); 69 | 70 | var transformed = jstransform([visitNode], str, options).code; 71 | 72 | return trimTrailingSpaces(transformed); 73 | } 74 | 75 | /** 76 | * @param {String} path 77 | * @param {Object=} options 78 | * @returns {String} 79 | */ 80 | function fromFile(path, options) { 81 | options = processOptions(options); 82 | var transformed = jstransform([visitNode], fs.readFileSync(path, 'utf8'), options).code; 83 | return trimTrailingSpaces(transformed); 84 | } 85 | 86 | function processOptions(options){ 87 | if (typeof options !== 'object') { 88 | options = {}; 89 | } 90 | 91 | if (typeof options.factory !== 'string') { 92 | throw new Error('Missing options.factory function name.'); 93 | } 94 | 95 | // parses the file as an ES6 module, except disabled implicit strict-mode 96 | if (typeof options.sourceType === 'undefined') { 97 | options.sourceType = 'nonStrictModule'; 98 | } 99 | 100 | // defaults to true to keep existing behaviour (but inconsietent with babel and react-tools) 101 | if (typeof options.arrayChildren === 'undefined') { 102 | options.arrayChildren = true; 103 | } 104 | 105 | if (typeof options.spreadFn !== 'string') { 106 | options.spreadFn = 'Object.assign'; 107 | } 108 | 109 | if (typeof options.unknownTagPattern !== 'string') { 110 | options.unknownTagPattern = '{tag}'; 111 | } 112 | 113 | return options; 114 | } 115 | 116 | /** 117 | * Make a browserify transform. 118 | * 119 | * @example 120 | * 121 | * ```javascript 122 | * var browserify = require('browserify'); 123 | * var jsxify = require('jsx-transform').browserifyTransform; 124 | * 125 | * browserify() 126 | * .transform(jsxify, options) 127 | * .bundle() 128 | * ``` 129 | * 130 | * Use `.configure(options)` to return a configured transform: 131 | * 132 | * ```javascript 133 | * var browserify = require('browserify'); 134 | * var jsxify = require('jsx-transform').browserifyTransform; 135 | * 136 | * browserify({ 137 | * transforms: [jsxify.configure(options)] 138 | * }).bundle() 139 | * ``` 140 | * 141 | * Use in `package.json`: 142 | * 143 | * ```json 144 | * "browserify": { 145 | * "transform": [ 146 | * ["jsx-transform/browserify", { "factory": "h" }] 147 | * ] 148 | * } 149 | * ``` 150 | * 151 | * @param {String=} filename 152 | * @param {Object=} options 153 | * @param {String=} options.extensions Array of file extensions to run 154 | * browserify transform on (default: `['.js', '.jsx', '.es', '.es6']`). 155 | * @returns {Function} browserify transform 156 | */ 157 | function browserifyTransform(filename, options) { 158 | return browserifyTransform.configure(options)(filename); 159 | } 160 | 161 | browserifyTransform.configure = function (options) { 162 | if (typeof options.extensions === 'undefined') { 163 | options.extensions = ['.js', '.jsx', '.es', '.es6']; 164 | } 165 | 166 | return function (filename) { 167 | if (!~options.extensions.indexOf(getExtension(filename))) { 168 | // We don't need to apply any transforms, just provide a simple pass-through stream 169 | return through(); 170 | } 171 | 172 | var data = ""; 173 | 174 | return through(function (chunk, enc, next) { 175 | // This function receives chunks of data and we don't want to perform any transforms on an incomplete file. 176 | // We buffer the data until the flush function is called. We can then safely perform the transforms on the full file. 177 | data += chunk.toString('utf8'); 178 | next(); 179 | }, function (next) { 180 | try { 181 | this.push(fromString(data, options)); 182 | next(); 183 | } catch (err) { 184 | next(err); 185 | } 186 | }); 187 | }; 188 | } 189 | -------------------------------------------------------------------------------- /test/jsx.js: -------------------------------------------------------------------------------- 1 | var Browserify = require('browserify'); 2 | var expect = require('expect.js'); 3 | var fs = require('fs'); 4 | var jsx = process.env.JSCOV ? require('../lib-cov/jsx') : require('../lib/jsx'); 5 | var path = require('path'); 6 | 7 | describe('jsx.fromString()', function() { 8 | var fixtureJSX = fs.readFileSync(path.join(__dirname, 'fixture.jsx'), 'utf8'); 9 | var fixtureJS = fs.readFileSync(path.join(__dirname, 'fixture.js'), 'utf8'); 10 | 11 | var selfClosingFixtureJSX = fs.readFileSync( 12 | path.join(__dirname, 'fixture_selfclosing.jsx'), 13 | 'utf8' 14 | ); 15 | var es6FixtureJSX = fs.readFileSync( 16 | path.join(__dirname, 'fixture_es6.jsx'), 17 | 'utf8' 18 | ); 19 | 20 | var fixtureJSXSpreadAttrs = fs.readFileSync( 21 | path.join(__dirname, 'fixture_spread_attrs.jsx'), 22 | 'utf8' 23 | ); 24 | 25 | var fixtureJSSpreadAttrs = fs.readFileSync( 26 | path.join(__dirname, 'fixture_spread_attrs.js'), 27 | 'utf8' 28 | ); 29 | 30 | var fixtureJSSpreadAttrsB = fs.readFileSync( 31 | path.join(__dirname, 'fixture_spread_attrs_b.js'), 32 | 'utf8' 33 | ); 34 | 35 | var fixtureJSSpreadAttrsC = fs.readFileSync( 36 | path.join(__dirname, 'fixture_spread_attrs_c.js'), 37 | 'utf8' 38 | ); 39 | 40 | var fixtureNamespaceJS = fs.readFileSync( 41 | path.join(__dirname, 'fixture_namespace.js'), 42 | 'utf8' 43 | ); 44 | 45 | var fixtureNamespaceJSX = fs.readFileSync( 46 | path.join(__dirname, 'fixture_namespace.jsx'), 47 | 'utf8' 48 | ); 49 | 50 | it('desugars JSX', function() { 51 | var result = jsx.fromString(fixtureJSX, { 52 | factory: 'DOM' 53 | }); 54 | expect(result).to.be.a('string'); 55 | expect(result).to.equal(fixtureJS); 56 | }); 57 | 58 | it('desugars JSX with ES6 module exports', function () { 59 | var result = jsx.fromString(es6FixtureJSX, { 60 | factory: 'DOM' 61 | }); 62 | expect(result).to.be.a('string'); 63 | expect(result).to.contain("DOM('h1"); 64 | }); 65 | 66 | it('fromStrings self-closing tags', function () { 67 | var result = jsx.fromString(selfClosingFixtureJSX, { 68 | factory: 'DOM' 69 | }); 70 | expect(result).to.be.a('string'); 71 | expect(result).to.contain("DOM('link"); 72 | }); 73 | 74 | it('renders JS expressions inside JSX tag', function () { 75 | var result = jsx.fromString(fixtureJSX, { 76 | factory: 'DOM' 77 | }); 78 | expect(result).to.be.a('string'); 79 | expect(result).to.contain("x = 2"); 80 | }); 81 | 82 | it('handles namespace', function() { 83 | var result = jsx.fromString(fixtureNamespaceJSX, { 84 | factory: 'h' 85 | }); 86 | expect(result).to.be.a('string'); 87 | expect(result).to.equal(fixtureNamespaceJS); 88 | }); 89 | 90 | describe('options.factory', function() { 91 | it('throws error if not set', function () { 92 | expect(function () { 93 | jsx.fromString(fixtureJSX); 94 | }).to.throwError(/Missing options.factory function/); 95 | }); 96 | 97 | it('set factory', function() { 98 | var result = jsx.fromString(fixtureJSX, { 99 | factory: "mercury.h" 100 | }); 101 | expect(result).to.be.a('string'); 102 | expect(result).to.contain("mercury.h('h1"); 103 | }); 104 | }); 105 | 106 | describe('options.passUnknownTagsToFactory', function() { 107 | it('passes unknown tags to options.factory', function() { 108 | var result = jsx.fromString(fixtureJSX, { 109 | factory: 'DOM', 110 | passUnknownTagsToFactory: true 111 | }); 112 | expect(result).to.be.a('string'); 113 | expect(result).to.contain("DOM(Component"); 114 | }); 115 | }); 116 | 117 | describe('options.unknownTagsAsString', function() { 118 | it('passes unknown tags to docblock ident as string', function () { 119 | var result = jsx.fromString(fixtureJSX, { 120 | factory: 'DOM', 121 | passUnknownTagsToFactory: true, 122 | unknownTagsAsString: true 123 | }); 124 | expect(result).to.be.a('string'); 125 | expect(result).to.contain("DOM('Component'"); 126 | }); 127 | }); 128 | 129 | describe('options.arrayChildren', function() { 130 | it('dont pass array for children', function() { 131 | var arrayArgsJS = fs.readFileSync( 132 | path.join(__dirname, 'fixture_array_args.js'), 133 | 'utf8' 134 | ); 135 | var result = jsx.fromString(fixtureJSX, { 136 | factory: 'DOM', 137 | arrayChildren: false 138 | }); 139 | expect(result).to.be.a('string'); 140 | expect(result).to.equal(arrayArgsJS); 141 | }) 142 | }) 143 | 144 | it('supports custom component patterns', function () { 145 | var result = jsx.fromString('', { 146 | factory: 'DOM', 147 | unknownTagPattern: '{tag}.render', 148 | arrayChildren: false 149 | }); 150 | expect(result).to.be.a('string'); 151 | expect(result).to.equal('Component.render({foo: "bar"})'); 152 | }); 153 | 154 | it('supports spread attributes', function () { 155 | var result = jsx.fromString(fixtureJSXSpreadAttrs, { 156 | factory: 'DOM', 157 | arrayChildren: false 158 | }); 159 | expect(result).to.be.a('string'); 160 | expect(result).to.equal(fixtureJSSpreadAttrs); 161 | 162 | result = jsx.fromString(fixtureJSXSpreadAttrs, { 163 | factory: 'DOM', 164 | passUnknownTagsToFactory: true, 165 | arrayChildren: false 166 | }); 167 | expect(result).to.be.a('string'); 168 | expect(result).to.equal(fixtureJSSpreadAttrsB); 169 | 170 | result = jsx.fromString(fixtureJSXSpreadAttrs, { 171 | factory: 'DOM', 172 | passUnknownTagsToFactory: true, 173 | unknownTagsAsString: true, 174 | arrayChildren: false 175 | }); 176 | expect(result).to.be.a('string'); 177 | expect(result).to.equal(fixtureJSSpreadAttrsC); 178 | }); 179 | }); 180 | 181 | describe('jsx.browserifyTransform()', function () { 182 | it('transforms JSX', function (done) { 183 | var bundler = Browserify({ 184 | entries: [path.join(__dirname, 'fixture.jsx')] 185 | }); 186 | 187 | bundler.transform(jsx.browserifyTransform, { 188 | factory: 'DOM' 189 | }); 190 | 191 | bundler.bundle(function (err, buf) { 192 | done(err); 193 | }); 194 | }); 195 | 196 | it('ignores .json files', function (done) { 197 | var bundler = Browserify({ 198 | entries: [path.join(__dirname, 'fixture.jsx')] 199 | }); 200 | 201 | bundler.transform(jsx.browserifyTransform, { 202 | factory: 'DOM', 203 | }); 204 | 205 | bundler.add(path.join(__dirname, '..', 'package.json')); 206 | 207 | bundler.bundle(function (err, buf) { 208 | done(err); 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /lib/visitor.js: -------------------------------------------------------------------------------- 1 | var utils = require('jstransform/src/utils'); 2 | var Syntax = require('jstransform').Syntax; 3 | 4 | module.exports = visitNode; 5 | 6 | /** 7 | * Visit tag node and desugar JSX. 8 | * 9 | * @see {@link https://github.com/facebook/jstransform} 10 | * @param {Function} traverse 11 | * @param {Object} object 12 | * @param {String} path 13 | * @param {Object} state 14 | * @returns {Boolean} 15 | * @private 16 | */ 17 | function visitNode(traverse, object, path, state) { 18 | var options = state.g.opts; 19 | var factory = (options.factory); 20 | var arrayChildren = options.arrayChildren 21 | var openingEl = object.openingElement; 22 | var closingEl = object.closingElement; 23 | var nameObj = openingEl.name; 24 | var attributes = openingEl.attributes; 25 | var spreadFn = options.spreadFn; 26 | var unknownTagPattern = options.unknownTagPattern; 27 | 28 | if (!options.renameAttrs) { 29 | options.renameAttrs = {}; 30 | } 31 | 32 | utils.catchup(openingEl.range[0], state, trimLeft); 33 | 34 | var tagName = nameObj.name; 35 | var isJSXIdentifier = nameObj.type === Syntax.JSXIdentifier; 36 | var knownTag = tagName[0] !== tagName[0].toUpperCase() && isJSXIdentifier; 37 | var hasAtLeastOneSpreadAttribute = attributes.some(function (attr) { 38 | return attr.type === Syntax.JSXSpreadAttribute; 39 | }); 40 | var secondArg = false; 41 | 42 | if (knownTag) { 43 | utils.append(factory + "('", state); // DOM('div', ...) 44 | } else if (options.passUnknownTagsToFactory) { 45 | if (options.unknownTagsAsString) { 46 | utils.append(factory + "('", state); 47 | } else { 48 | utils.append(factory + '(', state); 49 | } 50 | } 51 | 52 | utils.move(nameObj.range[0], state); 53 | 54 | if (knownTag) { 55 | // DOM('div', ...) 56 | utils.catchup(nameObj.range[1], state); 57 | utils.append("'", state); 58 | secondArg = true 59 | } else if (options.passUnknownTagsToFactory) { 60 | // DOM(Component, ...) 61 | utils.catchup(nameObj.range[1], state); 62 | if (options.unknownTagsAsString) { 63 | utils.append("'", state); 64 | } 65 | secondArg = true 66 | } else { 67 | // Component(...) 68 | tagName = unknownTagPattern.replace('{tag}', nameObj.name); 69 | utils.append(tagName, state); 70 | utils.move( 71 | nameObj.range[1] + (tagName.length - nameObj.name.length), 72 | state 73 | ); 74 | utils.append('(', state); 75 | } 76 | 77 | if (hasAtLeastOneSpreadAttribute) { 78 | if (options.passUnknownTagsToFactory || knownTag) { 79 | utils.append(', ' + spreadFn + '({', state); 80 | } else { 81 | utils.append(spreadFn + '({', state); 82 | } 83 | } else if (attributes.length) { 84 | if (secondArg) { 85 | utils.append(', ', state); 86 | } 87 | utils.append('{', state); 88 | } 89 | 90 | var previousWasSpread = false; 91 | 92 | attributes.forEach(function(attr, index) { 93 | var isLast = (index === (attributes.length - 1)); 94 | 95 | if (attr.type === Syntax.JSXSpreadAttribute) { 96 | // close the previous or initial object 97 | if (!previousWasSpread) { 98 | utils.append('}, ', state); 99 | } 100 | 101 | // Move to the expression start, ignoring everything except parenthesis 102 | // and whitespace. 103 | utils.catchup(attr.range[0], state, stripNonParen); 104 | // Plus 1 to skip `{`. 105 | utils.move(attr.range[0] + 1, state); 106 | utils.catchup(attr.argument.range[0], state, stripNonParen); 107 | 108 | traverse(attr.argument, path, state); 109 | 110 | utils.catchup(attr.argument.range[1], state); 111 | 112 | // Move to the end, ignoring parenthesis and the closing `}` 113 | utils.catchup(attr.range[1] - 1, state, stripNonParen); 114 | 115 | if (!isLast) { 116 | utils.append(', ', state); 117 | } 118 | 119 | utils.move(attr.range[1], state); 120 | 121 | previousWasSpread = true; 122 | 123 | return; 124 | } 125 | 126 | // If the next attribute is a spread, we're effective last in this object 127 | if (!isLast) { 128 | isLast = attributes[index + 1].type === Syntax.JSXSpreadAttribute; 129 | } 130 | 131 | var name 132 | if (attr.name.namespace) { 133 | name = attr.name.namespace.name + ':' + attr.name.name.name 134 | } 135 | else { 136 | name = attr.name.name; 137 | } 138 | 139 | utils.catchup(attr.range[0], state, trimLeft); 140 | 141 | if (previousWasSpread) { 142 | utils.append('{', state); 143 | } 144 | 145 | utils.append(quoteJSObjKey(name) + ': ', state); 146 | 147 | if (attr.value) { 148 | utils.move(attr.name.range[1], state); 149 | utils.catchupNewlines(attr.value.range[0], state); 150 | if (attr.value.type === Syntax.Literal) { 151 | renderJSXLiteral(attr.value, isLast, state); 152 | } else { 153 | renderJSXExpressionContainer(traverse, attr.value, isLast, path, state); 154 | } 155 | } else { 156 | state.g.buffer += 'true'; 157 | state.g.position = attr.name.range[1]; 158 | if (!isLast) { 159 | utils.append(', ', state); 160 | } 161 | } 162 | 163 | utils.catchup(attr.range[1], state, trimLeft); 164 | 165 | previousWasSpread = false; 166 | }); 167 | 168 | if (!openingEl.selfClosing) { 169 | utils.catchup(openingEl.range[1] - 1, state, trimLeft); 170 | utils.move(openingEl.range[1], state); 171 | } 172 | 173 | if (attributes.length && !previousWasSpread) { 174 | utils.append('}', state); 175 | } 176 | 177 | if (hasAtLeastOneSpreadAttribute) { 178 | utils.append(')', state); 179 | } 180 | 181 | // filter out whitespace 182 | var children = object.children.filter(function(child) { 183 | return !(child.type === Syntax.Literal 184 | && typeof child.value === 'string' 185 | && child.value.match(/^[ \t]*[\r\n][ \t\r\n]*$/)); 186 | }); 187 | 188 | if (children.length) { 189 | if (!attributes.length) { 190 | if (secondArg) { 191 | utils.append(', ', state); 192 | } 193 | utils.append('null', state); 194 | } 195 | var lastRenderableIndex; 196 | 197 | children.forEach(function(child, index) { 198 | if (child.type !== Syntax.JSXExpressionContainer || 199 | child.expression.type !== Syntax.JSXEmptyExpression) { 200 | lastRenderableIndex = index; 201 | } 202 | }); 203 | 204 | if (lastRenderableIndex !== undefined) { 205 | utils.append(', ', state); 206 | } 207 | 208 | if (arrayChildren && children.length) { 209 | utils.append('[', state); 210 | } 211 | 212 | children.forEach(function(child, index) { 213 | utils.catchup(child.range[0], state, trimLeft); 214 | 215 | var isFirst = index === 0; 216 | var isLast = index >= lastRenderableIndex; 217 | 218 | if (child.type === Syntax.Literal) { 219 | renderJSXLiteral(child, isLast, state); 220 | } else if (child.type === Syntax.JSXExpressionContainer) { 221 | renderJSXExpressionContainer(traverse, child, isLast, path, state); 222 | } else { 223 | traverse(child, path, state); 224 | if (!isLast) { 225 | utils.append(',', state); 226 | } 227 | } 228 | 229 | utils.catchup(child.range[1], state, trimLeft); 230 | }); 231 | } 232 | 233 | if (openingEl.selfClosing) { 234 | // everything up to /> 235 | utils.catchup(openingEl.range[1] - 2, state, trimLeft); 236 | utils.move(openingEl.range[1], state); 237 | } else { 238 | // everything up to 239 | utils.catchup(closingEl.range[0], state, trimLeft); 240 | utils.move(closingEl.range[1], state); 241 | } 242 | 243 | if (arrayChildren && children.length) { 244 | utils.append(']', state); 245 | } 246 | 247 | utils.append(')', state); 248 | 249 | return false; 250 | } 251 | 252 | /** 253 | * Returns true if node is JSX tag. 254 | * 255 | * @param {Object} object 256 | * @param {String} path 257 | * @param {Object} state 258 | * @returns {Boolean} 259 | * @private 260 | */ 261 | visitNode.test = function(object, path, state) { 262 | return object.type === Syntax.JSXElement; 263 | }; 264 | 265 | /** 266 | * Taken from {@link https://github.com/facebook/react/blob/0.10-stable/vendor/fbtransform/transforms/xjs.js} 267 | * 268 | * @param {Object} object 269 | * @param {Boolean} isLast 270 | * @param {Object} state 271 | * @param {Number} start 272 | * @param {Number} end 273 | * @private 274 | */ 275 | function renderJSXLiteral(object, isLast, state, start, end) { 276 | var lines = object.value.split(/\r\n|\n|\r/); 277 | 278 | if (start) { 279 | utils.append(start, state); 280 | } 281 | 282 | var lastNonEmptyLine = 0; 283 | 284 | lines.forEach(function (line, index) { 285 | if (line.match(/[^ \t]/)) { 286 | lastNonEmptyLine = index; 287 | } 288 | }); 289 | 290 | lines.forEach(function (line, index) { 291 | var isFirstLine = index === 0; 292 | var isLastLine = index === lines.length - 1; 293 | var isLastNonEmptyLine = index === lastNonEmptyLine; 294 | 295 | // replace rendered whitespace tabs with spaces 296 | var trimmedLine = line.replace(/\t/g, ' '); 297 | 298 | // trim whitespace touching a newline 299 | if (!isFirstLine) { 300 | trimmedLine = trimmedLine.replace(/^[ ]+/, ''); 301 | } 302 | if (!isLastLine) { 303 | trimmedLine = trimmedLine.replace(/[ ]+$/, ''); 304 | } 305 | 306 | if (!isFirstLine) { 307 | utils.append(line.match(/^[ \t]*/)[0], state); 308 | } 309 | 310 | if (trimmedLine || isLastNonEmptyLine) { 311 | utils.append( 312 | JSON.stringify(trimmedLine) + 313 | (!isLastNonEmptyLine ? " + ' ' +" : ''), 314 | state); 315 | 316 | if (isLastNonEmptyLine) { 317 | if (end) { 318 | utils.append(end, state); 319 | } 320 | if (!isLast) { 321 | utils.append(', ', state); 322 | } 323 | } 324 | 325 | // only restore tail whitespace if line had literals 326 | if (trimmedLine && !isLastLine) { 327 | utils.append(line.match(/[ \t]*$/)[0], state); 328 | } 329 | } 330 | 331 | if (!isLastLine) { 332 | utils.append('\n', state); 333 | } 334 | }); 335 | 336 | utils.move(object.range[1], state); 337 | } 338 | 339 | /** 340 | * Taken from {@link https://github.com/facebook/react/blob/0.10-stable/vendor/fbtransform/transforms/xjs.js} 341 | * 342 | * @param {Function} traverse 343 | * @param {Object} object 344 | * @param {Boolean} isLast 345 | * @param {String} path 346 | * @param {Object} state 347 | * @returns {Boolean} 348 | * @private 349 | */ 350 | function renderJSXExpressionContainer(traverse, object, isLast, path, state) { 351 | // Plus 1 to skip `{`. 352 | utils.move(object.range[0] + 1, state); 353 | traverse(object.expression, path, state); 354 | 355 | if (!isLast && object.expression.type !== Syntax.JSXEmptyExpression) { 356 | // If we need to append a comma, make sure to do so after the expression. 357 | utils.catchup(object.expression.range[1], state, trimLeft); 358 | utils.append(', ', state); 359 | } 360 | 361 | // Minus 1 to skip `}`. 362 | utils.catchup(object.range[1] - 1, state, trimLeft); 363 | utils.move(object.range[1], state); 364 | return false; 365 | } 366 | 367 | /** 368 | * Quote invalid object literal keys. 369 | * 370 | * @param {String} name 371 | * @returns {String} 372 | * @private 373 | */ 374 | function quoteJSObjKey(name) { 375 | if (!/^[a-z_$][a-z\d_$]*$/i.test(name)) { 376 | return "'" + name + "'"; 377 | } 378 | return name; 379 | } 380 | 381 | /** 382 | * Trim whitespace left of `val`. 383 | * 384 | * @param {String} val 385 | * @returns {String} 386 | * @private 387 | */ 388 | function trimLeft(val) { 389 | return val.replace(/^ +/, ''); 390 | } 391 | 392 | /** 393 | * Removes all non-parenthesis characters 394 | */ 395 | var reNonParen = /([^\(\)])/g; 396 | function stripNonParen(value) { 397 | return value.replace(reNonParen, ''); 398 | } 399 | --------------------------------------------------------------------------------