From d6652ee361a8b4ab1e09710b1db1d3886e8e02c0 Mon Sep 17 00:00:00 2001 From: geowarin Date: Tue, 8 Mar 2016 16:54:33 +0100 Subject: [PATCH] Inline source maps We do this by monkey patching the jsx visitor to add instrumentation --- compiler.js | 2 +- package.json | 4 +- src/instrument.js | 96 ------- src/jsxTransform.js | 36 ++- src/nodeTypes.js | 48 ---- src/transforms/custom-require-visitor.js | 34 +++ src/transforms/react-jsx-visitors.js | 250 ++++++++++++++++++ src/transforms/top-level-render-visitor.js | 28 ++ test/fixtures/simple_transform_input.jsx | 2 +- test/fixtures/simple_transform_result.jsx | 2 +- .../transform_and_instrument_input.jsx | 2 +- .../transform_and_instrument_result.jsx | 7 +- ..._and_instrument_result_with_source_map.jsx | 9 +- ...nsform_and_instrument_top_level_result.jsx | 3 +- test/spec/jsxTransform.spec.js | 44 ++- 15 files changed, 385 insertions(+), 182 deletions(-) delete mode 100644 src/instrument.js delete mode 100644 src/nodeTypes.js create mode 100644 src/transforms/custom-require-visitor.js create mode 100644 src/transforms/react-jsx-visitors.js create mode 100644 src/transforms/top-level-render-visitor.js diff --git a/compiler.js b/compiler.js index 6ee44a7..63d9173 100644 --- a/compiler.js +++ b/compiler.js @@ -3,4 +3,4 @@ // Can be used as a mocha compiler // mocha --compilers electron-hot/compiler.js // This will not instrument the files and hot reload will be disabled -require('./src/jsxTransform').install({doNotInstrument: true}); \ No newline at end of file +require('./src/jsxTransform').install({doNotInstrument: true, sourceMapInline: false}); \ No newline at end of file diff --git a/package.json b/package.json index 3cdb2b0..7f0dd4a 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,7 @@ }, "homepage": "https://github.com/geowarin/electron-hot-loader#readme", "dependencies": { - "escodegen": "1.8.0", - "esprima": "2.7.2", - "estraverse": "4.1.1", + "esprima-fb": "^15001.1.0-dev-harmony-fb", "jstransform": "11.0.3", "react-deep-force-update": "2.0.1", "react-proxy": "2.0.3", diff --git a/src/instrument.js b/src/instrument.js deleted file mode 100644 index 3f3e1a3..0000000 --- a/src/instrument.js +++ /dev/null @@ -1,96 +0,0 @@ -"use strict"; - -const esprima = require('esprima'); -const estraverse = require('estraverse'); -const escodegen = require('escodegen'); -var t = require('./nodeTypes'); -var requireRegister = require.resolve('./index'); - -module.exports = function instrument(source) { - var ast = esprima.parse(source, {attachComment: true}); - - var firstRequireDeclarationIndex; - var requireDeclarationsByName = {}; - - estraverse.traverse(ast, { - enter: function (node, parent) { - if (t.isRequireDeclaration(node)) { - if (!firstRequireDeclarationIndex) { - firstRequireDeclarationIndex = parent.body.indexOf(node); - } - requireDeclarationsByName[node.declarations[0].id.name] = node.declarations[0].init.arguments[0].value - } - } - }); - - estraverse.replace(ast, { - enter: function (node) { - if (t.isTopLevelAPIRender(node)) { - var rootWrapperTemplate = esprima.parse( - '__electronHot__.registerRoot(ARGS)' - ); - - var retRootNode = rootWrapperTemplate.body[0].expression; - retRootNode.arguments[0] = node; - this.skip(); - return retRootNode; - } - } - }); - - estraverse.replace(ast, { - enter: function (node) { - - if (t.isCreateElementCall(node)) { - - var wrapperTemplate = esprima.parse( - [ - 'React.createElement(' + - ' __electronHot__.register(COMPONENT_ARG, RESOLVE_ARG))' - ].join('') - ); - - var retNode = wrapperTemplate.body[0].expression; - - const componentArg = node.arguments[0]; - const requireDeclaration = requireDeclarationsByName[componentArg.name]; - if (!requireDeclaration) { - console.warn("Could not find a require declaration for the component " + componentArg.name); - return node; - } - - // change COMPONENT_ARG - retNode.arguments[0].arguments[0] = componentArg; - // RESOLVE_ARG - retNode.arguments[0].arguments[1] = esprima.parse("require.resolve('" + requireDeclaration + "')").body[0].expression; - - - // props - if (node.arguments.length > 1) { - retNode.arguments[1] = node.arguments[1]; - } - // children - if (node.arguments.length > 2) { - retNode.arguments[2] = node.arguments[2]; - } - - //Prevent further traversal and ComponentWrapper wrapping - this.skip(); - return retNode; - } - }, - - leave: function (node) { - - if (node.type === 'Program') { - - var beforeChunk = esprima.parse('var __electronHot__ = require("' + requireRegister + '");'); - - node.body.splice(firstRequireDeclarationIndex, 0, beforeChunk); - return node; - } - } - }); - - return escodegen.generate(ast, {comment: true}); -}; \ No newline at end of file diff --git a/src/jsxTransform.js b/src/jsxTransform.js index 7e54c7f..2699b76 100644 --- a/src/jsxTransform.js +++ b/src/jsxTransform.js @@ -1,22 +1,36 @@ "use strict"; var installed = false; +var inlineSourceMap = require('jstransform/src/inline-source-map'); module.exports = { install, transform }; -function transform(source, options) { +function transform(filename, source, options) { - var jstransform = require('jstransform/simple'); - var content = jstransform.transform(source, options).code; - if (options.doNotInstrument === true) { - return content; + const jsxVisitors = require('./transforms/react-jsx-visitors').visitorList; + const requireVisitor = require('./transforms/custom-require-visitor'); + const topLevelVisitor = require('./transforms/top-level-render-visitor'); + const jstransform = require('jstransform'); + + let visitors = []; + if (options.doNotInstrument !== true) { + visitors = visitors.concat(requireVisitor).concat(topLevelVisitor); + } + visitors = visitors.concat(jsxVisitors); + + let result; + if (options.sourceMapInline) { + result = jstransform.transform(visitors, source, {sourceMap: true, filename: filename, doNotInstrument: options.doNotInstrument}); + var map = inlineSourceMap(result.sourceMap, source, filename); + result.code = result.code + '\n' + map; + } else { + result = jstransform.transform(visitors, source, {doNotInstrument: options.doNotInstrument}); } - var instrument = require('./instrument'); - return instrument(content); + return result.code; } function install(options) { @@ -31,19 +45,17 @@ function install(options) { options = options || {}; Module._extensions[options.extension || '.jsx'] = function(module, filename) { - if (!options.hasOwnProperty('react')) { - options.react = true - } if (!options.hasOwnProperty('sourceMapInline')) { options.sourceMapInline = true } var content = fs.readFileSync(filename, 'utf8'); try { - var instrumented = transform(content, options); + var instrumented = transform(filename, content, options); module._compile(instrumented, filename) } catch (e) { - console.error("Error compiling " + filename, e) + console.error("Error compiling " + filename, e); + console.error(e.stack); } }; diff --git a/src/nodeTypes.js b/src/nodeTypes.js deleted file mode 100644 index ebfc34d..0000000 --- a/src/nodeTypes.js +++ /dev/null @@ -1,48 +0,0 @@ -"use strict"; - -var t = exports; - -//Matches: var SomeComponent = React.createClass({}) -t.isComponentDeclaration = function(node) { - return ( - node.type === 'VariableDeclaration' && - node.declarations[0] && - node.declarations[0].type === 'VariableDeclarator' && - node.declarations[0].init && - node.declarations[0].init.type === 'CallExpression' && - node.declarations[0].init.callee.type === 'MemberExpression' && - node.declarations[0].init.callee.property.name === 'createClass' - ); -}; - -t.isRequireDeclaration = function(node) { - return ( - node.type === 'VariableDeclaration' && - node.declarations[0] && - node.declarations[0].type === 'VariableDeclarator' && - node.declarations[0].init && - node.declarations[0].init.type === 'CallExpression' && - node.declarations[0].init.callee.type === 'Identifier' && - node.declarations[0].init.callee.name === 'require' - ); -}; - -//Matches on React.createElement(SomeCustomELement, ......) -t.isCreateElementCall = function(node) { - return ( - node.type === 'CallExpression' && - node.callee.type === 'MemberExpression' && - node.callee.property.name === 'createElement' && - node.arguments[0].type === 'Identifier' - ); -}; - -//React.render() -t.isTopLevelAPIRender = function(node) { - return ( - node.type === 'CallExpression' && - node.callee.type === 'MemberExpression' && - (node.callee.object.name === 'React' || node.callee.object.name === 'ReactDOM') && - node.callee.property.name === 'render' - ); -}; \ No newline at end of file diff --git a/src/transforms/custom-require-visitor.js b/src/transforms/custom-require-visitor.js new file mode 100644 index 0000000..1bb8874 --- /dev/null +++ b/src/transforms/custom-require-visitor.js @@ -0,0 +1,34 @@ +var jstransform = require('jstransform'); +var utils = require('jstransform/src/utils'); +var requireRegister = require.resolve('../index'); + +function requireVisitor(traverse, node, path, state) { + + if (!state.g.alreadyAddedElectronHotRequire) { + utils.append("var __electronHot__ = require('" + requireRegister + "');\n", state); + state.g.alreadyAddedElectronHotRequire = true; + } + + var requireNodesMap = state.g.requireNodesMap; + if (!requireNodesMap) { + requireNodesMap = {}; + } + requireNodesMap[node.declarations[0].id.name] = node.declarations[0].init.arguments[0].value; + + state.g.requireNodesMap = requireNodesMap; + + utils.catchup(node.range[1], state); +} +requireVisitor.test = function(node, path, state) { + return ( + node.type === 'VariableDeclaration' && + node.declarations[0] && + node.declarations[0].type === 'VariableDeclarator' && + node.declarations[0].init && + node.declarations[0].init.type === 'CallExpression' && + node.declarations[0].init.callee.type === 'Identifier' && + node.declarations[0].init.callee.name === 'require' + ); +}; + +module.exports = requireVisitor; \ No newline at end of file diff --git a/src/transforms/react-jsx-visitors.js b/src/transforms/react-jsx-visitors.js new file mode 100644 index 0000000..df3b7a5 --- /dev/null +++ b/src/transforms/react-jsx-visitors.js @@ -0,0 +1,250 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +/*global exports:true*/ + +var Syntax = require('esprima-fb').Syntax; +var utils = require('jstransform/src/utils'); + +var jsxHelpers = require('jstransform/visitors/jsx-helpers'); + +var renderJSXExpressionContainer = jsxHelpers.renderJSXExpressionContainer; +var renderJSXLiteral = jsxHelpers.renderJSXLiteral; +var quoteAttrName = jsxHelpers.quoteAttrName; +var trimLeft = jsxHelpers.trimLeft; + +/** + * Customized desugar processor for React JSX. Currently: + * + * => React.createElement(X, null) + * => React.createElement(X, {prop: '1'}, null) + * => React.createElement(X, {prop:'2'}, + * React.createElement(Y, null) + * ) + *
=> React.createElement("div", null) + */ + +/** + * Removes all non-whitespace/parenthesis characters + */ +var reNonWhiteParen = /([^\s\(\)])/g; +function stripNonWhiteParen(value) { + return value.replace(reNonWhiteParen, ''); +} + +var tagConvention = /^[a-z]|\-/; +function isTagName(name) { + return tagConvention.test(name); +} + +function visitReactTag(traverse, object, path, state) { + var openingElement = object.openingElement; + var nameObject = openingElement.name; + var attributesObject = openingElement.attributes; + + utils.catchup(openingElement.range[0], state, trimLeft); + + if (nameObject.type === Syntax.JSXNamespacedName && nameObject.namespace) { + throw new Error('Namespace tags are not supported. ReactJSX is not XML.'); + } + + // We assume that the React runtime is already in scope + utils.append('React.createElement(', state); + + if (nameObject.type === Syntax.JSXIdentifier && isTagName(nameObject.name)) { + utils.append('"' + nameObject.name + '"', state); + utils.move(nameObject.range[1], state); + } else { + // Use utils.catchup in this case so we can easily handle + // JSXMemberExpressions which look like Foo.Bar.Baz. This also handles + // JSXIdentifiers that aren't fallback tags. + // GWA: monkey patch + if (state.g.opts.doNotInstrument !== true) { + utils.append('__electronHot__.register(', state); + } + utils.move(nameObject.range[0], state); + utils.catchup(nameObject.range[1], state); + // GWA: monkey patch + if (state.g.opts.doNotInstrument !== true) { + utils.append(", require.resolve('"+ state.g.requireNodesMap[nameObject.name] + "'))", state); + } + } + + utils.append(', ', state); + + var hasAttributes = attributesObject.length; + + var hasAtLeastOneSpreadProperty = attributesObject.some(function(attr) { + return attr.type === Syntax.JSXSpreadAttribute; + }); + + // if we don't have any attributes, pass in null + if (hasAtLeastOneSpreadProperty) { + utils.append('React.__spread({', state); + } else if (hasAttributes) { + utils.append('{', state); + } else { + utils.append('null', state); + } + + // keep track of if the previous attribute was a spread attribute + var previousWasSpread = false; + + // write attributes + attributesObject.forEach(function(attr, index) { + var isLast = index === attributesObject.length - 1; + + if (attr.type === Syntax.JSXSpreadAttribute) { + // Close the previous object or initial object + if (!previousWasSpread) { + utils.append('}, ', state); + } + + // Move to the expression start, ignoring everything except parenthesis + // and whitespace. + utils.catchup(attr.range[0], state, stripNonWhiteParen); + // Plus 1 to skip `{`. + utils.move(attr.range[0] + 1, state); + utils.catchup(attr.argument.range[0], state, stripNonWhiteParen); + + traverse(attr.argument, path, state); + + utils.catchup(attr.argument.range[1], state); + + // Move to the end, ignoring parenthesis and the closing `}` + utils.catchup(attr.range[1] - 1, state, stripNonWhiteParen); + + if (!isLast) { + utils.append(', ', state); + } + + utils.move(attr.range[1], state); + + previousWasSpread = true; + + return; + } + + // If the next attribute is a spread, we're effective last in this object + if (!isLast) { + isLast = attributesObject[index + 1].type === Syntax.JSXSpreadAttribute; + } + + if (attr.name.namespace) { + throw new Error( + 'Namespace attributes are not supported. ReactJSX is not XML.'); + } + var name = attr.name.name; + + utils.catchup(attr.range[0], state, trimLeft); + + if (previousWasSpread) { + utils.append('{', state); + } + + utils.append(quoteAttrName(name), state); + utils.append(': ', state); + + if (!attr.value) { + state.g.buffer += 'true'; + state.g.position = attr.name.range[1]; + if (!isLast) { + utils.append(', ', state); + } + } else { + utils.move(attr.name.range[1], state); + // Use catchupNewlines to skip over the '=' in the attribute + utils.catchupNewlines(attr.value.range[0], state); + if (attr.value.type === Syntax.Literal) { + renderJSXLiteral(attr.value, isLast, state); + } else { + renderJSXExpressionContainer(traverse, attr.value, isLast, path, state); + } + } + + utils.catchup(attr.range[1], state, trimLeft); + + previousWasSpread = false; + }); + + if (!openingElement.selfClosing) { + utils.catchup(openingElement.range[1] - 1, state, trimLeft); + utils.move(openingElement.range[1], state); + } + + if (hasAttributes && !previousWasSpread) { + utils.append('}', state); + } + + if (hasAtLeastOneSpreadProperty) { + utils.append(')', state); + } + + // filter out whitespace + var childrenToRender = object.children.filter(function(child) { + return !(child.type === Syntax.Literal + && typeof child.value === 'string' + && child.value.match(/^[ \t]*[\r\n][ \t\r\n]*$/)); + }); + if (childrenToRender.length > 0) { + var lastRenderableIndex; + + childrenToRender.forEach(function(child, index) { + if (child.type !== Syntax.JSXExpressionContainer || + child.expression.type !== Syntax.JSXEmptyExpression) { + lastRenderableIndex = index; + } + }); + + if (lastRenderableIndex !== undefined) { + utils.append(', ', state); + } + + childrenToRender.forEach(function(child, index) { + utils.catchup(child.range[0], state, trimLeft); + + var isLast = index >= lastRenderableIndex; + + if (child.type === Syntax.Literal) { + renderJSXLiteral(child, isLast, state); + } else if (child.type === Syntax.JSXExpressionContainer) { + renderJSXExpressionContainer(traverse, child, isLast, path, state); + } else { + traverse(child, path, state); + if (!isLast) { + utils.append(', ', state); + } + } + + utils.catchup(child.range[1], state, trimLeft); + }); + } + + if (openingElement.selfClosing) { + // everything up to /> + utils.catchup(openingElement.range[1] - 2, state, trimLeft); + utils.move(openingElement.range[1], state); + } else { + // everything up to + utils.catchup(object.closingElement.range[0], state, trimLeft); + utils.move(object.closingElement.range[1], state); + } + + utils.append(')', state); + return false; +} + +visitReactTag.test = function(object, path, state) { + return object.type === Syntax.JSXElement; +}; + +exports.visitorList = [ + visitReactTag +]; diff --git a/src/transforms/top-level-render-visitor.js b/src/transforms/top-level-render-visitor.js new file mode 100644 index 0000000..4df6f52 --- /dev/null +++ b/src/transforms/top-level-render-visitor.js @@ -0,0 +1,28 @@ +var jstransform = require('jstransform'); +var utils = require('jstransform/src/utils'); + +function topLevelRenderVisitor(traverse, node, path, state) { + utils.append("__electronHot__.registerRoot(", state); + + const beginOfArgs = node.arguments[0].range[0]; + const endOfArgs = node.arguments[node.arguments.length - 1].range[0]; + + // Write ReactDOM.render( + utils.catchup(beginOfArgs, state); + traverse(node.arguments, path, state); + utils.catchup(endOfArgs, state); + + utils.append(")", state); + return false; +} + +topLevelRenderVisitor.test = function (node, path, state) { + return ( + node.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + (node.callee.object.name === 'React' || node.callee.object.name === 'ReactDOM') && + node.callee.property.name === 'render' + ); +}; + +module.exports = topLevelRenderVisitor; \ No newline at end of file diff --git a/test/fixtures/simple_transform_input.jsx b/test/fixtures/simple_transform_input.jsx index 9a37ab4..5318e85 100644 --- a/test/fixtures/simple_transform_input.jsx +++ b/test/fixtures/simple_transform_input.jsx @@ -2,6 +2,6 @@ const React = require('react'); module.exports = class Component extends React.Component { render() { - return
Some text
+ return
Some text
} }; diff --git a/test/fixtures/simple_transform_result.jsx b/test/fixtures/simple_transform_result.jsx index ff0a327..8d80e2d 100644 --- a/test/fixtures/simple_transform_result.jsx +++ b/test/fixtures/simple_transform_result.jsx @@ -2,6 +2,6 @@ const React = require('react'); module.exports = class Component extends React.Component { render() { - return React.createElement("div", null, "Some text") + return React.createElement("div", null, "Some text") } }; diff --git a/test/fixtures/transform_and_instrument_input.jsx b/test/fixtures/transform_and_instrument_input.jsx index db2ca7e..78d759e 100644 --- a/test/fixtures/transform_and_instrument_input.jsx +++ b/test/fixtures/transform_and_instrument_input.jsx @@ -3,6 +3,6 @@ const OtherComponent = require('./OtherComponent.jsx'); module.exports = class Component extends React.Component { render() { - return + return } }; diff --git a/test/fixtures/transform_and_instrument_result.jsx b/test/fixtures/transform_and_instrument_result.jsx index a2d0ba8..8d559d3 100644 --- a/test/fixtures/transform_and_instrument_result.jsx +++ b/test/fixtures/transform_and_instrument_result.jsx @@ -1,8 +1,9 @@ -const React = require('react'); var __electronHot__ = require('_electronHotLocation_'); +const React = require('react'); const OtherComponent = require('./OtherComponent.jsx'); + module.exports = class Component extends React.Component { render() { - return React.createElement(__electronHot__.register(OtherComponent, require.resolve('./OtherComponent.jsx')), null); + return React.createElement(__electronHot__.register(OtherComponent, require.resolve('./OtherComponent.jsx')), null) } -}; \ No newline at end of file +}; diff --git a/test/fixtures/transform_and_instrument_result_with_source_map.jsx b/test/fixtures/transform_and_instrument_result_with_source_map.jsx index 23d5525..34ef324 100644 --- a/test/fixtures/transform_and_instrument_result_with_source_map.jsx +++ b/test/fixtures/transform_and_instrument_result_with_source_map.jsx @@ -1,8 +1,11 @@ -const React = require('react'); var __electronHot__ = require('_electronHotLocation_'); +const React = require('react'); const OtherComponent = require('./OtherComponent.jsx'); + module.exports = class Component extends React.Component { render() { - return React.createElement(__electronHot__.register(OtherComponent, require.resolve('./OtherComponent.jsx')), null); + return React.createElement(__electronHot__.register(OtherComponent, require.resolve('./OtherComponent.jsx')), null) } -}; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNvdXJjZS5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7QUFDL0IsTUFBTSxjQUFjLEdBQUcsT0FBTyxDQUFDLHNCQUFzQixDQUFDLENBQUM7O0FBRXZELE1BQU0sQ0FBQyxPQUFPLEdBQUcsTUFBTSxTQUFTLFNBQVMsS0FBSyxDQUFDLFNBQVMsQ0FBQztJQUNyRCxNQUFNLEdBQUc7S0FDUixPQUFPLG9CQUFDLGNBQWMsRUFBQSxJQUFBLENBQUcsQ0FBQTtLQUN6QjtDQUNKLENBQUMiLCJmaWxlIjoic291cmNlLmpzIiwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgUmVhY3QgPSByZXF1aXJlKCdyZWFjdCcpO1xuY29uc3QgT3RoZXJDb21wb25lbnQgPSByZXF1aXJlKCcuL090aGVyQ29tcG9uZW50LmpzeCcpO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGNsYXNzIENvbXBvbmVudCBleHRlbmRzIFJlYWN0LkNvbXBvbmVudCB7XG4gICAgcmVuZGVyKCkge1xuXHQgICAgcmV0dXJuIDxPdGhlckNvbXBvbmVudCAvPlxuICAgIH1cbn07XG4iXX0= +}; + +//# sourceMappingURL=data:application/json;base64,_ignore_ \ No newline at end of file diff --git a/test/fixtures/transform_and_instrument_top_level_result.jsx b/test/fixtures/transform_and_instrument_top_level_result.jsx index a2d069a..ffc2ba2 100644 --- a/test/fixtures/transform_and_instrument_top_level_result.jsx +++ b/test/fixtures/transform_and_instrument_top_level_result.jsx @@ -1,5 +1,6 @@ -const React = require('react'); var __electronHot__ = require('_electronHotLocation_'); +const React = require('react'); const ReactDOM = require('react-dom'); const App = require('./ui/App.jsx'); + __electronHot__.registerRoot(ReactDOM.render(React.createElement(__electronHot__.register(App, require.resolve('./ui/App.jsx')), null), document.getElementById('root'))); \ No newline at end of file diff --git a/test/spec/jsxTransform.spec.js b/test/spec/jsxTransform.spec.js index 9f07d8b..1329462 100644 --- a/test/spec/jsxTransform.spec.js +++ b/test/spec/jsxTransform.spec.js @@ -5,19 +5,11 @@ const jsxTransform = require('../../src/jsxTransform'); const fs = require('fs'); -function expectTransformation(options, fileToTransform, actualFile) { - const input = fs.readFileSync(fileToTransform).toString(); - const transformed = jsxTransform.transform(input, options); - let expected = fs.readFileSync(actualFile).toString(); - expected = expected.replace(/_electronHotLocation_/m, require.resolve('../../src/')); - expect(transformed).toEqual(expected); -} - describe('jsxTransform', () => { it('should transform a JSX file without instrumenting it', () => { expectTransformation( - {doNotInstrument: true, react: true}, + {doNotInstrument: true}, './test/fixtures/simple_transform_input.jsx', './test/fixtures/simple_transform_result.jsx' ) @@ -25,7 +17,7 @@ describe('jsxTransform', () => { it('should transform a JSX file and instrument it', () => { expectTransformation( - {react: true}, + {}, './test/fixtures/transform_and_instrument_input.jsx', './test/fixtures/transform_and_instrument_result.jsx' ) @@ -33,7 +25,7 @@ describe('jsxTransform', () => { it('should transform a JSX file, instrument it and keep the source map', () => { expectTransformation( - {react: true, sourceMapInline: true}, + {sourceMapInline: true}, './test/fixtures/transform_and_instrument_input.jsx', './test/fixtures/transform_and_instrument_result_with_source_map.jsx' ) @@ -41,9 +33,37 @@ describe('jsxTransform', () => { it('should transform and instrument React top level render', () => { expectTransformation( - {react: true}, + {}, './test/fixtures/transform_and_instrument_top_level_input.jsx', './test/fixtures/transform_and_instrument_top_level_result.jsx' ) }); }); + +// When the token _ignore_ is found in the expected output, +// we replace it in the actual output until a new line is found +// Used to avoid comparing base64 source maps in tests +function replaceIgnored(expected, transformed) { + const ignoreRegexp = /_ignore_/g; + let match; + while (match = ignoreRegexp.exec(expected)) { + let until = transformed.indexOf('\n', match.index); + if (until === -1) { + until = transformed.length; + } + transformed = transformed.substring(0, match.index) + match[0] + transformed.substring(until); + } + return transformed; +} + +function expectTransformation(options, fileToTransform, actualFile) { + const input = fs.readFileSync(fileToTransform).toString(); + let transformed = jsxTransform.transform(fileToTransform, input, options); + + let expected = fs.readFileSync(actualFile).toString(); + + expected = expected.replace(/_electronHotLocation_/m, require.resolve('../../src/')); + transformed = replaceIgnored(expected, transformed); + + expect(transformed).toEqual(expected); +} \ No newline at end of file