* Single child for rt-template (fixes #138)

* Nested scope functions (fixes #139)

* Forbid nested rt-import (fixes #134)

* Vendor prefixes style keys (#144)

* Renamed test file

* Forbid expressions in style keys (#145)

* fixed rt-import and AMD (#147)

* Updated documentation

* Fixed links in README.md

* Sanitize comments (fix #158)

* Disable comments when es6 (fix#157)

* Added test case for comments

* Simplified TypeScript output

* Test cases for simplified TypeScript output

* Fixed wrong TypeScript output (#161)

* fix repeat with custom index (#162)
This commit is contained in:
nino-porcino 2016-07-06 10:25:08 +02:00 committed by Ido
parent c3a6e46220
commit 46caeea387
31 changed files with 236 additions and 119 deletions

View File

@ -44,9 +44,9 @@ http://plugins.jetbrains.com/plugin/7648
* [rt-scope](#rt-scope)
* [rt-props](#rt-props)
* [rt-class](#rt-class)
* [rt-import](#using-other-components-in-the-template)
* [rt-import](#rt-import)
* ~~rt-require~~ (deprecated, use rt-import)
* [rt-template](#rt-template-and-defining-properties-template-functions)
* [rt-template](#rt-template)
* [rt-include](#rt-include)
* [styles](#styles)
* [event handlers](#event-handlers)
@ -379,7 +379,7 @@ define([
## stateless components
Since React v0.14, [React allows defining a component as a pure function of its props](https://facebook.github.io/react/docs/reusable-components.html#stateless-functions).
To enable creating a stateless component using react templates, add the `rt-stateless` attribute to the template's root element.
Using `rt-stateless` generates a stateless functional component instead of a render function.
Using `rt-stateless` generates a stateless functional component instead of a render function.
The resulting function receives `props` and `context` parameters to be used in the template instead of `this.props`.
###### Sample:
@ -472,14 +472,17 @@ export default function () {
###### Compiled (AMD):
```javascript
define('div', [
'react/addons',
'react',
'lodash',
'module-name',
'module-name',
'module-name',
'module-name'
], function (React, _, member, alias2, alias3, alias4) {
], function (React, _, $2, $3, alias3, $5) {
'use strict';
var member = $2.member;
var alias2 = $3.member;
var alias4 = $5.default;
return function () {
return React.createElement('div', {});
};

View File

@ -34,17 +34,12 @@ _.forEach(reactSupportedAttributes, attributeReactName => {
const htmlSelfClosingTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
const templateAMDTemplate = _.template("define(<%= name ? '\"'+name + '\", ' : '' %>[<%= requirePaths %>], function (<%= requireNames %>) {\n'use strict';\n <%= injectedFunctions %>\nreturn function(<%= statelessParams %>){ return <%= body %>};\n});");
const templateCommonJSTemplate = _.template("'use strict';\n<%= vars %>\n\n<%= injectedFunctions %>\nmodule.exports = function(<%= statelessParams %>){ return <%= body %>};\n");
const templateES6Template = _.template('<%= vars %>\n\n<%= injectedFunctions %>\nexport default function(<%= statelessParams %>){ return <%= body %>}\n');
const templatePJSTemplate = _.template(`var <%= name %> = function (<%= statelessParams %>) {
<%= injectedFunctions %>
return <%= body %>
};
`);
const templateTypescriptTemplate = _.template('<%= vars %>\n\n<%= injectedFunctions %>\nvar fn = function() { return <%= body %> };\nexport = fn\n');
const templateJSRTTemplate = _.template('(function () {\n <%= injectedFunctions %>\n return function(){\nreturn <%= body %>}}\n)()');
const templateAMDTemplate = _.template("define(<%= name ? '\"'+name + '\", ' : '' %>[<%= requirePaths %>], function (<%= AMDArguments %>) {\n'use strict';\n<%= AMDSubstitutions %>return <%= renderFunction %>;\n});");
const templateCommonJSTemplate = _.template("'use strict';\n<%= vars %>\nmodule.exports = <%= renderFunction %>;\n");
const templateES6Template = _.template('<%= vars %>\nexport default <%= renderFunction %>\n');
const templatePJSTemplate = _.template('var <%= name %> = <%= renderFunction %>');
const templateTypescriptTemplate = _.template('<%= vars %>\nexport = <%= renderFunction %>;\n');
const templateJSRTTemplate = _.template('<%= renderFunction %>');
const templates = {
amd: templateAMDTemplate,
@ -60,7 +55,8 @@ const defaultCase = _.constant(true);
const buildImportTypeScript = _.cond([
[isImportAsterisk, d => `import ${d.alias} = require('${d.moduleName}');`],
[defaultCase, d => `import ${d.alias} = require('${d.moduleName}').${d.member};`]
[_.matches({member: 'default'}), d => `import ${d.alias} from '${d.moduleName}';`],
[defaultCase, d => `import { ${d.member} as ${d.alias} } from '${d.moduleName}';`]
]);
const buildImportES6 = _.cond([

View File

@ -123,6 +123,9 @@ function generateTemplateProps(node, context) {
if (!_.has(child.attribs, 'prop')) {
throw RTCodeError.build(context, child, 'rt-template must have a prop attribute');
}
if (_.filter(child.children, {type: 'tag'}).length !== 1) {
throw RTCodeError.build(context, child, "'rt-template' should have a single non-text element as direct child");
}
const childTemplate = _.find(context.options.propTemplates, {prop: child.attribs.prop}) || {arguments: []};
templateProp = {
@ -225,9 +228,13 @@ function handleStyleProp(val, node, context) {
.filter(i => _.includes(i, ':'))
.map(i => {
const pair = i.split(':');
const key = pair[0].trim();
if (/\{|\}/g.test(key)) {
throw RTCodeError.build(context, node, 'style attribute keys cannot contain { } expressions');
}
const value = pair.slice(1).join(':').trim();
return _.camelCase(pair[0].trim()) + ' : ' + utils.convertText(node, context, value.trim());
const parsedKey = /(^-moz-)|(^-o-)|(^-webkit-)/ig.test(key) ? _.upperFirst(_.camelCase(key)) : _.camelCase(key);
return parsedKey + ' : ' + utils.convertText(node, context, value.trim());
})
.join(',');
return `{${styleStr}}`;
@ -290,6 +297,10 @@ function convertHtmlToReact(node, context) {
boundParams: _.clone(context.boundParams)
}, context);
if (node.type === 'tag' && node.name === importAttr) {
throw RTCodeError.build(context, node, "'rt-import' must be a toplevel node");
}
if (node.type === 'tag' && node.name === includeNode) {
const srcFile = node.attribs[includeSrcAttr];
if (!srcFile) {
@ -391,7 +402,7 @@ function convertHtmlToReact(node, context) {
// the scope variables are evaluated in context of the current iteration.
if (node.attribs[repeatAttr]) {
data.repeatFunction = generateInjectedFunc(context, 'repeat' + _.upperFirst(data.item), 'return ' + data.body);
data.repeatBinds = ['this'].concat(_.reject(context.boundParams, p => p === data.item || p === data.item + 'Index' || data.innerScope && p in data.innerScope.innerMapping));
data.repeatBinds = ['this'].concat(_.reject(context.boundParams, p => p === data.item || p === data.index || data.innerScope && p in data.innerScope.innerMapping));
data.body = repeatTemplate(data);
}
if (node.attribs[ifAttr]) {
@ -399,7 +410,8 @@ function convertHtmlToReact(node, context) {
}
return data.body;
} else if (node.type === 'comment') {
return commentTemplate(node);
const sanitizedComment = node.data.split('*/').join('* /');
return commentTemplate({data: sanitizedComment});
} else if (node.type === 'text') {
return node.data.trim() ? utils.convertText(node, context, node.data) : '';
}
@ -562,33 +574,44 @@ function convertRT(html, reportContext, options) {
const context = defaultContext(html, options, reportContext);
const body = parseAndConvertHtmlToReact(html, context);
const injectedFunctions = context.injectedFunctions.join('\n');
const statelessParams = context.stateless ? 'props, context' : '';
const renderFunction = `function(${statelessParams}) { ${injectedFunctions}return ${body} }`;
const requirePaths = _.map(context.defines, d => `"${d.moduleName}"`).join(',');
const requireNames = _.map(context.defines, d => `${d.alias}`).join(',');
const AMDArguments = _.map(context.defines, (d, i) => (d.member === '*' ? `${d.alias}` : `$${i}`)).join(','); //eslint-disable-line
const AMDSubstitutions = _.map(context.defines, (d, i) => (d.member === '*' ? null : `var ${d.alias} = $${i}.${d.member};`)).join('\n'); //eslint-disable-line
const buildImport = reactSupport.buildImport[options.modules] || reactSupport.buildImport.commonjs;
const requires = _.map(context.defines, buildImport).join('\n');
const header = options.flow ? '/* @flow */\n' : '';
const vars = header + requires;
const data = {
body,
injectedFunctions: context.injectedFunctions.join('\n'),
renderFunction,
requireNames,
requirePaths,
AMDArguments,
AMDSubstitutions,
vars,
name: options.name,
statelessParams: context.stateless ? 'props, context' : ''
name: options.name
};
let code = templates[options.modules](data);
if (options.modules !== 'typescript' && options.modules !== 'jsrt') {
code = parseJS(code);
code = parseJS(code, options);
}
return code;
}
function parseJS(code) {
function parseJS(code, options) {
try {
let tree = esprima.parse(code, {range: true, tokens: true, comment: true, sourceType: 'module'});
tree = escodegen.attachComments(tree, tree.comments, tree.tokens);
// fix for https://github.com/wix/react-templates/issues/157
// do not include comments for es6 modules due to bug in dependency "escodegen"
// to be removed when https://github.com/estools/escodegen/issues/263 will be fixed
// remove also its test case "test/data/comment.rt.es6.js"
if (options.modules !== 'es6') {
tree = escodegen.attachComments(tree, tree.comments, tree.tokens);
}
return escodegen.generate(tree, {comment: true});
} catch (e) {
throw new RTCodeError(e.message, e.index, -1);
@ -601,7 +624,7 @@ function convertJSRTToJS(text, reportContext, options) {
const templateMatcherJSRT = /<template>([^]*?)<\/template>/gm;
const code = text.replace(templateMatcherJSRT, (template, html) => convertRT(html, reportContext, options).replace(/;$/, ''));
return parseJS(code);
return parseJS(code, options);
}
module.exports = {

6
test/data/comment.rt Normal file
View File

@ -0,0 +1,6 @@
<div>
<!-- this is a comment -->
<div>hello</div>
<!-- /* this is another comment */ -->
<div>hello</div>
</div>

View File

@ -0,0 +1,9 @@
define([
'react',
'lodash'
], function (React, _) {
'use strict';
return function () {
return React.createElement('div', {} /* this is a comment */, React.createElement('div', {}, 'hello') /* /* this is another comment * / */, React.createElement('div', {}, 'hello'));
};
});

View File

@ -0,0 +1,5 @@
import * as React from 'react';
import * as _ from 'lodash';
export default function () {
return React.createElement('div', {}, React.createElement('div', {}, 'hello'), React.createElement('div', {}, 'hello'));
}

View File

@ -1,6 +1,4 @@
import React = require('react');
import _ = require('lodash');
export = function() { return React.createElement('div',{}) };
var fn = function() { return React.createElement('div',{}) };
export = fn

View File

@ -3,11 +3,11 @@ define([
'lodash'
], function (React, _) {
'use strict';
function scopeActiveUsers1() {
var activeUsers = this.activeUsers;
return React.createElement('div', { 'key': 'active-users' }, React.createElement('span', {}, 'some text'));
}
return function () {
function scopeActiveUsers1() {
var activeUsers = this.activeUsers;
return React.createElement('div', { 'key': 'active-users' }, React.createElement('span', {}, 'some text'));
}
return this.activeUsers ? scopeActiveUsers1.apply(this, []) : null;
};
});

View File

@ -5,8 +5,11 @@ define('div', [
'module-name',
'module-name',
'module-name'
], function (React, _, member, alias2, alias3, alias4) {
], function (React, _, $2, $3, alias3, $5) {
'use strict';
var member = $2.member;
var alias2 = $3.member;
var alias4 = $5.default;
return function () {
return React.createElement('div', {});
};

View File

@ -1,10 +1,8 @@
import React = require('react');
import _ = require('lodash');
import member = require('module-name').member;
import alias2 = require('module-name').member;
import { member as member } from 'module-name';
import { member as alias2 } from 'module-name';
import alias3 = require('module-name');
import alias4 = require('module-name').default;
import alias4 from 'module-name';
export = function() { return React.createElement('div',{}) };
var fn = function() { return React.createElement('div',{}) };
export = fn

View File

@ -0,0 +1,3 @@
<div>
<rt-import name="*" as="React" from="react"/>
</div>

View File

@ -0,0 +1,5 @@
<div>
<rt-template prop="some">
text children are not allowed
</rt-template>
</div>

View File

@ -0,0 +1,6 @@
<div>
<rt-template prop="some">
<div>1</div>
<div>2</div>
</rt-template>
</div>

View File

@ -0,0 +1,3 @@
<div rt-scope="'ground' as g">
<span style="back{g}:red"></span>
</div>

View File

@ -0,0 +1,13 @@
define([
'react',
'lodash'
], function (React, _) {
'use strict';
return function () {
function scopeG1() {
var g = 'ground';
return React.createElement('div', {}, React.createElement('span', { 'style': { backG: 'red' } }));
}
return scopeG1.apply(this, []);
};
});

View File

@ -1,13 +1,13 @@
'use strict';
var React = require('react-native');
var _ = require('lodash');
function renderRow1(rowData) {
return React.createElement(React.Text, {}, rowData);
}
function renderRow2(item) {
return React.createElement(React.Text, {}, item);
}
module.exports = function () {
function renderRow1(rowData) {
return React.createElement(React.Text, {}, rowData);
}
function renderRow2(item) {
return React.createElement(React.Text, {}, item);
}
return React.createElement(React.View, {}, React.createElement(React.ListView, {
'dataSource': this.state.dataSource,
'renderRow': renderRow1.bind(this)

View File

@ -1,10 +1,10 @@
'use strict';
var React = require('react-native');
var _ = require('lodash');
function renderRow1(rowData) {
return React.createElement(React.Text, {}, rowData);
}
module.exports = function () {
function renderRow1(rowData) {
return React.createElement(React.Text, {}, rowData);
}
return React.createElement(React.View, {}, React.createElement(React.ListView, {
'dataSource': this.state.dataSource,
'renderRow': renderRow1.bind(this)

View File

@ -3,10 +3,10 @@ define([
'lodash'
], function (React, _) {
'use strict';
function renderRow1(rowData) {
return React.createElement('div', {}, rowData);
}
return function () {
function renderRow1(rowData) {
return React.createElement('div', {}, rowData);
}
return React.createElement('div', {}, React.createElement(List, {
'data': [
1,

View File

@ -3,10 +3,10 @@ define([
'lodash'
], function (React, _) {
'use strict';
function templateProp1(arg1) {
return React.createElement('div', {}, arg1);
}
return function () {
function templateProp1(arg1) {
return React.createElement('div', {}, arg1);
}
return React.createElement('div', { 'templateProp': templateProp1.bind(this) });
};
});

View File

@ -3,14 +3,14 @@ define([
'lodash'
], function (React, _) {
'use strict';
function templateProp1(name, arg1) {
return React.createElement('div', {}, 'Name: ', name, ' ', arg1);
}
function scopeName2() {
var name = 'boten';
return React.createElement('div', { 'templateProp': templateProp1.bind(this, name) });
}
return function () {
function templateProp1(name, arg1) {
return React.createElement('div', {}, 'Name: ', name, ' ', arg1);
}
function scopeName2() {
var name = 'boten';
return React.createElement('div', { 'templateProp': templateProp1.bind(this, name) });
}
return scopeName2.apply(this, []);
};
});

View File

@ -3,13 +3,13 @@ define([
'lodash'
], function (React, _) {
'use strict';
function templateProp21(arg1, inner1, inner2) {
return React.createElement('div', {}, arg1 + inner1 + inner2);
}
function templateProp2(arg1) {
return React.createElement('div', { 'templateProp2': templateProp21.bind(this, arg1) }, React.createElement('div', {}, arg1));
}
return function () {
function templateProp21(arg1, inner1, inner2) {
return React.createElement('div', {}, arg1 + inner1 + inner2);
}
function templateProp2(arg1) {
return React.createElement('div', { 'templateProp2': templateProp21.bind(this, arg1) }, React.createElement('div', {}, arg1));
}
return React.createElement('div', { 'templateProp': templateProp2.bind(this) });
};
});

View File

@ -3,14 +3,14 @@ define([
'lodash'
], function (React, _) {
'use strict';
function repeatItem1(item, customIndex) {
return React.createElement('li', {}, item, ' is number ', customIndex);
}
return function () {
function repeatItem1(item, customIndex) {
return React.createElement('li', {}, item, ' is number ', customIndex);
}
return React.createElement.apply(this, [
'ul',
{},
_.map(this.props.collection, repeatItem1.bind(this, customIndex))
_.map(this.props.collection, repeatItem1.bind(this))
]);
};
});

View File

@ -3,25 +3,25 @@ define([
'lodash'
], function (React, _) {
'use strict';
function onClick1(items, itemsIndex, evt) {
this.happend(evt);
return false;
}
function onMouseDown2(items, itemsIndex) {
this.happend();
return false;
}
function repeatItems3(items, itemsIndex) {
return React.createElement('div', {}, React.createElement('span', {
'style': {
width: 'auto',
lineHeight: '5px'
},
'onClick': onClick1.bind(this, items, itemsIndex),
'onMouseDown': onMouseDown2.bind(this, items, itemsIndex)
}, 'Mock'));
}
return function () {
function onClick1(items, itemsIndex, evt) {
this.happend(evt);
return false;
}
function onMouseDown2(items, itemsIndex) {
this.happend();
return false;
}
function repeatItems3(items, itemsIndex) {
return React.createElement('div', {}, React.createElement('span', {
'style': {
width: 'auto',
lineHeight: '5px'
},
'onClick': onClick1.bind(this, items, itemsIndex),
'onMouseDown': onMouseDown2.bind(this, items, itemsIndex)
}, 'Mock'));
}
return React.createElement.apply(this, [
'p',
{},

View File

@ -2,7 +2,5 @@ import React = require('react');
import _ = require('lodash');
import myComp = require('comps/myComp');
import utils = require('utils/utils');
export = function() { return React.createElement(myComp,{},"\n",(utils.translate('Hello','es')),"\n") };
var fn = function() { return React.createElement(myComp,{},"\n",(utils.translate('Hello','es')),"\n") };
export = fn

View File

@ -4,10 +4,8 @@ define([
], function (React, _) {
var comp = React.createClass({
render: function () {
return function () {
return React.createElement('div', {}, 'hello world');
};
}()
return React.createElement('div', {}, 'hello world');
}
});
return comp;
});

View File

@ -0,0 +1,4 @@
<div>
<span style="-moz-transform:2;-ms-transform:2;-o-transform:2;-webkit-transform:2;transform:2"></span>
<span style="-Moz-transform:2;-Ms-transform:2;-O-transform:2;-Webkit-transform:2;Transform:2"></span>
</div>

View File

@ -0,0 +1,25 @@
define([
'react',
'lodash'
], function (React, _) {
'use strict';
return function () {
return React.createElement('div', {}, React.createElement('span', {
'style': {
MozTransform: '2',
msTransform: '2',
OTransform: '2',
WebkitTransform: '2',
transform: '2'
}
}), React.createElement('span', {
'style': {
MozTransform: '2',
msTransform: '2',
OTransform: '2',
WebkitTransform: '2',
transform: '2'
}
}));
};
});

View File

@ -3,24 +3,24 @@ define([
'lodash'
], function (React, _) {
'use strict';
function repeatN1(verb, n, nIndex) {
return [
React.createElement('div', { 'key': '2211' }, verb, ' ', n, '-a'),
React.createElement('div', { 'key': '2213' }, verb, ' ', n, '-b')
];
}
function scopeVerb2() {
var verb = 'rendered';
return [
1 < 0 ? [React.createElement('div', { 'key': '551' }, 'this is not ', verb)] : null,
1 > 0 ? [React.createElement('div', { 'key': '1401' }, 'this is ', verb)] : null,
_.map([
1,
2
], repeatN1.bind(this, verb))
];
}
return function () {
function repeatN1(verb, n, nIndex) {
return [
React.createElement('div', { 'key': '2211' }, verb, ' ', n, '-a'),
React.createElement('div', { 'key': '2213' }, verb, ' ', n, '-b')
];
}
function scopeVerb2() {
var verb = 'rendered';
return [
1 < 0 ? [React.createElement('div', { 'key': '551' }, 'this is not ', verb)] : null,
1 > 0 ? [React.createElement('div', { 'key': '1401' }, 'this is ', verb)] : null,
_.map([
1,
2
], repeatN1.bind(this, verb))
];
}
return React.createElement('div', {}, scopeVerb2.apply(this, []));
};
});

View File

@ -28,8 +28,12 @@ module.exports = {
{file: 'invalid-rt-import-1.rt', issue: new RTCodeError("'*' imports must have an 'as' attribute", 0, 36, 1, 1)},
{file: 'invalid-rt-import-2.rt', issue: new RTCodeError("default imports must have an 'as' attribute", 0, 42, 1, 1)},
{file: 'invalid-rt-import-3.rt', issue: new RTCodeError("'rt-import' needs 'name' and 'from' attributes", 0, 13, 1, 1)},
{file: 'invalid-rt-import-4.rt', issue: new RTCodeError("'rt-import' must be a toplevel node", 9, 54, 2, 4)},
{file: 'invalid-rt-template-1.rt', issue: new RTCodeError("'rt-template' should have a single non-text element as direct child", 9, 88, 2, 4)},
{file: 'invalid-rt-template-2.rt', issue: new RTCodeError("'rt-template' should have a single non-text element as direct child", 9, 90, 2, 4)},
{file: 'invalid-brace.rt', issue: new RTCodeError('Unexpected end of input', 128, 163, 5, 11)},
{file: 'invalid-style.rt', issue: new RTCodeError('Unexpected token ILLEGAL', 10, 39, 2, 5)},
{file: 'invalid-style-1.rt', issue: new RTCodeError('Unexpected token ILLEGAL', 10, 39, 2, 5)},
{file: 'invalid-style-2.rt', issue: new RTCodeError('style attribute keys cannot contain { } expressions', 35, 68, 2, 5)},
{file: 'invalid-virtual.rt', issue: new RTCodeError('Document should not have <rt-virtual> as root element', 0, 60, 1, 1)}
];

View File

@ -29,7 +29,7 @@ module.exports = {
});
test('conversion test', t => {
const files = ['div.rt', 'test.rt', 'repeat.rt', 'repeat-with-index.rt', 'inputs.rt', 'virtual.rt', 'stateless.rt'];
const files = ['div.rt', 'test.rt', 'repeat.rt', 'repeat-with-index.rt', 'inputs.rt', 'virtual.rt', 'stateless.rt', 'style-vendor-prefix.rt'];
testFiles(t, files);
});
@ -78,6 +78,23 @@ module.exports = {
}
});
test('convert comment with AMD and ES6 modules', t => {
const files = [
{source: 'comment.rt', expected: 'comment.rt.amd.js', options: {modules: 'amd'}},
{source: 'comment.rt', expected: 'comment.rt.es6.js', options: {modules: 'es6'}}
];
t.plan(files.length);
files.forEach(check);
function check(testData) {
const filename = path.join(dataPath, testData.source);
const html = readFileNormalized(filename);
const expected = readFileNormalized(path.join(dataPath, testData.expected));
const actual = reactTemplates.convertTemplateToReact(html, testData.options).replace(/\r/g, '').trim();
compareAndWrite(t, actual, expected, filename);
}
});
test('rt-require with all module types', t => {
const files = [
{source: 'require.rt', expected: 'require.rt.commonjs.js', options: {modules: 'commonjs'}},