From cb096f7bd6efd78b4ffc7fcb00878c26f9219093 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 3 Sep 2015 02:26:51 +0300 Subject: [PATCH] Initial commit --- .gitignore | 4 + .npmignore | 3 + README.md | 180 ++++++++++++++++++ package.json | 35 ++++ src/index.js | 293 ++++++++++++++++++++++++++++++ test/fixtures/classic/actual.js | 87 +++++++++ test/fixtures/classic/expected.js | 169 +++++++++++++++++ test/fixtures/modern/actual.js | 21 +++ test/fixtures/modern/expected.js | 145 +++++++++++++++ test/fixtures/vanilla/actual.js | 7 + test/fixtures/vanilla/expected.js | 22 +++ test/index.js | 36 ++++ 12 files changed, 1002 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 README.md create mode 100644 package.json create mode 100644 src/index.js create mode 100644 test/fixtures/classic/actual.js create mode 100644 test/fixtures/classic/expected.js create mode 100644 test/fixtures/modern/actual.js create mode 100644 test/fixtures/modern/expected.js create mode 100644 test/fixtures/vanilla/actual.js create mode 100644 test/fixtures/vanilla/expected.js create mode 100644 test/index.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11891ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +lib +*.log +DS_Store diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..89fa922 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +src +*.log +DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..84dc415 --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ +# babel-plugin-react-transform + +This Babel plugin wraps all React components into arbitrary transforms written by the community. +In other words, **it lets you instrument React components** in any custom way. + +Such transforms can do a variety of things: + +* catch errors inside `render()` and render them in a [red screen of death](https://github.com/KeywordBrain/redbox-react); +* enable hot reloading a la [React Hot Loader](https://github.com/gaearon/react-hot-loader); +* render an inline prop inspector a la [React DevTools](https://github.com/facebook/react-devtools); +* highlight parts of the screen when components update, +* etc. + +The limit is your imagination and the time you feel compelled to spend on writing these transforms. +Time will show whether it is an amazing, or a terrible idea. + +## Installation + +First, install the plugin: + +``` +npm install --save-dev babel-plugin-react-transform +``` + +Then, install the transforms you’re interested in: + +``` +# Okay, these don't actually exist yet but I'll publish them soon +npm install --save-dev react-transform-webpack-hmr +npm install --save-dev react-transform-catch-errors +``` + +Then edit your `.babelrc` to include `extra.babel-plugin-react-transform`. +It must be an array of the transforms you want to use: + +```js +{ + "stage": 0, + "plugins": [ + "babel-plugin-react-transform" + ], + "extra": { + // must be defined and be an array + "babel-plugin-react-transform": [{ + // can be an NPM module name or a local path + "target": "react-transform-webpack-hmr", + // will be available as options.imports to the transform + "imports": ["react"], + // will be available as options.locals to the transform + "locals": ["module"] + }, { + // can be an NPM module name or a local path + "target": "react-transform-catch-errors", + // will be available as options.imports to the transform + "imports": ["react", "redbox-react"] + }, { + // can be an NPM module name or a local path + "target": "./src/my-custom-transform" + }] + } +} +``` + +As you can see each transform, apart from the `target` field where you write it name, also has `imports` and `locals` fields. You should consult the docs of each individual transform to learn which `imports` and `locals` it might need, and how it uses them. You probably already guessed that this is just a way to inject local variables (like `module`) or dependencies (like `react`) into the transforms that need them. + +## Writing a Transform + +It’s not hard to write a custom transform! First, make sure you call your NPM package `react-transform-*` so we have uniform naming across the transforms. The only thing you should export from your transform module is a function. + +```js +export default function myTransform() { + // ¯\_(ツ)_/¯ +} +``` + +This function should *return another function*: + +```js +export default function myTransform() { + return function wrap(ReactClass) { + // ¯\_(ツ)_/¯ + return ReactClass; + } +} +``` + +As you can see, you’ll receive `ReactClass` as a parameter. It’s up to you to do something with it: monkeypatch its methods, create another component with the same prototype and a few different methods, wrap it into a higher-order component, etc. Be creative! + +```js +export default function logAllUpdates() { + return function wrap(ReactClass) { + const displayName = // ¯\_(ツ)_/¯ + const originalComponentDidUpdate = ReactClass.prototype.componentDidUpdate; + + ReactClass.prototype.componentDidUpdate = function componentDidUpdate() { + console.info(`${displayName} updated:`, this.props, this.state); + + if (originalComponentDidUpdate) { + originalComponentDidUpdate.apply(this, arguments); + } + } + + return ReactClass; + } +} +``` + +Oh, how do I get `displayName`? +Actually, we give your transformation function a single argument called `options`. Yes, `options`: + +```js +export default function logAllUpdates(options) { +``` + +It contains some useful data. For example, your `options` could look like this: + +```js +{ + // the file being processed + filename: '/Users/dan/p/my-projects/src/App.js', + // remember that "imports" .babelrc option? + imports: [React], + // remember that "locals" .babelrc option? + locals: [module], + // all components declared in the current file + components: { + $_MyComponent: { + // with their displayName when available + displayName: 'MyComponent' + }, + $_SomeOtherComponent: { + displayName: 'SomeOtherComponent', + // and telling whether they are defined inside a function + isInFunction: true + } + } +} +``` + +Of course, you might not want to use *all* options, but isn’t it nice to know that you have access to them in the top scope—which means before the component definitions actually run? (Hint: a hot reloading plugin might use this to decide whether a module is worthy of reloading, even if it contains an error and no React components have yet been wrapped because of it.) + +So, to retrieve the `displayName` (or `isInFunction`, when available), use the `options` parameter *and* the second `uniqueId` parameter given to the inner function after `ReactClass`: + +```js +export default function logAllUpdates(options) { + return function wrap(ReactClass, uniqueId) { + const displayName = options.components[uniqueId].displayName || ''; +``` + +This is it! + +Sure, it’s a slightly contrived example, as you can grab `ReactClass.displayName` just fine, but it illustrates a point: you have information about all of the components inside a file before that file executes, which is *very* handy for some transformations. + +Here is the complete code for this example transformation function: + +```js +export default function logAllUpdates(options) { + return function wrap(ReactClass, uniqueId) { + const displayName = options.components[uniqueId].displayName || ''; + const originalComponentDidUpdate = ReactClass.prototype.componentDidUpdate; + + ReactClass.prototype.componentDidUpdate = function componentDidUpdate() { + console.info(`${displayName} updated:`, this.props, this.state); + + if (originalComponentDidUpdate) { + originalComponentDidUpdate.apply(this, arguments); + } + } + + return ReactClass; + } +} +``` + +Now go ahead and write your own! +Don’t forget to tag it with `react-transform` keyword on npm. + +## License + +MIT diff --git a/package.json b/package.json new file mode 100644 index 0000000..c0b1094 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "babel-plugin-react-transform", + "version": "1.0.0", + "description": "Babel plugin to instrument React components with custom transforms", + "main": "lib/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/gaearon/babel-plugin-react-transform.git" + }, + "author": "Dan Abramov ", + "license": "MIT", + "bugs": { + "url": "https://github.com/gaearon/babel-plugin-react-transform/issues" + }, + "homepage": "https://github.com/gaearon/babel-plugin-react-transform#readme", + "devDependencies": { + "babel": "^5.8.23", + "mocha": "^2.2.5" + }, + "scripts": { + "build": "babel-plugin build", + "test": "mocha --compilers js:babel/register", + "test:watch": "npm run test -- --watch", + "prepublish": "npm run build" + }, + "keywords": [ + "babel-plugin", + "react-transform", + "instrumentation", + "dx", + "react", + "reactjs", + "components" + ] +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..c3062d2 --- /dev/null +++ b/src/index.js @@ -0,0 +1,293 @@ +import path from 'path'; + +export default function ({ Plugin, types: t }) { + const parentDir = path.resolve(path.join(__dirname, '..', '..')); + function resolvePathConservatively(specifiedPath, filePath) { + if (specifiedPath[0] === '.') { + throw new Error( + `Relative path like ${specifiedPath} is only allowed if ` + + `babel-plugin-wrap-react-components is inside a node_modules folder.` + ); + } + return specifiedPath; + } + function resolvePathAssumingWeAreInNodeModules(specifiedPath, filePath) { + if (specifiedPath[0] === '.') { + return '.' + path.sep + path.relative( + path.dirname(filePath), + path.resolve(path.join(parentDir, '..', specifiedPath)) + ); + } + return specifiedPath; + } + const resolvePath = path.basename(parentDir) === 'node_modules' ? + resolvePathAssumingWeAreInNodeModules : + resolvePathConservatively; + + const depthKey = Symbol('depth'); + const recordsKey = Symbol('records'); + const wrapComponentIdKey = Symbol('wrapComponentId'); + + function isRenderMethod(member) { + return member.kind === 'method' && + member.key.name === 'render'; + } + + /** + * Does this class have a render function? + */ + function isComponentishClass(cls) { + return cls.body.body.filter(isRenderMethod).length > 0; + } + + const isCreateClassCallExpression = t.buildMatchMemberExpression('React.createClass'); + /** + * Does this node look like a createClass() call? + */ + function isCreateClass(node) { + if (!node || !t.isCallExpression(node)) { + return false; + } + if (!isCreateClassCallExpression(node.callee)) { + return false; + } + const args = node.arguments; + if (args.length !== 1) { + return false; + } + const first = args[0]; + if (!t.isObjectExpression(first)) { + return false; + } + return true; + } + + /** + * Infers a displayName from either a class node, or a createClass() call node. + */ + function findDisplayName(node) { + if (node.id) { + return node.id.name; + } + if (!node.arguments) { + return; + } + const props = node.arguments[0].properties; + for (let i = 0; i < props.length; i++) { + const prop = props[i]; + const key = t.toComputedKey(prop); + if (t.isLiteral(key, { value: 'displayName' })) { + return prop.value.value; + } + } + } + + /** + * Enforces plugin options to be defined and returns them. + */ + function getPluginOptions(file) { + if (!file.opts || !file.opts.extra) { + return; + } + const pluginOptions = file.opts.extra['babel-plugin-wrap-react-components']; + if (!Array.isArray(pluginOptions) || pluginOptions.length === 0) { + throw new Error( + 'babel-plugin-wrap-react-components requires that you specify ' + + 'extras["babel-plugin-wrap-react-components"] in .babelrc ' + + 'or in your Babel Node API call options, and that it is an array ' + + 'with more than zero elements.' + ); + } + return pluginOptions; + } + + /** + * Creates a record about us having visited a valid React component. + * Such records will later be merged into a single object. + */ + function createComponentRecord(node, scope, file, state) { + const displayName = findDisplayName(node) || undefined; + const uniqueId = scope.generateUidIdentifier( + '$' + (displayName || 'Unknown') + ).name; + + let props = []; + if (typeof displayName === 'string') { + props.push(t.property('init', + t.identifier('displayName'), + t.literal(displayName) + )); + } + if (state[depthKey] > 0) { + props.push(t.property('init', + t.identifier('isInFunction'), + t.literal(true) + )); + } + + return [uniqueId, t.objectExpression(props)]; + } + + /** + * Memorizes the fact that we have visited a valid component in the plugin state. + * We will later retrieved memorized records to compose an object out of them. + */ + function addComponentRecord(node, scope, file, state) { + const [uniqueId, definition] = createComponentRecord(node, scope, file, state); + state[recordsKey] = state[recordsKey] || []; + state[recordsKey].push(t.property('init', + t.identifier(uniqueId), + definition + )); + return uniqueId; + } + + /** + * Have we visited any components so far? + */ + function foundComponentRecords(state) { + const records = state[recordsKey]; + return records && records.length > 0; + } + + /** + * Turns all component records recorded so far, into a variable. + */ + function defineComponentRecords(scope, state) { + const records = state[recordsKey]; + state[recordsKey] = []; + + const id = scope.generateUidIdentifier('components'); + return [id, t.variableDeclaration('var', [ + t.variableDeclarator(id, t.objectExpression(records)) + ])]; + } + + /** + * Imports and calls a particular transformation target function. + * You may specify several such transformations, so they are handled separately. + */ + function defineInitTransformCall(scope, file, recordsId, targetOptions) { + const id = scope.generateUidIdentifier('reactComponentWrapper'); + const { target, imports = [], locals = [] } = targetOptions; + const { filename } = file.opts; + + return [id, t.variableDeclaration('var', [ + t.variableDeclarator(id, + t.callExpression(file.addImport(resolvePath(target, filename)), [ + t.objectExpression([ + t.property('init', t.identifier('filename'), t.literal(filename)), + t.property('init', t.identifier('components'), recordsId), + t.property('init', t.identifier('locals'), t.arrayExpression( + locals.map(local => t.identifier(local)) + )), + t.property('init', t.identifier('imports'), t.arrayExpression( + imports.map(imp => file.addImport(resolvePath(imp, filename), imp, 'absolute')) + )) + ]) + ]) + ) + ])]; + } + + /** + * Defines the function that calls every transform. + * This is the function every component will be wrapped with. + */ + function defineWrapComponent(wrapComponentId, initTransformIds) { + return t.functionDeclaration(wrapComponentId, [t.identifier('uniqueId')], + t.blockStatement([ + t.returnStatement( + t.functionExpression(null, [t.identifier('ReactClass')], t.blockStatement([ + t.returnStatement( + initTransformIds.reduce((composed, initTransformId) => + t.callExpression(initTransformId, [composed, t.identifier('uniqueId')]), + t.identifier('ReactClass') + ) + ) + ])) + ) + ]) + ); + } + + return new Plugin('babel-plugin-wrap-react-components', { + visitor: { + Function: { + enter(node, parent, scope, file) { + if (!this.state[depthKey]) { + this.state[depthKey] = 0; + } + this.state[depthKey]++; + }, + exit(node, parent, scope, file) { + this.state[depthKey]--; + } + }, + + Class(node, parent, scope, file) { + if (!isComponentishClass(node)) { + return; + } + + const wrapReactComponentId = this.state[wrapComponentIdKey]; + const uniqueId = addComponentRecord(node, scope, file, this.state); + + node.decorators = node.decorators || []; + node.decorators.push(t.decorator( + t.callExpression(wrapReactComponentId, [t.literal(uniqueId)]) + )); + }, + + CallExpression: { + exit(node, parent, scope, file) { + if (!isCreateClass(node)) { + return; + } + + const wrapReactComponentId = this.state[wrapComponentIdKey]; + const uniqueId = addComponentRecord(node, scope, file, this.state); + + return t.callExpression( + t.callExpression(wrapReactComponentId, [t.literal(uniqueId)]), + [node] + ); + } + }, + + Program: { + enter(node, parent, scope, file) { + this.state[wrapComponentIdKey] = scope.generateUidIdentifier('wrapComponent'); + }, + + exit(node, parent, scope, file) { + if (!foundComponentRecords(this.state)) { + return; + } + + // Generate a variable holding component records + const allTransformOptions = getPluginOptions(file); + const [recordsId, recordsVar] = defineComponentRecords(scope, this.state); + + // Import transformation functions and initialize them + const initTransformCalls = allTransformOptions.map(transformOptions => + defineInitTransformCall(scope, file, recordsId, transformOptions) + ); + const initTransformIds = initTransformCalls.map(c => c[0]); + const initTransformVars = initTransformCalls.map(c => c[1]); + + // Create one uber function calling each transformation + const wrapComponentId = this.state[wrapComponentIdKey]; + const wrapComponent = defineWrapComponent(wrapComponentId, initTransformIds); + + return t.program([ + recordsVar, + ...initTransformVars, + wrapComponent, + ...node.body + ]); + } + } + } + }); +} diff --git a/test/fixtures/classic/actual.js b/test/fixtures/classic/actual.js new file mode 100644 index 0000000..0b6bc89 --- /dev/null +++ b/test/fixtures/classic/actual.js @@ -0,0 +1,87 @@ +var React = require('react'); +var connect = require('some-hoc'); +var twice = require('other-hoc'); + +var A = React.createClass({ + displayName: 'A', + + render() {} +}); + +var A2 = connect(twice(React.createClass({ + displayName: 'A2', + + render() {} +}))); + +module.exports.B = React.createClass({ + displayName: 'B', + + render() {} +}); + +module.exports.B2 = connect(twice(React.createClass({ + displayName: 'B2', + + render() {} +}))); + +var more = { + C: React.createClass({ + displayName: 'C', + + render() {} + }), + + C2: connect(twice(React.createClass({ + displayName: 'C2', + + render() {} + }))), + + nested: { + D: React.createClass({ + displayName: 'D', + + render() {} + }), + + D2: connect(twice(React.createClass({ + displayName: 'D2', + + render() {} + }))) + } +}; + +export default React.createClass({ + displayName: 'E', + + render() {} +}); + +var Untitled = React.createClass({ + render() {} +}); + +var DynamicName = React.createClass({ + displayName: Math.random(), + + render() {} +}); + +var Something = (Math.random() > .5) ? + React.createClass({ displayName: 'ComponentInsideCondition', render() { } }) : + React.createClass({ displayName: 'AnotherComponentInsideCondition', render() { } }); + +function factory() { + var ComponentInsideFunction = React.createClass({ + displayName: 'ComponentInsideFunction', + render() { } + }); + + return React.createClass({ + displayName: 'ComponentInsideFunction', + render() { } + }); +} diff --git a/test/fixtures/classic/expected.js b/test/fixtures/classic/expected.js new file mode 100644 index 0000000..9b06844 --- /dev/null +++ b/test/fixtures/classic/expected.js @@ -0,0 +1,169 @@ +'use strict'; + +var _myCustomModuleWrap2 = require('my-custom-module/wrap'); + +var _myCustomModuleWrap3 = _interopRequireDefault(_myCustomModuleWrap2); + +var _react = require('react'); + +var _myOtherCustomModuleWrap2 = require('my-other-custom-module/wrap'); + +var _myOtherCustomModuleWrap3 = _interopRequireDefault(_myOtherCustomModuleWrap2); + +Object.defineProperty(exports, '__esModule', { + value: true +}); +var _components = { + _$A: { + displayName: 'A' + }, + _$A2: { + displayName: 'A2' + }, + _$B: { + displayName: 'B' + }, + _$B2: { + displayName: 'B2' + }, + _$C: { + displayName: 'C' + }, + _$C2: { + displayName: 'C2' + }, + _$D: { + displayName: 'D' + }, + _$D2: { + displayName: 'D2' + }, + _$E: { + displayName: 'E' + }, + _$Unknown: {}, + _$Unknown2: {}, + _$ComponentInsideCondition: { + displayName: 'ComponentInsideCondition' + }, + _$AnotherComponentInsideCondition: { + displayName: 'AnotherComponentInsideCondition' + }, + _$ComponentInsideFunction: { + displayName: 'ComponentInsideFunction', + isInFunction: true + }, + _$ComponentInsideFunction2: { + displayName: 'ComponentInsideFunction', + isInFunction: true + } +}; + +var _reactComponentWrapper = (0, _myCustomModuleWrap3['default'])({ + filename: '%FIXTURE_PATH%', + components: _components, + locals: [module], + imports: [_react] +}); + +var _reactComponentWrapper2 = (0, _myOtherCustomModuleWrap3['default'])({ + filename: '%FIXTURE_PATH%', + components: _components, + locals: [], + imports: [] +}); + +function _wrapComponent(uniqueId) { + return function (ReactClass) { + return _reactComponentWrapper2(_reactComponentWrapper(ReactClass, uniqueId), uniqueId); + }; +} + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var React = require('react'); +var connect = require('some-hoc'); +var twice = require('other-hoc'); + +var A = _wrapComponent('_$A')(React.createClass({ + displayName: 'A', + + render: function render() {} +})); + +var A2 = connect(twice(_wrapComponent('_$A2')(React.createClass({ + displayName: 'A2', + + render: function render() {} +})))); + +module.exports.B = _wrapComponent('_$B')(React.createClass({ + displayName: 'B', + + render: function render() {} +})); + +module.exports.B2 = connect(twice(_wrapComponent('_$B2')(React.createClass({ + displayName: 'B2', + + render: function render() {} +})))); + +var more = { + C: _wrapComponent('_$C')(React.createClass({ + displayName: 'C', + + render: function render() {} + })), + + C2: connect(twice(_wrapComponent('_$C2')(React.createClass({ + displayName: 'C2', + + render: function render() {} + })))), + + nested: { + D: _wrapComponent('_$D')(React.createClass({ + displayName: 'D', + + render: function render() {} + })), + + D2: connect(twice(_wrapComponent('_$D2')(React.createClass({ + displayName: 'D2', + + render: function render() {} + })))) + } +}; + +exports['default'] = _wrapComponent('_$E')(React.createClass({ + displayName: 'E', + + render: function render() {} +})); + +var Untitled = _wrapComponent('_$Unknown')(React.createClass({ + render: function render() {} +})); + +var DynamicName = _wrapComponent('_$Unknown2')(React.createClass({ + displayName: Math.random(), + + render: function render() {} +})); + +var Something = Math.random() > .5 ? _wrapComponent('_$ComponentInsideCondition')(React.createClass({ displayName: 'ComponentInsideCondition', render: function render() {} })) : _wrapComponent('_$AnotherComponentInsideCondition')(React.createClass({ displayName: 'AnotherComponentInsideCondition', render: function render() {} })); + +function factory() { + var ComponentInsideFunction = _wrapComponent('_$ComponentInsideFunction')(React.createClass({ + displayName: 'ComponentInsideFunction', + render: function render() {} + })); + + return _wrapComponent('_$ComponentInsideFunction2')(React.createClass({ + displayName: 'ComponentInsideFunction', + render: function render() {} + })); +} +module.exports = exports['default']; \ No newline at end of file diff --git a/test/fixtures/modern/actual.js b/test/fixtures/modern/actual.js new file mode 100644 index 0000000..d9d8aea --- /dev/null +++ b/test/fixtures/modern/actual.js @@ -0,0 +1,21 @@ +class NotComponent { + bender() {} +} + +class LikelyComponent { + render() {} +} + +let Something = (Math.random() > .5) ? + class ComponentInsideCondition { render() { } } : + class AnotherComponentInsideCondition { render() { } }; + +function factory() { + class ComponentInsideFunction { + render() { } + } + + return class ComponentInsideFunction { + render() { } + }; +} diff --git a/test/fixtures/modern/expected.js b/test/fixtures/modern/expected.js new file mode 100644 index 0000000..edbd654 --- /dev/null +++ b/test/fixtures/modern/expected.js @@ -0,0 +1,145 @@ +"use strict"; + +var _myCustomModuleWrap2 = require("my-custom-module/wrap"); + +var _myCustomModuleWrap3 = _interopRequireDefault(_myCustomModuleWrap2); + +var _react = require("react"); + +var _myOtherCustomModuleWrap2 = require("my-other-custom-module/wrap"); + +var _myOtherCustomModuleWrap3 = _interopRequireDefault(_myOtherCustomModuleWrap2); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var _components = { + _$LikelyComponent: { + displayName: "LikelyComponent" + }, + _$ComponentInsideCondition: { + displayName: "ComponentInsideCondition" + }, + _$AnotherComponentInsideCondition: { + displayName: "AnotherComponentInsideCondition" + }, + _$ComponentInsideFunction: { + displayName: "ComponentInsideFunction", + isInFunction: true + }, + _$ComponentInsideFunction2: { + displayName: "ComponentInsideFunction", + isInFunction: true + } +}; + +var _reactComponentWrapper = (0, _myCustomModuleWrap3["default"])({ + filename: "%FIXTURE_PATH%", + components: _components, + locals: [module], + imports: [_react] +}); + +var _reactComponentWrapper2 = (0, _myOtherCustomModuleWrap3["default"])({ + filename: "%FIXTURE_PATH%", + components: _components, + locals: [], + imports: [] +}); + +function _wrapComponent(uniqueId) { + return function (ReactClass) { + return _reactComponentWrapper2(_reactComponentWrapper(ReactClass, uniqueId), uniqueId); + }; +} + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } + +var NotComponent = (function () { + function NotComponent() { + _classCallCheck(this, NotComponent); + } + + _createClass(NotComponent, [{ + key: "bender", + value: function bender() {} + }]); + + return NotComponent; +})(); + +var LikelyComponent = (function () { + function LikelyComponent() { + _classCallCheck(this, _LikelyComponent); + } + + _createClass(LikelyComponent, [{ + key: "render", + value: function render() {} + }]); + + var _LikelyComponent = LikelyComponent; + LikelyComponent = _wrapComponent("_$LikelyComponent")(LikelyComponent) || LikelyComponent; + return LikelyComponent; +})(); + +var Something = Math.random() > .5 ? (function () { + function ComponentInsideCondition() { + _classCallCheck(this, _ComponentInsideCondition); + } + + _createClass(ComponentInsideCondition, [{ + key: "render", + value: function render() {} + }]); + + var _ComponentInsideCondition = ComponentInsideCondition; + ComponentInsideCondition = _wrapComponent("_$ComponentInsideCondition")(ComponentInsideCondition) || ComponentInsideCondition; + return ComponentInsideCondition; +})() : (function () { + function AnotherComponentInsideCondition() { + _classCallCheck(this, _AnotherComponentInsideCondition); + } + + _createClass(AnotherComponentInsideCondition, [{ + key: "render", + value: function render() {} + }]); + + var _AnotherComponentInsideCondition = AnotherComponentInsideCondition; + AnotherComponentInsideCondition = _wrapComponent("_$AnotherComponentInsideCondition")(AnotherComponentInsideCondition) || AnotherComponentInsideCondition; + return AnotherComponentInsideCondition; +})(); + +function factory() { + var ComponentInsideFunction = (function () { + function ComponentInsideFunction() { + _classCallCheck(this, _ComponentInsideFunction); + } + + _createClass(ComponentInsideFunction, [{ + key: "render", + value: function render() {} + }]); + + var _ComponentInsideFunction = ComponentInsideFunction; + ComponentInsideFunction = _wrapComponent("_$ComponentInsideFunction")(ComponentInsideFunction) || ComponentInsideFunction; + return ComponentInsideFunction; + })(); + + return (function () { + function ComponentInsideFunction() { + _classCallCheck(this, _ComponentInsideFunction2); + } + + _createClass(ComponentInsideFunction, [{ + key: "render", + value: function render() {} + }]); + + var _ComponentInsideFunction2 = ComponentInsideFunction; + ComponentInsideFunction = _wrapComponent("_$ComponentInsideFunction2")(ComponentInsideFunction) || ComponentInsideFunction; + return ComponentInsideFunction; + })(); +} diff --git a/test/fixtures/vanilla/actual.js b/test/fixtures/vanilla/actual.js new file mode 100644 index 0000000..875a7a8 --- /dev/null +++ b/test/fixtures/vanilla/actual.js @@ -0,0 +1,7 @@ +class NotAComponent { + bender() {} +} + +var Stuff = Recat.createGlass({ + bender() {} +}) diff --git a/test/fixtures/vanilla/expected.js b/test/fixtures/vanilla/expected.js new file mode 100644 index 0000000..61f3237 --- /dev/null +++ b/test/fixtures/vanilla/expected.js @@ -0,0 +1,22 @@ +"use strict"; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var NotAComponent = (function () { + function NotAComponent() { + _classCallCheck(this, NotAComponent); + } + + _createClass(NotAComponent, [{ + key: "bender", + value: function bender() {} + }]); + + return NotAComponent; +})(); + +var Stuff = Recat.createGlass({ + bender: function bender() {} +}); diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..7ac38e2 --- /dev/null +++ b/test/index.js @@ -0,0 +1,36 @@ +import path from 'path'; +import fs from 'fs'; +import assert from 'assert'; +import { transformFileSync } from 'babel'; +import plugin from '../src'; + +function trim(str) { + return str.replace(/^\s+|\s+$/, ''); +} + +describe('finds React components', () => { + const fixturesDir = path.join(__dirname, 'fixtures'); + fs.readdirSync(fixturesDir).map((caseName) => { + it(`should ${caseName.split('-').join(' ')}`, () => { + const fixtureDir = path.join(fixturesDir, caseName); + const actualPath = path.join(fixtureDir, 'actual.js'); + const actual = transformFileSync(actualPath, { + plugins: [plugin], + extra: { + 'babel-plugin-wrap-react-components': [{ + target: 'my-custom-module/wrap', + locals: ['module'], + imports: ['react'] + }, { + target: 'my-other-custom-module/wrap' + }] + } + }).code; + const expected = fs.readFileSync( + path.join(fixtureDir, 'expected.js') + ).toString().replace(/%FIXTURE_PATH%/g, actualPath); + + assert.equal(trim(actual), trim(expected)); + }); + }); +});