From 46caeea387fead1c62d4837ca5fd700e334ef6c7 Mon Sep 17 00:00:00 2001 From: nino-porcino Date: Wed, 6 Jul 2016 10:25:08 +0200 Subject: [PATCH] Fix #138, #139, #134, #144, #145, #147, #157, #158, #161, #162 (#142) * 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) --- README.md | 13 +++-- src/reactSupport.js | 20 ++++---- src/reactTemplates.js | 47 ++++++++++++++----- test/data/comment.rt | 6 +++ test/data/comment.rt.amd.js | 9 ++++ test/data/comment.rt.es6.js | 5 ++ test/data/div.rt.typescript.ts | 4 +- test/data/if-with-scope/valid-if-scope.rt.js | 8 ++-- test/data/import.rt.amd.js | 5 +- test/data/import.rt.typescript.ts | 10 ++-- test/data/invalid/invalid-rt-import-4.rt | 3 ++ test/data/invalid/invalid-rt-template-1.rt | 5 ++ test/data/invalid/invalid-rt-template-2.rt | 6 +++ .../{invalid-style.rt => invalid-style-1.rt} | 0 test/data/invalid/invalid-style-2.rt | 3 ++ test/data/invalid/invalid-style-2.rt.js | 13 +++++ .../native/listViewAndCustomTemplate.rt.js | 12 ++--- test/data/native/listViewTemplate.rt.js | 6 +-- .../data/propTemplates/implicitTemplate.rt.js | 6 +-- test/data/propTemplates/simpleTemplate.rt.js | 6 +-- test/data/propTemplates/templateInScope.rt.js | 14 +++--- test/data/propTemplates/twoTemplates.rt.js | 12 ++--- test/data/repeat-with-index.rt.js | 8 ++-- test/data/repeat.rt.js | 36 +++++++------- test/data/require.rt.typescript.ts | 4 +- test/data/simple.js | 6 +-- test/data/style-vendor-prefix.rt | 4 ++ test/data/style-vendor-prefix.rt.js | 25 ++++++++++ test/data/virtual.rt.js | 34 +++++++------- test/src/rt.invalid.spec.js | 6 ++- test/src/rt.valid.spec.js | 19 +++++++- 31 files changed, 236 insertions(+), 119 deletions(-) create mode 100644 test/data/comment.rt create mode 100644 test/data/comment.rt.amd.js create mode 100644 test/data/comment.rt.es6.js create mode 100644 test/data/invalid/invalid-rt-import-4.rt create mode 100644 test/data/invalid/invalid-rt-template-1.rt create mode 100644 test/data/invalid/invalid-rt-template-2.rt rename test/data/invalid/{invalid-style.rt => invalid-style-1.rt} (100%) create mode 100644 test/data/invalid/invalid-style-2.rt create mode 100644 test/data/invalid/invalid-style-2.rt.js create mode 100644 test/data/style-vendor-prefix.rt create mode 100644 test/data/style-vendor-prefix.rt.js diff --git a/README.md b/README.md index 4e7d07e..ef50635 100644 --- a/README.md +++ b/README.md @@ -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', {}); }; diff --git a/src/reactSupport.js b/src/reactSupport.js index bcc08f4..1ead764 100644 --- a/src/reactSupport.js +++ b/src/reactSupport.js @@ -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([ diff --git a/src/reactTemplates.js b/src/reactTemplates.js index 6fec433..6772d6f 100644 --- a/src/reactTemplates.js +++ b/src/reactTemplates.js @@ -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 = /