├── 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 = ;
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 [](http://travis-ci.org/alexmingoia/jsx-transform) [](https://npmjs.org/package/jsx-transform) [](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 [](http://travis-ci.org/alexmingoia/jsx-transform) [](https://npmjs.org/package/jsx-transform) [](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 |
--------------------------------------------------------------------------------