diff --git a/lib/6to5/transformation/file.js b/lib/6to5/transformation/file.js index 8ee4b9d312..03265bea7b 100644 --- a/lib/6to5/transformation/file.js +++ b/lib/6to5/transformation/file.js @@ -163,6 +163,12 @@ File.prototype.normalizeOptions = function (opts) { opts.optional = transform._ensureTransformerNames("optional", opts.optional); opts.loose = transform._ensureTransformerNames("loose", opts.loose); + if (opts.reactCompat) { + opts.optional.push("reactCompat"); + console.error("The reactCompat option has been moved into the optional transformer " + + "`reactCompat` - backwards compatibility will be removed in v4.0.0"); + } + return opts; }; diff --git a/lib/6to5/transformation/helpers/build-react-transformer.js b/lib/6to5/transformation/helpers/build-react-transformer.js new file mode 100644 index 0000000000..414358ad79 --- /dev/null +++ b/lib/6to5/transformation/helpers/build-react-transformer.js @@ -0,0 +1,278 @@ +"use strict"; + +// Based upon the excellent jsx-transpiler by Ingvar Stepanyan (RReverser) +// https://github.com/RReverser/jsx-transpiler + +// jsx + +var isString = require("lodash/lang/isString"); +var esutils = require("esutils"); +var react = require("./react"); +var t = require("../../types"); + +module.exports = function (exports, opts) { + exports.check = function (node) { + if (t.isJSX(node)) return true; + if (react.isCreateClass(node)) return true; + return false; + }; + + exports.JSXIdentifier = function (node, parent) { + if (node.name === "this" && t.isReferenced(node, parent)) { + return t.thisExpression(); + } else if (esutils.keyword.isIdentifierName(node.name)) { + node.type = "Identifier"; + } else { + return t.literal(node.name); + } + }; + + exports.JSXNamespacedName = function (node, parent, scope, file) { + throw file.errorWithNode(node, "Namespace tags are not supported. ReactJSX is not XML."); + }; + + exports.JSXMemberExpression = { + exit: function (node) { + node.computed = t.isLiteral(node.property); + node.type = "MemberExpression"; + } + }; + + exports.JSXExpressionContainer = function (node) { + return node.expression; + }; + + exports.JSXAttribute = { + exit: function (node) { + var value = node.value || t.literal(true); + return t.inherits(t.property("init", node.name, value), node); + } + }; + exports.JSXOpeningElement = { + exit: function (node, parent, scope, file) { + var tagExpr = node.name; + var args = []; + + var tagName; + if (t.isIdentifier(tagExpr)) { + tagName = tagExpr.name; + } else if (t.isLiteral(tagExpr)) { + tagName = tagExpr.value; + } + + var state = { + tagExpr: tagExpr, + tagName: tagName, + args: args + }; + + if (opts.pre) { + opts.pre(state); + } + + var attribs = node.attributes; + if (attribs.length) { + attribs = buildJSXOpeningElementAttributes(attribs, file); + } else { + attribs = t.literal(null); + } + + args.push(attribs); + + if (opts.post) { + opts.post(state); + } + + return state.call || t.callExpression(state.callee, args); + } + }; + + /** + * The logic for this is quite terse. It's because we need to + * support spread elements. We loop over all attributes, + * breaking on spreads, we then push a new object containg + * all prior attributes to an array for later processing. + */ + + var buildJSXOpeningElementAttributes = function (attribs, file) { + var _props = []; + var objs = []; + + var pushProps = function () { + if (!_props.length) return; + + objs.push(t.objectExpression(_props)); + _props = []; + }; + + while (attribs.length) { + var prop = attribs.shift(); + if (t.isJSXSpreadAttribute(prop)) { + pushProps(); + objs.push(prop.argument); + } else { + _props.push(prop); + } + } + + pushProps(); + + if (objs.length === 1) { + // only one object + attribs = objs[0]; + } else { + // looks like we have multiple objects + if (!t.isObjectExpression(objs[0])) { + objs.unshift(t.objectExpression([])); + } + + // spread it + attribs = t.callExpression( + file.addHelper("extends"), + objs + ); + } + + return attribs; + }; + + exports.JSXElement = { + exit: function (node) { + var callExpr = node.openingElement; + + for (var i = 0; i < node.children.length; i++) { + var child = node.children[i]; + + if (t.isLiteral(child) && typeof child.value === "string") { + cleanJSXElementLiteralChild(child, callExpr.arguments); + continue; + } else if (t.isJSXEmptyExpression(child)) { + continue; + } + + callExpr.arguments.push(child); + } + + callExpr.arguments = flatten(callExpr.arguments); + + if (callExpr.arguments.length >= 3) { + callExpr._prettyCall = true; + } + + return t.inherits(callExpr, node); + } + }; + + var isStringLiteral = function (node) { + return t.isLiteral(node) && isString(node.value); + }; + + var flatten = function (args) { + var flattened = []; + var last; + + for (var i = 0; i < args.length; i++) { + var arg = args[i]; + if (isStringLiteral(arg) && isStringLiteral(last)) { + last.value += arg.value; + } else { + last = arg; + flattened.push(arg); + } + } + + return flattened; + }; + + var cleanJSXElementLiteralChild = function (child, args) { + var lines = child.value.split(/\r\n|\n|\r/); + + var lastNonEmptyLine = 0; + var i; + + for (i = 0; i < lines.length; i++) { + if (lines[i].match(/[^ \t]/)) { + lastNonEmptyLine = i; + } + } + + for (i = 0; i < lines.length; i++) { + var line = lines[i]; + + var isFirstLine = i === 0; + var isLastLine = i === lines.length - 1; + var isLastNonEmptyLine = i === lastNonEmptyLine; + + // replace rendered whitespace tabs with spaces + var trimmedLine = line.replace(/\t/g, " "); + + // trim whitespace touching a newline + if (!isFirstLine) { + trimmedLine = trimmedLine.replace(/^[ ]+/, ""); + } + + // trim whitespace touching an endline + if (!isLastLine) { + trimmedLine = trimmedLine.replace(/[ ]+$/, ""); + } + + if (trimmedLine) { + if (!isLastNonEmptyLine) { + trimmedLine += " "; + } + + args.push(t.literal(trimmedLine)); + } + } + }; + + // display names + + var addDisplayName = function (id, call) { + var props = call.arguments[0].properties; + var safe = true; + + for (var i = 0; i < props.length; i++) { + var prop = props[i]; + if (t.isIdentifier(prop.key, { name: "displayName" })) { + safe = false; + break; + } + } + + if (safe) { + props.unshift(t.property("init", t.identifier("displayName"), t.literal(id))); + } + }; + + exports.ExportDeclaration = function (node, parent, scope, file) { + if (node.default && react.isCreateClass(node.declaration)) { + addDisplayName(file.opts.basename, node.declaration); + } + }; + + exports.AssignmentExpression = + exports.Property = + exports.VariableDeclarator = function (node) { + var left, right; + + if (t.isAssignmentExpression(node)) { + left = node.left; + right = node.right; + } else if (t.isProperty(node)) { + left = node.key; + right = node.value; + } else if (t.isVariableDeclarator(node)) { + left = node.id; + right = node.init; + } + + if (t.isMemberExpression(left)) { + left = left.property; + } + + if (t.isIdentifier(left) && react.isCreateClass(right)) { + addDisplayName(left.name, right); + } + }; +}; diff --git a/lib/6to5/transformation/helpers/react.js b/lib/6to5/transformation/helpers/react.js index 6c0f2ad99e..663ab51095 100644 --- a/lib/6to5/transformation/helpers/react.js +++ b/lib/6to5/transformation/helpers/react.js @@ -20,3 +20,7 @@ exports.isCreateClass = function (node) { }; exports.isReactComponent = t.buildMatchMemberExpression("React.Component"); + +exports.isCompatTag = function (tagName) { + return tagName && /^[a-z]|\-/.test(tagName); +}; diff --git a/lib/6to5/transformation/transformers/index.js b/lib/6to5/transformation/transformers/index.js index f778ae0c8e..106297f7c3 100644 --- a/lib/6to5/transformation/transformers/index.js +++ b/lib/6to5/transformation/transformers/index.js @@ -11,6 +11,7 @@ module.exports = { "playground.memoizationOperator": require("./playground/memoization-operator"), "playground.objectGetterMemoization": require("./playground/object-getter-memoization"), + reactCompat: require("./other/react-compat"), react: require("./other/react"), _modules: require("./internal/modules"), diff --git a/lib/6to5/transformation/transformers/other/react-compat.js b/lib/6to5/transformation/transformers/other/react-compat.js new file mode 100644 index 0000000000..b8771eae35 --- /dev/null +++ b/lib/6to5/transformation/transformers/other/react-compat.js @@ -0,0 +1,29 @@ +"use strict"; + +var react = require("../../helpers/react"); +var t = require("../../../types"); + +exports.manipulateOptions = function (opts) { + opts.blacklist.push("react"); +}; + +exports.optional = true; + +require("../../helpers/build-react-transformer")(exports, { + pre: function (state) { + state.callee = state.tagExpr; + }, + + post: function (state) { + if (react.isCompatTag(state.tagName)) { + state.call = t.callExpression( + t.memberExpression( + t.memberExpression(t.identifier("React"), t.identifier("DOM")), + state.tagExpr, + t.isLiteral(state.tagExpr) + ), + state.args + ); + } + } +}); diff --git a/lib/6to5/transformation/transformers/other/react.js b/lib/6to5/transformation/transformers/other/react.js index e20a4fc85d..fcfc7168b7 100644 --- a/lib/6to5/transformation/transformers/other/react.js +++ b/lib/6to5/transformation/transformers/other/react.js @@ -1,291 +1,20 @@ "use strict"; -// Based upon the excellent jsx-transpiler by Ingvar Stepanyan (RReverser) -// https://github.com/RReverser/jsx-transpiler +var react = require("../../helpers/react"); +var t = require("../../../types"); -// jsx - -var isString = require("lodash/lang/isString"); -var esutils = require("esutils"); -var react = require("../../helpers/react"); -var t = require("../../../types"); - -exports.check = function (node) { - if (t.isJSX(node)) return true; - if (react.isCreateClass(node)) return true; - return false; -}; - -exports.JSXIdentifier = function (node, parent) { - if (node.name === "this" && t.isReferenced(node, parent)) { - return t.thisExpression(); - } else if (esutils.keyword.isIdentifierName(node.name)) { - node.type = "Identifier"; - } else { - return t.literal(node.name); - } -}; - -exports.JSXNamespacedName = function (node, parent, scope, file) { - throw file.errorWithNode(node, "Namespace tags are not supported. ReactJSX is not XML."); -}; - -exports.JSXMemberExpression = { - exit: function (node) { - node.computed = t.isLiteral(node.property); - node.type = "MemberExpression"; - } -}; - -exports.JSXExpressionContainer = function (node) { - return node.expression; -}; - -exports.JSXAttribute = { - exit: function (node) { - var value = node.value || t.literal(true); - return t.inherits(t.property("init", node.name, value), node); - } -}; - -var isCompatTag = function (tagName) { - return /^[a-z]|\-/.test(tagName); -}; - -exports.JSXOpeningElement = { - exit: function (node, parent, scope, file) { - var reactCompat = file.opts.reactCompat; - var tagExpr = node.name; - var args = []; - - var tagName; - if (t.isIdentifier(tagExpr)) { - tagName = tagExpr.name; - } else if (t.isLiteral(tagExpr)) { - tagName = tagExpr.value; - } - - if (!reactCompat) { - if (tagName && isCompatTag(tagName)) { - args.push(t.literal(tagName)); - } else { - args.push(tagExpr); - } - } - - var attribs = node.attributes; - if (attribs.length) { - attribs = buildJSXOpeningElementAttributes(attribs, file); +require("../../helpers/build-react-transformer")(exports, { + pre: function (state) { + var tagName = state.tagName; + var args = state.args; + if (react.isCompatTag(tagName)) { + args.push(t.literal(tagName)); } else { - attribs = t.literal(null); + args.push(state.tagExpr); } + }, - args.push(attribs); - - if (reactCompat) { - if (tagName && isCompatTag(tagName)) { - return t.callExpression( - t.memberExpression( - t.memberExpression(t.identifier("React"), t.identifier("DOM")), - tagExpr, - t.isLiteral(tagExpr) - ), - args - ); - } - } else { - tagExpr = t.memberExpression(t.identifier("React"), t.identifier("createElement")); - } - - return t.callExpression(tagExpr, args); + post: function (state) { + state.callee = t.memberExpression(t.identifier("React"), t.identifier("createElement")); } -}; - -/** - * The logic for this is quite terse. It's because we need to - * support spread elements. We loop over all attributes, - * breaking on spreads, we then push a new object containg - * all prior attributes to an array for later processing. - */ - -var buildJSXOpeningElementAttributes = function (attribs, file) { - var _props = []; - var objs = []; - - var pushProps = function () { - if (!_props.length) return; - - objs.push(t.objectExpression(_props)); - _props = []; - }; - - while (attribs.length) { - var prop = attribs.shift(); - if (t.isJSXSpreadAttribute(prop)) { - pushProps(); - objs.push(prop.argument); - } else { - _props.push(prop); - } - } - - pushProps(); - - if (objs.length === 1) { - // only one object - attribs = objs[0]; - } else { - // looks like we have multiple objects - if (!t.isObjectExpression(objs[0])) { - objs.unshift(t.objectExpression([])); - } - - // spread it - attribs = t.callExpression( - file.addHelper("extends"), - objs - ); - } - - return attribs; -}; - -exports.JSXElement = { - exit: function (node) { - var callExpr = node.openingElement; - - for (var i = 0; i < node.children.length; i++) { - var child = node.children[i]; - - if (t.isLiteral(child) && typeof child.value === "string") { - cleanJSXElementLiteralChild(child, callExpr.arguments); - continue; - } else if (t.isJSXEmptyExpression(child)) { - continue; - } - - callExpr.arguments.push(child); - } - - callExpr.arguments = flatten(callExpr.arguments); - - if (callExpr.arguments.length >= 3) { - callExpr._prettyCall = true; - } - - return t.inherits(callExpr, node); - } -}; - -var isStringLiteral = function (node) { - return t.isLiteral(node) && isString(node.value); -}; - -var flatten = function (args) { - var flattened = []; - var last; - - for (var i = 0; i < args.length; i++) { - var arg = args[i]; - if (isStringLiteral(arg) && isStringLiteral(last)) { - last.value += arg.value; - } else { - last = arg; - flattened.push(arg); - } - } - - return flattened; -}; - -var cleanJSXElementLiteralChild = function (child, args) { - var lines = child.value.split(/\r\n|\n|\r/); - - var lastNonEmptyLine = 0; - var i; - - for (i = 0; i < lines.length; i++) { - if (lines[i].match(/[^ \t]/)) { - lastNonEmptyLine = i; - } - } - - for (i = 0; i < lines.length; i++) { - var line = lines[i]; - - var isFirstLine = i === 0; - var isLastLine = i === lines.length - 1; - var isLastNonEmptyLine = i === lastNonEmptyLine; - - // replace rendered whitespace tabs with spaces - var trimmedLine = line.replace(/\t/g, " "); - - // trim whitespace touching a newline - if (!isFirstLine) { - trimmedLine = trimmedLine.replace(/^[ ]+/, ""); - } - - // trim whitespace touching an endline - if (!isLastLine) { - trimmedLine = trimmedLine.replace(/[ ]+$/, ""); - } - - if (trimmedLine) { - if (!isLastNonEmptyLine) { - trimmedLine += " "; - } - - args.push(t.literal(trimmedLine)); - } - } -}; - -// display names - -var addDisplayName = function (id, call) { - var props = call.arguments[0].properties; - var safe = true; - - for (var i = 0; i < props.length; i++) { - var prop = props[i]; - if (t.isIdentifier(prop.key, { name: "displayName" })) { - safe = false; - break; - } - } - - if (safe) { - props.unshift(t.property("init", t.identifier("displayName"), t.literal(id))); - } -}; - -exports.ExportDeclaration = function (node, parent, scope, file) { - if (node.default && react.isCreateClass(node.declaration)) { - addDisplayName(file.opts.basename, node.declaration); - } -}; - -exports.AssignmentExpression = -exports.Property = -exports.VariableDeclarator = function (node) { - var left, right; - - if (t.isAssignmentExpression(node)) { - left = node.left; - right = node.right; - } else if (t.isProperty(node)) { - left = node.key; - right = node.value; - } else if (t.isVariableDeclarator(node)) { - left = node.id; - right = node.init; - } - - if (t.isMemberExpression(left)) { - left = left.property; - } - - if (t.isIdentifier(left) && react.isCreateClass(right)) { - addDisplayName(left.name, right); - } -}; +}); diff --git a/test/fixtures/transformation/react/compat-convert-component/actual.js b/test/fixtures/transformation/react-compat/convert-component/actual.js similarity index 100% rename from test/fixtures/transformation/react/compat-convert-component/actual.js rename to test/fixtures/transformation/react-compat/convert-component/actual.js diff --git a/test/fixtures/transformation/react/compat-convert-component/expected.js b/test/fixtures/transformation/react-compat/convert-component/expected.js similarity index 100% rename from test/fixtures/transformation/react/compat-convert-component/expected.js rename to test/fixtures/transformation/react-compat/convert-component/expected.js diff --git a/test/fixtures/transformation/react/compat-convert-tags/actual.js b/test/fixtures/transformation/react-compat/convert-tags/actual.js similarity index 100% rename from test/fixtures/transformation/react/compat-convert-tags/actual.js rename to test/fixtures/transformation/react-compat/convert-tags/actual.js diff --git a/test/fixtures/transformation/react/compat-convert-tags/expected.js b/test/fixtures/transformation/react-compat/convert-tags/expected.js similarity index 100% rename from test/fixtures/transformation/react/compat-convert-tags/expected.js rename to test/fixtures/transformation/react-compat/convert-tags/expected.js diff --git a/test/fixtures/transformation/react-compat/options.json b/test/fixtures/transformation/react-compat/options.json new file mode 100644 index 0000000000..061424d050 --- /dev/null +++ b/test/fixtures/transformation/react-compat/options.json @@ -0,0 +1,4 @@ +{ + "blacklist": "useStrict", + "optional": "reactCompat" +} diff --git a/test/fixtures/transformation/react/compat-convert-component/options.json b/test/fixtures/transformation/react/compat-convert-component/options.json deleted file mode 100644 index 1f358613e7..0000000000 --- a/test/fixtures/transformation/react/compat-convert-component/options.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "reactCompat": true -} diff --git a/test/fixtures/transformation/react/compat-convert-tags/options.json b/test/fixtures/transformation/react/compat-convert-tags/options.json deleted file mode 100644 index 1f358613e7..0000000000 --- a/test/fixtures/transformation/react/compat-convert-tags/options.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "reactCompat": true -}